Skip to content
Open
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
72 changes: 72 additions & 0 deletions js/examples/nextjs/app/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,16 @@ const RP_ID = process.env.NEXT_PUBLIC_RP_ID;
const STAGING_CONNECT_BASE_URL = "https://staging.world.org/verify";
const CONNECT_URL_OVERRIDE_TOOLTIP =
"Enable this to change the deeplink base URL to the staging verify endpoint. Useful when testing with a Staging iOS World App build that supports this override.";
const RETURN_TO_TOOLTIP =
"Enable this to append a return_to callback to the connector URL. The default value just reopens Chrome, and you can override it before starting a verification.";

type PresetKind = "orb" | "secure_document" | "document" | "device" | "selfie";

function createChromeAppDeeplink(url: string): string {
const parsed = new URL(url);
return parsed.protocol === "https:" ? "googlechromes://" : "googlechrome://";
}

function createPreset(kind: PresetKind, signal: string) {
switch (kind) {
case "orb":
Expand Down Expand Up @@ -113,6 +120,9 @@ export function DemoClient(): ReactElement {
const [useStagingConnectBaseUrl, setUseStagingConnectBaseUrl] =
useState(false);
const [isConnectUrlTooltipOpen, setIsConnectUrlTooltipOpen] = useState(false);
const [useReturnTo, setUseReturnTo] = useState(false);
const [returnTo, setReturnTo] = useState("");
const [isReturnToTooltipOpen, setIsReturnToTooltipOpen] = useState(false);

const widgetPreset = useMemo(
() => createPreset(widgetPresetKind, widgetSignal),
Expand All @@ -122,6 +132,9 @@ export function DemoClient(): ReactElement {
environment === "staging" && useStagingConnectBaseUrl
? STAGING_CONNECT_BASE_URL
: undefined;
const effectiveReturnTo = useReturnTo
? returnTo.trim() || undefined
: undefined;

useEffect(() => {
document.documentElement.setAttribute(
Expand All @@ -137,6 +150,18 @@ export function DemoClient(): ReactElement {
}
}, [environment]);

useEffect(() => {
if (typeof window === "undefined") {
return;
}

setReturnTo((current) =>
current.length > 0
? current
: createChromeAppDeeplink(window.location.href),
);
}, []);

const startWidgetFlow = async (presetKind: PresetKind) => {
setWidgetError(null);
setWidgetVerifyResult(null);
Expand Down Expand Up @@ -256,6 +281,52 @@ export function DemoClient(): ReactElement {
<span className="config-note">{STAGING_CONNECT_BASE_URL}</span>
</div>
)}
<div className="config-row">
<label htmlFor="cfgReturnToEnabled">Return to</label>
<div
className="tooltip"
onMouseEnter={() => setIsReturnToTooltipOpen(true)}
onMouseLeave={() => setIsReturnToTooltipOpen(false)}
>
<button
type="button"
className="tooltip-trigger"
aria-label="Explain return_to"
aria-describedby={
isReturnToTooltipOpen ? "return-to-tooltip" : undefined
}
aria-expanded={isReturnToTooltipOpen}
onFocus={() => setIsReturnToTooltipOpen(true)}
onBlur={() => setIsReturnToTooltipOpen(false)}
onClick={() => setIsReturnToTooltipOpen(true)}
>
?
</button>
{isReturnToTooltipOpen && (
<span
id="return-to-tooltip"
role="tooltip"
className="tooltip-content"
>
{RETURN_TO_TOOLTIP}
</span>
)}
</div>
<input
type="checkbox"
id="cfgReturnToEnabled"
checked={useReturnTo}
onChange={(e) => setUseReturnTo(e.target.checked)}
/>
<input
type="text"
id="cfgReturnTo"
value={returnTo}
onChange={(e) => setReturnTo(e.target.value)}
disabled={!useReturnTo}
placeholder="googlechromes://"
/>
</div>
</section>

<div className="stack">
Expand Down Expand Up @@ -294,6 +365,7 @@ export function DemoClient(): ReactElement {
}}
environment={environment}
override_connect_base_url={overrideConnectBaseUrl}
return_to={effectiveReturnTo}
/>
)}

Expand Down
1 change: 1 addition & 0 deletions js/packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const request = await IDKit.request({
signature: rpSig.sig,
},
allow_legacy_proofs: false,
return_to: "myapp://idkit/callback",
}).preset(orbLegacy({ signal: "user-123" }));

// Display QR code for World App
Expand Down
6 changes: 6 additions & 0 deletions js/packages/core/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ function createWasmBuilderFromConfig(
config.bridge_url ?? null,
config.allow_legacy_proofs ?? false,
config.override_connect_base_url ?? null,
config.return_to ?? null,
config.environment ?? null,
);
}
Expand All @@ -383,6 +384,7 @@ function createWasmBuilderFromConfig(
config.action_description ?? null,
config.bridge_url ?? null,
config.override_connect_base_url ?? null,
config.return_to ?? null,
config.environment ?? null,
);
}
Expand All @@ -394,6 +396,7 @@ function createWasmBuilderFromConfig(
config.action_description ?? null,
config.bridge_url ?? null,
config.override_connect_base_url ?? null,
config.return_to ?? null,
config.environment ?? null,
);
}
Expand Down Expand Up @@ -602,6 +605,7 @@ function createRequest(config: IDKitRequestConfig): IDKitBuilder {
rp_context: config.rp_context,
action_description: config.action_description,
bridge_url: config.bridge_url,
return_to: config.return_to,
allow_legacy_proofs: config.allow_legacy_proofs,
override_connect_base_url: config.override_connect_base_url,
environment: config.environment,
Expand Down Expand Up @@ -651,6 +655,7 @@ function createSession(config: IDKitSessionConfig): IDKitBuilder {
rp_context: config.rp_context,
action_description: config.action_description,
bridge_url: config.bridge_url,
return_to: config.return_to,
override_connect_base_url: config.override_connect_base_url,
environment: config.environment,
});
Expand Down Expand Up @@ -705,6 +710,7 @@ function proveSession(
rp_context: config.rp_context,
action_description: config.action_description,
bridge_url: config.bridge_url,
return_to: config.return_to,
override_connect_base_url: config.override_connect_base_url,
environment: config.environment,
});
Expand Down
1 change: 1 addition & 0 deletions js/packages/core/src/transports/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface BuilderConfig {
rp_context?: import("../types/config").RpContext;
action_description?: string;
bridge_url?: string;
return_to?: string;
allow_legacy_proofs?: boolean;
override_connect_base_url?: string;
environment?: string;
Expand Down
4 changes: 4 additions & 0 deletions js/packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export type IDKitRequestConfig = {
action_description?: string;
/** URL to a third-party bridge to use when connecting to the World App. Optional. */
bridge_url?: string;
/** Optional deep-link callback URL appended as `return_to` on the connector URL. */
return_to?: string;

/**
* Whether to accept legacy (v3) World ID proofs as fallback.
Expand Down Expand Up @@ -80,6 +82,8 @@ export type IDKitSessionConfig = {
action_description?: string;
/** URL to a third-party bridge to use when connecting to the World App. Optional. */
bridge_url?: string;
/** Optional deep-link callback URL appended as `return_to` on the connector URL. */
return_to?: string;
/** Optional override for the connect base URL (e.g., for staging environments) */
override_connect_base_url?: string;

Expand Down
2 changes: 2 additions & 0 deletions js/packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function Example() {
action: "my-action",
rp_context,
allow_legacy_proofs: false,
return_to: "myapp://idkit/callback",
preset: orbLegacy({ signal: "user-123" }),
});
const isBusy =
Expand Down Expand Up @@ -72,6 +73,7 @@ function WidgetExample() {
action="my-action"
rp_context={rpContext}
allow_legacy_proofs={false}
return_to="myapp://idkit/callback"
preset={orbLegacy({ signal: "user-123" })}
onSuccess={(result) => {
// required: runs after verification succeeds
Expand Down
142 changes: 142 additions & 0 deletions js/packages/react/src/__tests__/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ describe("request/session hooks", () => {
});

expect(requestMock).toHaveBeenCalledTimes(1);
expect(requestMock).toHaveBeenCalledWith({
app_id: "app_test",
action: "test-action",
rp_context: baseRpContext,
action_description: undefined,
bridge_url: undefined,
return_to: undefined,
allow_legacy_proofs: false,
override_connect_base_url: undefined,
environment: undefined,
});
expect(result.current.connectorURI).toBe("wc://request");
expect(result.current.result).toEqual({ proof: "ok" });
});
Expand Down Expand Up @@ -151,6 +162,15 @@ describe("request/session hooks", () => {
});

expect(createSessionMock).toHaveBeenCalledTimes(1);
expect(createSessionMock).toHaveBeenCalledWith({
app_id: "app_test",
rp_context: baseRpContext,
action_description: undefined,
bridge_url: undefined,
override_connect_base_url: undefined,
return_to: undefined,
environment: undefined,
});
expect(proveSessionMock).not.toHaveBeenCalled();
expect(result.current.result?.session_id).toBe("session_1");
});
Expand Down Expand Up @@ -189,11 +209,133 @@ describe("request/session hooks", () => {
action_description: undefined,
bridge_url: undefined,
override_connect_base_url: undefined,
return_to: undefined,
environment: undefined,
});
expect(result.current.result?.session_id).toBe("session_2");
});

