From c6803edc85b4e78e5e48de2ad2097f4674a58ae9 Mon Sep 17 00:00:00 2001 From: Takis Kakalis <80459599+Takaros999@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:19:30 -0700 Subject: [PATCH] feat(react): add compact qr handoff option --- .../react/src/__tests__/QRState.test.tsx | 22 ++++++- .../react/src/components/States/QRState.tsx | 60 ++++++++++++++++--- js/packages/react/src/lang/translations/en.ts | 3 + js/packages/react/src/lang/translations/es.ts | 3 + js/packages/react/src/lang/translations/th.ts | 3 + js/packages/react/src/lang/types.ts | 3 + js/packages/react/src/styles/widgetStyles.ts | 47 +++++++++++++++ 7 files changed, 132 insertions(+), 9 deletions(-) diff --git a/js/packages/react/src/__tests__/QRState.test.tsx b/js/packages/react/src/__tests__/QRState.test.tsx index f1abe1a8..830f2f49 100644 --- a/js/packages/react/src/__tests__/QRState.test.tsx +++ b/js/packages/react/src/__tests__/QRState.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { QRState } from "../components/States/QRState"; @@ -36,4 +36,24 @@ describe("QRState simulator callout", () => { screen.queryByRole("link", { name: /use the simulator/i }), ).toBeNull(); }); + + it("offers both app handoff and a QR fallback on smaller screens", () => { + render(); + + expect( + screen + .getByRole("link", { name: /open world app/i }) + .getAttribute("href"), + ).toBe(qrData); + expect(screen.getAllByTestId("qr-code")).toHaveLength(1); + + fireEvent.click(screen.getByRole("button", { name: /display qr code/i })); + + expect( + screen + .getByRole("button", { name: /hide qr code/i }) + .getAttribute("aria-expanded"), + ).toBe("true"); + expect(screen.getAllByTestId("qr-code")).toHaveLength(2); + }); }); diff --git a/js/packages/react/src/components/States/QRState.tsx b/js/packages/react/src/components/States/QRState.tsx index 95d58d53..f0b0df10 100644 --- a/js/packages/react/src/components/States/QRState.tsx +++ b/js/packages/react/src/components/States/QRState.tsx @@ -1,6 +1,5 @@ import { useCallback, useState, type ReactElement } from "react"; import { __ } from "../../lang"; -import { useMedia } from "../../hooks/useMedia"; import { QRCode } from "../../widget/QRCode"; import { QRPlaceholderIcon } from "../Icons/QRPlaceholderIcon"; import { WorldcoinIcon } from "../Icons/WorldIcon"; @@ -14,8 +13,8 @@ export function QRState({ qrData, showSimulatorCallout, }: QRStateProps): ReactElement { - const media = useMedia(); const [copiedLink, setCopiedLink] = useState(false); + const [showMobileQR, setShowMobileQR] = useState(false); const copyLink = useCallback(() => { if (!qrData) return; @@ -26,12 +25,57 @@ export function QRState({ return ( <> - {/* Mobile: deep-link button */} + {/* Mobile and tablet: deep link with a QR fallback */}
- - - {__("Open World App")} - +
+ + + {__("Open World App")} + +
+
+ + {showMobileQR && ( +
+
+ {__("QR Code copied")} +
+
+
+ {qrData ? ( +
{ + if (event.key === "Enter") copyLink(); + }} + role="button" + tabIndex={0} + style={{ cursor: "pointer" }} + > + +
+ ) : ( +
+ +
+ )} +
+
+
+ )} +
{/* Desktop: QR code */} @@ -58,7 +102,7 @@ export function QRState({ tabIndex={0} style={{ cursor: "pointer" }} > - + ) : (
diff --git a/js/packages/react/src/lang/translations/en.ts b/js/packages/react/src/lang/translations/en.ts index db330dbf..9bae6cc4 100644 --- a/js/packages/react/src/lang/translations/en.ts +++ b/js/packages/react/src/lang/translations/en.ts @@ -30,6 +30,9 @@ export const en: TranslationStrings = { "Try Again": "Try Again", Close: "Close", "Open World App": "Open World App", + "Display QR Code": "Display QR Code", + "Hide QR Code": "Hide QR Code", + or: "or", "QR Code copied": "QR Code copied", "Connect your World ID": "Connect your World ID", "Use phone camera to scan the QR code": diff --git a/js/packages/react/src/lang/translations/es.ts b/js/packages/react/src/lang/translations/es.ts index 772fd2e6..11facfbc 100644 --- a/js/packages/react/src/lang/translations/es.ts +++ b/js/packages/react/src/lang/translations/es.ts @@ -31,6 +31,9 @@ export const es: TranslationStrings = { "Try Again": "Intentar de nuevo", Close: "Cerrar", "Open World App": "Abrir World App", + "Display QR Code": "Mostrar c\u00f3digo QR", + "Hide QR Code": "Ocultar c\u00f3digo QR", + or: "o", "QR Code copied": "C\u00f3digo QR copiado", "Connect your World ID": "Conecta tu World ID", "Use phone camera to scan the QR code": diff --git a/js/packages/react/src/lang/translations/th.ts b/js/packages/react/src/lang/translations/th.ts index cbf376e7..f7c2c45e 100644 --- a/js/packages/react/src/lang/translations/th.ts +++ b/js/packages/react/src/lang/translations/th.ts @@ -40,6 +40,9 @@ export const th: TranslationStrings = { "\u0e25\u0e2d\u0e07\u0e2d\u0e35\u0e01\u0e04\u0e23\u0e31\u0e49\u0e07", Close: "\u0e1b\u0e34\u0e14", "Open World App": "\u0e40\u0e1b\u0e34\u0e14 World App", + "Display QR Code": "\u0e41\u0e2a\u0e14\u0e07 QR Code", + "Hide QR Code": "\u0e0b\u0e48\u0e2d\u0e19 QR Code", + or: "\u0e2b\u0e23\u0e37\u0e2d", "QR Code copied": "\u0e04\u0e31\u0e14\u0e25\u0e2d\u0e01 QR Code \u0e41\u0e25\u0e49\u0e27", "Connect your World ID": diff --git a/js/packages/react/src/lang/types.ts b/js/packages/react/src/lang/types.ts index 78bc6bc6..ef2a3c81 100644 --- a/js/packages/react/src/lang/types.ts +++ b/js/packages/react/src/lang/types.ts @@ -26,6 +26,9 @@ export interface TranslationStrings { "Try Again": string; Close: string; "Open World App": string; + "Display QR Code": string; + "Hide QR Code": string; + or: string; "QR Code copied": string; "Connect your World ID": string; "Use phone camera to scan the QR code": string; diff --git a/js/packages/react/src/styles/widgetStyles.ts b/js/packages/react/src/styles/widgetStyles.ts index 2d395c2d..f4da3cad 100644 --- a/js/packages/react/src/styles/widgetStyles.ts +++ b/js/packages/react/src/styles/widgetStyles.ts @@ -344,6 +344,53 @@ export const WIDGET_STYLES = ` min-width: 0; } +.idkit-mobile-handoff { + display: flex; + width: 100%; + flex-direction: column; + align-items: center; + gap: 16px; +} +.idkit-mobile-handoff .idkit-deeplink-btn { + width: 100%; +} +.idkit-handoff-divider { + display: flex; + width: 100%; + align-items: center; + gap: 16px; + color: var(--idkit-text-muted); + font-size: 12px; + font-weight: 500; +} +.idkit-handoff-divider > span:first-child, +.idkit-handoff-divider > span:last-child { + height: 1px; + flex: 1; + background: var(--idkit-border-light); +} +.idkit-qr-toggle-btn { + box-sizing: border-box; + width: 100%; + border: 1px solid var(--idkit-border); + border-radius: 16px; + background: transparent; + color: var(--idkit-text); + padding: 16px 32px; + font-family: var(--idkit-font); + font-size: 16px; + font-weight: 500; + cursor: pointer; +} +.idkit-mobile-qr { + position: relative; +} +.idkit-mobile-qr .idkit-copy-toast { + top: 100%; + bottom: auto; + margin: 8px 0 0; +} + /* Loading spinner */ .idkit-spinner { animation: idkit-spin 1s linear infinite;