diff --git a/src/main/ipc/mobile.test.ts b/src/main/ipc/mobile.test.ts index e8cc2edb319..e5357089d82 100644 --- a/src/main/ipc/mobile.test.ts +++ b/src/main/ipc/mobile.test.ts @@ -15,6 +15,7 @@ vi.mock('electron', () => ({ vi.mock('qrcode', () => ({ default: { + create: vi.fn().mockReturnValue({ modules: { size: 21 } }), toDataURL: vi.fn().mockResolvedValue('data:image/png;base64,qr') } })) @@ -344,6 +345,7 @@ describe('registerMobileHandlers', () => { await expect(handlers.get('mobile:getPairingQR')?.(null, {})).resolves.toMatchObject({ available: true, + qrSize: 58, pairingUrl: 'orca://pair#mobile', endpoint: 'ws://100.102.47.57:6768', deviceId: 'mobile-1', @@ -422,6 +424,7 @@ describe('registerMobileHandlers', () => { ).resolves.toEqual({ available: true, qrDataUrl: null, + qrSize: null, qrError: 'encoding_failed', pairingUrl: 'orca://pair?code=copy-me', endpoint: 'wss://pair.example/oversized', diff --git a/src/main/ipc/mobile.ts b/src/main/ipc/mobile.ts index d76cb880342..e7eb3345aa5 100644 --- a/src/main/ipc/mobile.ts +++ b/src/main/ipc/mobile.ts @@ -132,6 +132,7 @@ export function registerMobileHandlers( return { available: true as const, qrDataUrl: qr.ok ? qr.qrDataUrl : null, + qrSize: qr.ok ? qr.qrSize : null, ...(!qr.ok ? { qrError: qr.reason } : {}), pairingUrl: offer.pairingUrl, // Why: with nothing advertised the offer's endpoint is the loopback fallback, which points at diff --git a/src/main/runtime/mobile-pairing-qr.test.ts b/src/main/runtime/mobile-pairing-qr.test.ts index 3c860cbf6eb..708b7c7bda6 100644 --- a/src/main/runtime/mobile-pairing-qr.test.ts +++ b/src/main/runtime/mobile-pairing-qr.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { PNG } from 'pngjs' import { encodePairingOffer } from '../../shared/pairing' import type { PairingOffer } from '../../shared/mobile-relay-pairing-offer' import { encodeMobilePairingQr } from './mobile-pairing-qr' @@ -49,6 +50,30 @@ async function discoverEndpointBoundary(relay: boolean): Promise { } describe('encodeMobilePairingQr', () => { + it('renders the current Relay-sized symbol at two pixels per module with a four-module quiet zone', async () => { + const { default: QRCode } = await import('qrcode') + const url = pairingUrl(100, true) + const moduleCount = QRCode.create(url, { errorCorrectionLevel: 'M' }).modules.size + expect(moduleCount).toBe(101) + + const result = await encodeMobilePairingQr(url) + expect(result.ok).toBe(true) + if (!result.ok) { + return + } + + const image = PNG.sync.read(Buffer.from(result.qrDataUrl.split(',')[1]!, 'base64')) + expect(result.qrSize).toBe(218) + expect(image.width).toBe(result.qrSize) + expect(image.height).toBe(result.qrSize) + expect(image.data.subarray((8 * image.width + 7) * 4, (8 * image.width + 8) * 4)).toEqual( + Buffer.from([255, 255, 255, 255]) + ) + expect(image.data.subarray((8 * image.width + 8) * 4, (8 * image.width + 9) * 4)).toEqual( + Buffer.from([0, 0, 0, 255]) + ) + }) + it.each([ ['direct', false], ['relay', true] diff --git a/src/main/runtime/mobile-pairing-qr.ts b/src/main/runtime/mobile-pairing-qr.ts index 8cb28494424..ad0964bf578 100644 --- a/src/main/runtime/mobile-pairing-qr.ts +++ b/src/main/runtime/mobile-pairing-qr.ts @@ -1,16 +1,21 @@ export type MobilePairingQrResult = - | { ok: true; qrDataUrl: string } + | { ok: true; qrDataUrl: string; qrSize: number } | { ok: false; reason: 'encoding_failed' } +const QUIET_ZONE_MODULES = 4 +const MODULE_PITCH_PX = 2 + export async function encodeMobilePairingQr(pairingUrl: string): Promise { try { const { default: QRCode } = await import('qrcode') + const qrCode = QRCode.create(pairingUrl, { errorCorrectionLevel: 'M' }) + const qrSize = (qrCode.modules.size + QUIET_ZONE_MODULES * 2) * MODULE_PITCH_PX const qrDataUrl = await QRCode.toDataURL(pairingUrl, { errorCorrectionLevel: 'M', - margin: 2, - width: 256 + margin: QUIET_ZONE_MODULES, + scale: MODULE_PITCH_PX }) - return { ok: true, qrDataUrl } + return { ok: true, qrDataUrl, qrSize } } catch { return { ok: false, reason: 'encoding_failed' } } diff --git a/src/preload/api/mobile-api.ts b/src/preload/api/mobile-api.ts index 02e82f34044..5ec8943838c 100644 --- a/src/preload/api/mobile-api.ts +++ b/src/preload/api/mobile-api.ts @@ -22,6 +22,8 @@ export type MobileApi = { | { available: true qrDataUrl: string | null + /** Natural bitmap width and height in pixels. */ + qrSize: number | null qrError?: 'encoding_failed' pairingUrl: string /** Null when no direct address was advertised — the QR pairs over Relay alone. */ diff --git a/src/preload/index.ts b/src/preload/index.ts index f1deda11071..937a91153fe 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -4884,6 +4884,8 @@ const api = { | { available: true qrDataUrl: string | null + /** Natural bitmap width and height in pixels. */ + qrSize: number | null qrError?: 'encoding_failed' pairingUrl: string /** Null when no direct address was advertised — the QR pairs over Relay alone. */ diff --git a/src/renderer/src/assets/mobile-page-qr-layout.test.ts b/src/renderer/src/assets/mobile-page-qr-layout.test.ts index b08d4201208..ed7b0bea7e5 100644 --- a/src/renderer/src/assets/mobile-page-qr-layout.test.ts +++ b/src/renderer/src/assets/mobile-page-qr-layout.test.ts @@ -4,8 +4,11 @@ import { describe, expect, it } from 'vitest' const mobilePageCss = fs.readFileSync(new URL('./mobile-page.css', import.meta.url), 'utf8') describe('mobile page QR grid layout (#9700)', () => { - it('defines a shared large-QR size token used by the box and grid tracks', () => { + it('defines separate install and adaptive pairing QR tracks', () => { expect(mobilePageCss).toMatch(/--mp-qr-large-size:\s*184px/) + expect(mobilePageCss).toMatch( + /--mp-pairing-qr-frame-size:\s*calc\(var\(--mp-pairing-qr-image-size\) \+ 20px\)/ + ) expect(mobilePageCss).toMatch( /\.mobile-page-root \.mp-qr-large\s*{[^}]*width:\s*var\(--mp-qr-large-size\)/s ) @@ -18,7 +21,7 @@ describe('mobile page QR grid layout (#9700)', () => { /\.mobile-page-root \.mp-step2-layout\s*{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)\s+var\(--mp-qr-large-size\)/s ) expect(mobilePageCss).toMatch( - /\.mobile-page-root \.mp-pairing-layout\s*{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)\s+var\(--mp-qr-large-size\)/s + /\.mobile-page-root \.mp-pairing-layout\s*{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)\s+var\(--mp-pairing-qr-frame-size\)/s ) expect(mobilePageCss).not.toMatch( /\.mobile-page-root \.mp-(?:step2|pairing)-layout\s*{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)\s+auto/s diff --git a/src/renderer/src/assets/mobile-page.css b/src/renderer/src/assets/mobile-page.css index 55d07eff54e..dba75298285 100644 --- a/src/renderer/src/assets/mobile-page.css +++ b/src/renderer/src/assets/mobile-page.css @@ -38,6 +38,8 @@ --mp-flow-card-inset: calc(var(--mp-flow-card-padding) + var(--mp-flow-card-border-width)); --mp-flow-shell-height: 400px; --mp-qr-large-size: 184px; + --mp-pairing-qr-image-size: 164px; + --mp-pairing-qr-frame-size: calc(var(--mp-pairing-qr-image-size) + 20px); --m-bg-base: #111111; --m-bg-panel: #1a1a1a; --m-bg-raised: #242424; @@ -596,6 +598,17 @@ height: calc(var(--mp-qr-large-size) - 20px); } +.mobile-page-root .mp-pairing-qr .mp-qr-large { + width: var(--mp-pairing-qr-frame-size); + height: var(--mp-pairing-qr-frame-size); +} + +.mobile-page-root .mp-pairing-qr .mp-qr-large img { + width: var(--mp-pairing-qr-image-size); + height: var(--mp-pairing-qr-image-size); + image-rendering: pixelated; +} + .mobile-page-root .mp-qr-refreshing { filter: blur(5px); opacity: 0.32; @@ -686,7 +699,7 @@ .mobile-page-root .mp-pairing-layout { display: grid; - grid-template-columns: minmax(0, 1fr) var(--mp-qr-large-size); + grid-template-columns: minmax(0, 1fr) var(--mp-pairing-qr-frame-size); grid-template-areas: 'copy qr' 'relay qr' diff --git a/src/renderer/src/components/mobile/MobileHero.test.tsx b/src/renderer/src/components/mobile/MobileHero.test.tsx index bbde9d2cfd2..83827c24643 100644 --- a/src/renderer/src/components/mobile/MobileHero.test.tsx +++ b/src/renderer/src/components/mobile/MobileHero.test.tsx @@ -218,6 +218,15 @@ describe('HeroFlow height', () => { expect(screen.queryByTestId('relay-mint-failure-notice')).not.toBeInTheDocument() }) + it('renders a pairing QR at its natural integer-scaled bitmap size', () => { + renderFlow(1, { pairQrDataUrl: 'data:image/png;base64,qr', pairQrSize: 218 }) + + const image = screen.getByRole('img', { name: 'Pairing QR' }) + const layout = image.closest('.mp-pairing-layout') as HTMLElement + expect(layout.style.getPropertyValue('--mp-pairing-qr-image-size')).toBe('218px') + expect(layout.style.getPropertyValue('--mp-pairing-qr-frame-size')).toBe('238px') + }) + it('shows an encoder error while keeping the copy fallback enabled', () => { renderFlow(1, { pairingQrError: true, diff --git a/src/renderer/src/components/mobile/MobileHero.tsx b/src/renderer/src/components/mobile/MobileHero.tsx index e99b7c67369..a30aebdaaa6 100644 --- a/src/renderer/src/components/mobile/MobileHero.tsx +++ b/src/renderer/src/components/mobile/MobileHero.tsx @@ -25,6 +25,7 @@ type HeroFlowProps = { onOpenInstallUrl: () => void onCopyInstallUrl: () => void pairQrDataUrl: string | null + pairQrSize?: number | null pairingUrl: string | null pairingQrError: boolean relayMintFailure: MobileRelayMintFailure | null @@ -63,6 +64,7 @@ export function HeroFlow({ onOpenInstallUrl, onCopyInstallUrl, pairQrDataUrl, + pairQrSize = null, pairingUrl, pairingQrError, relayMintFailure, @@ -222,6 +224,7 @@ export function HeroFlow({ > void refreshingNetworkInterfaces: boolean }): React.JSX.Element { + const pairingLayoutStyle = + pairQrSize == null + ? undefined + : ({ + '--mp-pairing-qr-image-size': `${pairQrSize}px`, + '--mp-pairing-qr-frame-size': `${pairQrSize + 20}px` + } as React.CSSProperties) const copyPairingCodeRef = useRef(null) const pairingWasReadyRef = useRef(pairingUrl != null && !pairLoading) const usingRelay = connectionMode === 'automatic' @@ -169,7 +178,10 @@ export function MobileHeroPairingStep({ ) return ( -
+
2
diff --git a/src/renderer/src/components/mobile/MobilePage.test.tsx b/src/renderer/src/components/mobile/MobilePage.test.tsx index c3e08e9dd34..41b7fe0fff8 100644 --- a/src/renderer/src/components/mobile/MobilePage.test.tsx +++ b/src/renderer/src/components/mobile/MobilePage.test.tsx @@ -56,6 +56,7 @@ vi.mock('./MobilePageContent', () => ({ beforeCustomAddressChange: (address: string) => Promise handleContinue: () => void pairQrDataUrl: string | null + pairQrSize: number | null pairingUrl: string | null pairingQrError: boolean relayMintFailure: MobileRelayMintFailure | null @@ -72,6 +73,7 @@ vi.mock('./MobilePageContent', () => ({ {props.connectionMode} {String(props.canGeneratePairing)} {props.pairQrDataUrl ?? 'none'} + {props.pairQrSize ?? 'none'} {props.pairingUrl ?? 'none'} {String(props.pairingQrError)} {props.relayMintFailure?.stage ?? 'none'} @@ -132,6 +134,7 @@ describe('MobilePage pairing connection mode', () => { getPairingQR.mockReset().mockResolvedValue({ available: true, qrDataUrl: 'data:image/png;base64,qr', + qrSize: 218, pairingUrl: 'orca://pair#automatic' }) listNetworkInterfaces.mockReset().mockResolvedValue({ interfaces: [] }) @@ -171,6 +174,7 @@ describe('MobilePage pairing connection mode', () => { await waitFor(() => expect(getPairingQR).toHaveBeenCalledWith({ connectionMode: 'automatic' })) await waitFor(() => expect(screen.getByTestId('pairing-qr')).toHaveTextContent('base64,qr')) + expect(screen.getByTestId('pairing-qr-size')).toHaveTextContent('218') expect(screen.getByTestId('mode')).toHaveTextContent('automatic') let resolveRotatedLocalQr: ((value: Record) => void) | undefined diff --git a/src/renderer/src/components/mobile/MobilePage.tsx b/src/renderer/src/components/mobile/MobilePage.tsx index c6a258f3283..fc1cb19f712 100644 --- a/src/renderer/src/components/mobile/MobilePage.tsx +++ b/src/renderer/src/components/mobile/MobilePage.tsx @@ -33,6 +33,7 @@ export default function MobilePage(): React.JSX.Element { const [iosChannel, setIosChannel] = useState('preview') const [pairQrDataUrl, setPairQrDataUrl] = useState(null) + const [pairQrSize, setPairQrSize] = useState(null) const [pairingUrl, setPairingUrl] = useState(null) const [pairingQrError, setPairingQrError] = useState(false) const [relayMintFailure, setRelayMintFailure] = useState(null) @@ -85,6 +86,7 @@ export default function MobilePage(): React.JSX.Element { hasGeneratedRef, pairingRequestIdRef, setPairQrDataUrl, + setPairQrSize, setPairingUrl, setPairingQrError, setPairLoading, @@ -109,6 +111,7 @@ export default function MobilePage(): React.JSX.Element { pairingRequestIdRef.current += 1 hasGeneratedRef.current = false setPairQrDataUrl(null) + setPairQrSize(null) setPairingUrl(null) setPairingQrError(false) setRelayMintFailure(null) @@ -171,6 +174,7 @@ export default function MobilePage(): React.JSX.Element { hasGeneratedRef, pairingRequestIdRef, setPairQrDataUrl, + setPairQrSize, setPairingUrl, setPairingQrError, setPairLoading, @@ -262,6 +266,7 @@ export default function MobilePage(): React.JSX.Element { const enterFlow = (): void => { hasGeneratedRef.current = false setPairQrDataUrl(null) + setPairQrSize(null) setPairingUrl(null) setPairingQrError(false) setRelayMintFailure(null) @@ -273,6 +278,7 @@ export default function MobilePage(): React.JSX.Element { const pairAnotherDevice = (): void => { hasGeneratedRef.current = false setPairQrDataUrl(null) + setPairQrSize(null) setPairingUrl(null) setPairingQrError(false) setRelayMintFailure(null) @@ -328,6 +334,7 @@ export default function MobilePage(): React.JSX.Element { connectionMode={connectionMode} handleConnectionModeChange={handleConnectionModeChange} pairQrDataUrl={pairQrDataUrl} + pairQrSize={pairQrSize} pairingUrl={pairingUrl} pairingQrError={pairingQrError} relayMintFailure={ diff --git a/src/renderer/src/components/mobile/MobilePageContent.tsx b/src/renderer/src/components/mobile/MobilePageContent.tsx index c0b44c18fd5..8879ca1f720 100644 --- a/src/renderer/src/components/mobile/MobilePageContent.tsx +++ b/src/renderer/src/components/mobile/MobilePageContent.tsx @@ -42,6 +42,7 @@ type MobilePageContentProps = { connectionMode: MobilePairingConnectionMode handleConnectionModeChange: (mode: MobilePairingConnectionMode) => void pairQrDataUrl: string | null + pairQrSize: number | null pairingUrl: string | null pairingQrError: boolean relayMintFailure: MobileRelayMintFailure | null @@ -88,6 +89,7 @@ export function MobilePageContent({ connectionMode, handleConnectionModeChange, pairQrDataUrl, + pairQrSize, pairingUrl, pairingQrError, relayMintFailure, @@ -136,6 +138,7 @@ export function MobilePageContent({ onOpenInstallUrl={openInstallUrl} onCopyInstallUrl={copyInstallUrl} pairQrDataUrl={pairQrDataUrl} + pairQrSize={pairQrSize} pairingUrl={pairingUrl} pairingQrError={pairingQrError} relayMintFailure={relayMintFailure} diff --git a/src/renderer/src/components/mobile/use-mobile-pairing-generation.ts b/src/renderer/src/components/mobile/use-mobile-pairing-generation.ts index 92db1931f84..084d256e424 100644 --- a/src/renderer/src/components/mobile/use-mobile-pairing-generation.ts +++ b/src/renderer/src/components/mobile/use-mobile-pairing-generation.ts @@ -23,6 +23,7 @@ export function useMobilePairingGeneration(params: { hasGeneratedRef: MutableRef pairingRequestIdRef: MutableRef setPairQrDataUrl: (value: string | null) => void + setPairQrSize: (value: number | null) => void setPairingUrl: (value: string | null) => void setPairingQrError: (value: boolean) => void setPairLoading: (value: boolean) => void @@ -42,6 +43,7 @@ export function useMobilePairingGeneration(params: { hasGeneratedRef, pairingRequestIdRef, setPairQrDataUrl, + setPairQrSize, setPairingUrl, setPairingQrError, setPairLoading, @@ -76,6 +78,7 @@ export function useMobilePairingGeneration(params: { if (result.available) { if (mountedRef.current) { setPairQrDataUrl(result.qrDataUrl) + setPairQrSize(result.qrSize) setPairingUrl(result.pairingUrl) setPairingQrError(result.qrDataUrl === null) setRelayMintFailure(null) @@ -84,6 +87,7 @@ export function useMobilePairingGeneration(params: { // Why: keep hasGenerated so step-2 auto-mint does not loop on failure. if (mountedRef.current) { setPairQrDataUrl(null) + setPairQrSize(null) setPairingUrl(null) setPairingQrError(false) if (result.reason === 'relay_mint_failed' && result.relayFailure) { @@ -106,6 +110,7 @@ export function useMobilePairingGeneration(params: { if (mountedRef.current && requestId === pairingRequestIdRef.current) { hasGeneratedRef.current = false setPairQrDataUrl(null) + setPairQrSize(null) setPairingUrl(null) setPairingQrError(false) setRelayMintFailure(null) @@ -130,6 +135,7 @@ export function useMobilePairingGeneration(params: { selectedAddress, setPairLoading, setPairQrDataUrl, + setPairQrSize, setPairingUrl, setPairingQrError, setRelayMintFailure, diff --git a/src/renderer/src/components/mobile/use-mobile-pairing-qr-invalidation.ts b/src/renderer/src/components/mobile/use-mobile-pairing-qr-invalidation.ts index 69cf51eb734..ee6be21eecd 100644 --- a/src/renderer/src/components/mobile/use-mobile-pairing-qr-invalidation.ts +++ b/src/renderer/src/components/mobile/use-mobile-pairing-qr-invalidation.ts @@ -20,6 +20,7 @@ export function useMobilePairingQrInvalidation(params: { hasGeneratedRef: MutableRef pairingRequestIdRef: MutableRef setPairQrDataUrl: (value: string | null) => void + setPairQrSize: (value: number | null) => void setPairingUrl: (value: string | null) => void setPairingQrError: (value: boolean) => void setPairLoading: (value: boolean) => void @@ -33,6 +34,7 @@ export function useMobilePairingQrInvalidation(params: { hasGeneratedRef, pairingRequestIdRef, setPairQrDataUrl, + setPairQrSize, setPairingUrl, setPairingQrError, setPairLoading, @@ -59,6 +61,7 @@ export function useMobilePairingQrInvalidation(params: { setPairingUrl(null) setPairingQrError(false) setPairQrDataUrl(null) + setPairQrSize(null) setRelayMintFailure?.(null) if (signedIn && canMintMobilePairingOffer({ connectionMode, signedIn })) { // Why: rotate on the sign-in edge — the token behind the QR cleared at @@ -73,6 +76,7 @@ export function useMobilePairingQrInvalidation(params: { hasGeneratedRef, pairingRequestIdRef, setPairQrDataUrl, + setPairQrSize, setPairingUrl, setPairingQrError, setPairLoading, @@ -97,6 +101,7 @@ export function useMobilePairingQrInvalidation(params: { setPairingUrl(null) setPairingQrError(false) setPairQrDataUrl(null) + setPairQrSize(null) setRelayMintFailure?.(null) if (shouldRegenerate && canMintMobilePairingOffer({ connectionMode, signedIn })) { // Why: no rotate here — the main process rotates exactly once when the @@ -115,6 +120,7 @@ export function useMobilePairingQrInvalidation(params: { hasGeneratedRef, pairingRequestIdRef, setPairQrDataUrl, + setPairQrSize, setPairingUrl, setPairingQrError, setPairLoading, diff --git a/src/renderer/src/components/settings/MobilePairingQrSection.test.tsx b/src/renderer/src/components/settings/MobilePairingQrSection.test.tsx index 9d62603a9c0..834e1982ce1 100644 --- a/src/renderer/src/components/settings/MobilePairingQrSection.test.tsx +++ b/src/renderer/src/components/settings/MobilePairingQrSection.test.tsx @@ -98,4 +98,28 @@ describe('MobilePairingQrSection', () => { expect(persistentAction).toHaveFocus() }) + + it('keeps the QR on integer CSS scaling at normal and enlarged sizes', () => { + render( + + ) + + const images = Array.from( + document.querySelectorAll('img[alt="QR Code for mobile pairing"]') + ) + expect(images.map((image) => image.style.width).sort()).toEqual(['218px', '436px']) + expect(images.map((image) => image.style.height).sort()).toEqual(['218px', '436px']) + expect(images.every((image) => image.style.imageRendering === 'pixelated')).toBe(true) + }) }) diff --git a/src/renderer/src/components/settings/MobilePairingQrSection.tsx b/src/renderer/src/components/settings/MobilePairingQrSection.tsx index b444d3412f7..d977bd29b01 100644 --- a/src/renderer/src/components/settings/MobilePairingQrSection.tsx +++ b/src/renderer/src/components/settings/MobilePairingQrSection.tsx @@ -7,6 +7,7 @@ import { translate } from '@/i18n/i18n' type MobilePairingQrSectionProps = { qrDataUrl: string | null + qrSize?: number | null qrError: boolean pairingUrl: string | null endpoint: string | null @@ -19,6 +20,7 @@ type MobilePairingQrSectionProps = { export function MobilePairingQrSection({ qrDataUrl, + qrSize = null, qrError, pairingUrl, endpoint, @@ -28,6 +30,8 @@ export function MobilePairingQrSection({ onCodeCopiedChange, onClearCodeCopiedTimer }: MobilePairingQrSectionProps): React.JSX.Element | null { + const renderedQrSize = qrSize ?? 192 + const enlargedQrSize = qrSize == null ? 288 : qrSize * 2 const pairingCodeButtonMountedRef = useRef(false) const pairingCodeButtonRef = useRef(null) const hadPairingUrlRef = useRef(pairingUrl != null) @@ -102,7 +106,12 @@ export function MobilePairingQrSection({ 'auto.components.settings.MobilePane.6436e56546', 'QR Code for mobile pairing' )} - className="size-48" + className="block" + style={{ + width: renderedQrSize, + height: renderedQrSize, + imageRendering: 'pixelated' + }} /> @@ -158,7 +167,7 @@ export function MobilePairingQrSection({ {qrDataUrl ? ( - + {translate( @@ -175,7 +184,12 @@ export function MobilePairingQrSection({ 'auto.components.settings.MobilePane.6436e56546', 'QR Code for mobile pairing' )} - className="size-72" + className="block" + style={{ + width: enlargedQrSize, + height: enlargedQrSize, + imageRendering: 'pixelated' + }} />
{endpoint && ( diff --git a/src/renderer/src/components/settings/MobilePane.tsx b/src/renderer/src/components/settings/MobilePane.tsx index 8dafdf21cef..39c6df203c0 100644 --- a/src/renderer/src/components/settings/MobilePane.tsx +++ b/src/renderer/src/components/settings/MobilePane.tsx @@ -31,6 +31,7 @@ export function MobilePane(): React.JSX.Element { const autoRestoreFitMs = useAppStore((s) => s.settings?.mobileAutoRestoreFitMs ?? null) const updateSettings = useAppStore((s) => s.updateSettings) const [qrDataUrl, setQrDataUrl] = useState(null) + const [qrSize, setQrSize] = useState(null) const [pairingUrl, setPairingUrl] = useState(null) const [qrError, setQrError] = useState(false) const [relayMintFailure, setRelayMintFailure] = useState(null) @@ -81,6 +82,7 @@ export function MobilePane(): React.JSX.Element { pairingRequestIdRef.current += 1 const hadPending = qrDisplayedRef.current || loadingRef.current setQrDataUrl(null) + setQrSize(null) setPairingUrl(null) setQrError(false) setRelayMintFailure(null) @@ -197,6 +199,7 @@ export function MobilePane(): React.JSX.Element { useAppStore.getState().recordFeatureInteraction('mobile-pairing') if (mountedRef.current) { setQrDataUrl(result.qrDataUrl) + setQrSize(result.qrSize) setPairingUrl(result.pairingUrl) setQrError(result.qrDataUrl === null) setRelayMintFailure(null) @@ -209,6 +212,7 @@ export function MobilePane(): React.JSX.Element { } } else if (mountedRef.current) { setQrDataUrl(null) + setQrSize(null) setPairingUrl(null) setQrError(false) setEndpoint(null) @@ -432,6 +436,7 @@ export function MobilePane(): React.JSX.Element {