it("request hook forwards return_to to core", async () => {
requestMock.mockReturnValue({
preset: vi.fn(async () =>
makeRequest(async () => ({
type: "confirmed",
result: { proof: "ok" },
})),
),
});

const { result } = renderHook(() =>
useIDKitRequest({
app_id: "app_test",
action: "test-action",
rp_context: baseRpContext,
allow_legacy_proofs: false,
return_to: "idkit://callback?step=proof",
preset: { type: "OrbLegacy" },
}),
);

act(() => {
result.current.open();
});

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(requestMock).toHaveBeenCalledWith({
app_id: "app_test",
action: "test-action",
rp_context: baseRpContext,
action_description: undefined,
bridge_url: undefined,
return_to: "idkit://callback?step=proof",
allow_legacy_proofs: false,
override_connect_base_url: undefined,
environment: undefined,
});
});

it("session hook forwards return_to to createSession", async () => {
createSessionMock.mockReturnValue({
preset: vi.fn(async () => ({
connectorURI: "wc://session-create",
pollOnce: vi.fn(async () => ({
type: "confirmed",
result: { session_id: "session_1", responses: [] },
})),
})),
});

const { result } = renderHook(() =>
useIDKitSession({
app_id: "app_test",
rp_context: baseRpContext,
return_to: "idkit://callback?step=create",
preset: { type: "OrbLegacy" },
}),
);

act(() => {
result.current.open();
});

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(createSessionMock).toHaveBeenCalledWith({
app_id: "app_test",
rp_context: baseRpContext,
action_description: undefined,
bridge_url: undefined,
override_connect_base_url: undefined,
return_to: "idkit://callback?step=create",
environment: undefined,
});
});

it("session hook forwards return_to to proveSession", async () => {
proveSessionMock.mockReturnValue({
preset: vi.fn(async () => ({
connectorURI: "wc://session-prove",
pollOnce: vi.fn(async () => ({
type: "confirmed",
result: { session_id: "session_2", responses: [] },
})),
})),
});

const { result } = renderHook(() =>
useIDKitSession({
app_id: "app_test",
rp_context: baseRpContext,
existing_session_id: "session_2",
return_to: "idkit://callback?step=prove",
preset: { type: "OrbLegacy" },
}),
);

act(() => {
result.current.open();
});

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(proveSessionMock).toHaveBeenCalledWith("session_2", {
app_id: "app_test",
rp_context: baseRpContext,
action_description: undefined,
bridge_url: undefined,
override_connect_base_url: undefined,
return_to: "idkit://callback?step=prove",
environment: undefined,
});
});

it("session hook fails on empty existing_session_id", async () => {
const { result } = renderHook(() =>
useIDKitSession({
Expand Down
1 change: 1 addition & 0 deletions js/packages/react/src/hooks/useIDKitRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function useIDKitRequest(
rp_context: config.rp_context,
action_description: config.action_description,
bridge_url: config.bridge_url,
return_to: config.return_to,
allow_legacy_proofs: config.allow_legacy_proofs,
override_connect_base_url: config.override_connect_base_url,
environment: config.environment,
Expand Down
Loading
Loading