fix(mobile): render pairing QR at scanner-safe scale (#15058)

This commit is contained in:
Jinwoo Hong
2026-08-17 10:29:18 -07:00
committed by GitHub
parent a1cd7eaa7e
commit 646e9b692b
19 changed files with 158 additions and 11 deletions
+3
View File
@@ -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',
+1
View File
@@ -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
@@ -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<number> {
}
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]
+9 -4
View File
@@ -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<MobilePairingQrResult> {
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' }
}
+2
View File
@@ -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. */
+2
View File
@@ -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. */
@@ -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
+14 -1
View File
@@ -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'
@@ -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,
@@ -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({
>
<MobileHeroPairingStep
pairQrDataUrl={pairQrDataUrl}
pairQrSize={pairQrSize}
pairingUrl={pairingUrl}
pairingQrError={pairingQrError}
relayMintFailure={relayMintFailure}
@@ -58,6 +58,7 @@ function emptyPairingQrMessage(args: {
export function MobileHeroPairingStep({
pairQrDataUrl,
pairQrSize = null,
pairingUrl,
pairingQrError,
relayMintFailure,
@@ -82,6 +83,7 @@ export function MobileHeroPairingStep({
refreshingNetworkInterfaces
}: {
pairQrDataUrl: string | null
pairQrSize?: number | null
pairingUrl: string | null
pairingQrError: boolean
relayMintFailure: MobileRelayMintFailure | null
@@ -105,6 +107,13 @@ export function MobileHeroPairingStep({
onRefreshNetworkInterfaces: () => 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<HTMLButtonElement | null>(null)
const pairingWasReadyRef = useRef(pairingUrl != null && !pairLoading)
const usingRelay = connectionMode === 'automatic'
@@ -169,7 +178,10 @@ export function MobileHeroPairingStep({
)
return (
<div className={cn('mp-pairing-layout', relayMintFailure != null && 'has-failure')}>
<div
className={cn('mp-pairing-layout', relayMintFailure != null && 'has-failure')}
style={pairingLayoutStyle}
>
<div className="mp-step2-copy mp-pairing-copy">
<div className="mp-eyebrow-row">
<div className="mp-step-num">2</div>
@@ -56,6 +56,7 @@ vi.mock('./MobilePageContent', () => ({
beforeCustomAddressChange: (address: string) => Promise<boolean>
handleContinue: () => void
pairQrDataUrl: string | null
pairQrSize: number | null
pairingUrl: string | null
pairingQrError: boolean
relayMintFailure: MobileRelayMintFailure | null
@@ -72,6 +73,7 @@ vi.mock('./MobilePageContent', () => ({
<span data-testid="mode">{props.connectionMode}</span>
<span data-testid="can-generate">{String(props.canGeneratePairing)}</span>
<span data-testid="pairing-qr">{props.pairQrDataUrl ?? 'none'}</span>
<span data-testid="pairing-qr-size">{props.pairQrSize ?? 'none'}</span>
<span data-testid="pairing-url">{props.pairingUrl ?? 'none'}</span>
<span data-testid="pairing-qr-error">{String(props.pairingQrError)}</span>
<span data-testid="relay-failure">{props.relayMintFailure?.stage ?? 'none'}</span>
@@ -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<string, unknown>) => void) | undefined
@@ -33,6 +33,7 @@ export default function MobilePage(): React.JSX.Element {
const [iosChannel, setIosChannel] = useState<IosChannel>('preview')
const [pairQrDataUrl, setPairQrDataUrl] = useState<string | null>(null)
const [pairQrSize, setPairQrSize] = useState<number | null>(null)
const [pairingUrl, setPairingUrl] = useState<string | null>(null)
const [pairingQrError, setPairingQrError] = useState(false)
const [relayMintFailure, setRelayMintFailure] = useState<MobileRelayMintFailure | null>(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={
@@ -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}
@@ -23,6 +23,7 @@ export function useMobilePairingGeneration(params: {
hasGeneratedRef: MutableRef<boolean>
pairingRequestIdRef: MutableRef<number>
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,
@@ -20,6 +20,7 @@ export function useMobilePairingQrInvalidation(params: {
hasGeneratedRef: MutableRef<boolean>
pairingRequestIdRef: MutableRef<number>
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,
@@ -98,4 +98,28 @@ describe('MobilePairingQrSection', () => {
expect(persistentAction).toHaveFocus()
})
it('keeps the QR on integer CSS scaling at normal and enlarged sizes', () => {
render(
<MobilePairingQrSection
qrDataUrl="data:image/png;base64,qr"
qrSize={218}
qrError={false}
pairingUrl="orca://pair#ready"
endpoint={null}
qrEnlarged
codeCopied={false}
onQrEnlargedChange={vi.fn()}
onCodeCopiedChange={vi.fn()}
onClearCodeCopiedTimer={vi.fn()}
/>
)
const images = Array.from(
document.querySelectorAll<HTMLImageElement>('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)
})
})
@@ -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<HTMLButtonElement | null>(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'
}}
/>
<Maximize2 className="absolute top-1.5 right-1.5 size-3 text-black/30 can-hover:opacity-0 transition-opacity group-hover:opacity-100" />
</button>
@@ -158,7 +167,7 @@ export function MobilePairingQrSection({
{qrDataUrl ? (
<Dialog open={qrEnlarged} onOpenChange={onQrEnlargedChange}>
<DialogContent className="sm:max-w-sm">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{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'
}}
/>
</div>
{endpoint && (
@@ -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<string | null>(null)
const [qrSize, setQrSize] = useState<number | null>(null)
const [pairingUrl, setPairingUrl] = useState<string | null>(null)
const [qrError, setQrError] = useState(false)
const [relayMintFailure, setRelayMintFailure] = useState<MobileRelayMintFailure | null>(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 {
<MobilePairingQrSection
qrDataUrl={qrDataUrl}
qrSize={qrSize}
qrError={qrError}
pairingUrl={pairingUrl}
endpoint={endpoint}