Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion js/packages/react/src/__tests__/QRState.test.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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(<QRState qrData={qrData} />);

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);
});
});
60 changes: 52 additions & 8 deletions js/packages/react/src/components/States/QRState.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -26,12 +25,57 @@ export function QRState({

return (
<>
{/* Mobile: deep-link button */}
{/* Mobile and tablet: deep link with a QR fallback */}
<div className="idkit-mobile-only">
<a href={qrData ?? undefined} className="idkit-deeplink-btn">
<WorldcoinIcon />
<span>{__("Open World App")}</span>
</a>
<div className="idkit-mobile-handoff">
<a href={qrData ?? undefined} className="idkit-deeplink-btn">
<WorldcoinIcon />
<span>{__("Open World App")}</span>
</a>
<div className="idkit-handoff-divider">
<span aria-hidden="true" />
<span>{__("or")}</span>
<span aria-hidden="true" />
</div>
<button
type="button"
className="idkit-qr-toggle-btn"
aria-expanded={showMobileQR}
onClick={() => setShowMobileQR((show) => !show)}
>
{showMobileQR ? __("Hide QR Code") : __("Display QR Code")}
</button>
{showMobileQR && (
<div className="idkit-mobile-qr">
Comment on lines +48 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the expanded handoff scroll within the compact modal

On compact viewports where the expanded handoff exceeds 95vh—for example, a 375×667 phone—the newly rendered QR adds roughly 186px beneath the actions, but .idkit-modal is capped at max-height: 95vh with overflow: hidden, and neither it nor .idkit-content is scrollable. Consequently, the bottom of the QR and footer are clipped with no way to reach them; the compact modal or its content needs vertical scrolling when this section is expanded.

Useful? React with 👍 / 👎.

<div
className={`idkit-copy-toast ${copiedLink ? "visible" : "hidden"}`}
>
<span>{__("QR Code copied")}</span>
</div>
<div className="idkit-qr-wrapper">
<div className="idkit-qr-inner">
{qrData ? (
<div
onClick={copyLink}
onKeyDown={(event) => {
if (event.key === "Enter") copyLink();
}}
role="button"
tabIndex={0}
style={{ cursor: "pointer" }}
>
<QRCode data={qrData} size={160} />
</div>
) : (
<div className="idkit-qr-placeholder">
<QRPlaceholderIcon />
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>

{/* Desktop: QR code */}
Expand All @@ -58,7 +102,7 @@ export function QRState({
tabIndex={0}
style={{ cursor: "pointer" }}
>
<QRCode data={qrData} size={media === "mobile" ? 160 : 200} />
<QRCode data={qrData} size={200} />
</div>
) : (
<div className="idkit-qr-placeholder">
Expand Down
3 changes: 3 additions & 0 deletions js/packages/react/src/lang/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
3 changes: 3 additions & 0 deletions js/packages/react/src/lang/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
3 changes: 3 additions & 0 deletions js/packages/react/src/lang/translations/th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
3 changes: 3 additions & 0 deletions js/packages/react/src/lang/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions js/packages/react/src/styles/widgetStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading