Skip to content
Draft
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
625 changes: 466 additions & 159 deletions js/examples/browser/index.html

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion js/examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"dependencies": {
"@worldcoin/idkit-core": "workspace:*",
"dotenv": "^16.4.5",
"express": "^5.1.0"
"express": "^5.1.0",
"i18n-iso-countries": "^7.14.0"
},
"devDependencies": {
"@types/express": "^5.0.2",
Expand Down
183 changes: 168 additions & 15 deletions js/examples/nextjs/app/arena/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ type ArenaCaseId =
| "v4_constraint_unsatisfied"
| "v4_future_genesis_no_fallback"
| "v4_future_genesis_fallback_success"
| "user_presence_v3_orb_failed"
| "user_presence_v3_secure_document_failed"
| "user_presence_v4_poh_failed"
| "user_presence_v4_passport_failed"
| "user_presence_v4_mnc_failed"
| "error_invalid_rp_signature"
| "error_duplicate_nonce"
| "error_nullifier_replayed"
Expand Down Expand Up @@ -105,6 +110,7 @@ type ArenaCaseDefinition = {
description: string;
prerequisite: string;
request: RequestDefinition;
requireUserPresence?: boolean;
contextCaseId?: RpContextCaseId;
};

Expand Down Expand Up @@ -147,6 +153,7 @@ type ActiveRun = {
rpContext: RpContext;
signal: string;
request: RequestDefinition;
requireUserPresence: boolean;
};

type FlowConfig =
Expand All @@ -163,6 +170,7 @@ const APP_ID = process.env.NEXT_PUBLIC_APP_ID as `app_${string}` | undefined;
const RP_ID = process.env.NEXT_PUBLIC_RP_ID;
const STAGING_CONNECT_BASE_URL = "https://staging.world.org/verify";
const FUTURE_GENESIS_OFFSET_SEC = 365 * 24 * 60 * 60;
const CONFIGURABLE_ALLOW_LEGACY_SECTION_ID = "world-id-4-success";

const PRESET_KIND_TO_NAME: Record<PresetKind, string> = {
orb: "Orb / Proof of Human",
Expand Down Expand Up @@ -251,7 +259,7 @@ const ARENA_SECTIONS: ArenaSection[] = [
expected: "success",
expectedProtocol: "4.0",
prerequisite: "Account has a 4.0 Proof of Human credential.",
description: "Requests proof_of_human with legacy fallback disabled.",
description: "Requests proof_of_human as a 4.0 success path.",
request: {
type: "constraints",
constraintKind: "proof_of_human",
Expand All @@ -264,7 +272,7 @@ const ARENA_SECTIONS: ArenaSection[] = [
expected: "success",
expectedProtocol: "4.0",
prerequisite: "Account has a 4.0 Passport credential.",
description: "Requests passport with legacy fallback disabled.",
description: "Requests passport as a 4.0 success path.",
request: {
type: "constraints",
constraintKind: "passport",
Expand All @@ -277,7 +285,7 @@ const ARENA_SECTIONS: ArenaSection[] = [
expected: "success",
expectedProtocol: "4.0",
prerequisite: "Account has a 4.0 MNC credential.",
description: "Requests mnc with legacy fallback disabled.",
description: "Requests mnc as a 4.0 success path.",
request: {
type: "constraints",
constraintKind: "mnc",
Expand Down Expand Up @@ -461,6 +469,81 @@ const ARENA_SECTIONS: ArenaSection[] = [
},
],
},
{
id: "user-presence-face-auth",
title: "User presence (Face Auth)",
description:
"Requests that require Face Auth and should fail when the user reaches Face Auth but does not complete it.",
cases: [
{
id: "user_presence_v3_orb_failed",
title: "WID 3.0 Orb with Face Auth",
expected: IDKitErrorCodes.UserPresenceFailed,
prerequisite:
"Account can satisfy the 3.0 Orb preset. Reach the Face Auth step and fail or cancel that step.",
description:
"Runs the Orb legacy preset with require_user_presence=true and expects user_presence_failed.",
request: { type: "preset", presetKind: "orb" },
requireUserPresence: true,
},
{
id: "user_presence_v3_secure_document_failed",
title: "WID 3.0 Secure Document with Face Auth",
expected: IDKitErrorCodes.UserPresenceFailed,
prerequisite:
"Account can satisfy the secure document legacy preset. Reach the Face Auth step and fail or cancel that step.",
description:
"Runs the secure document legacy preset with require_user_presence=true and expects user_presence_failed.",
request: { type: "preset", presetKind: "secure_document" },
requireUserPresence: true,
},
{
id: "user_presence_v4_poh_failed",
title: "WID 4.0 Proof of Human with Face Auth",
expected: IDKitErrorCodes.UserPresenceFailed,
prerequisite:
"Account has a 4.0 Proof of Human credential. Reach the Face Auth step and fail or cancel that step.",
description:
"Requests proof_of_human with require_user_presence=true and expects user_presence_failed.",
request: {
type: "constraints",
constraintKind: "proof_of_human",
allowLegacyProofs: false,
},
requireUserPresence: true,
},
{
id: "user_presence_v4_passport_failed",
title: "WID 4.0 Passport with Face Auth",
expected: IDKitErrorCodes.UserPresenceFailed,
prerequisite:
"Account has a 4.0 Passport credential. Reach the Face Auth step and fail or cancel that step.",
description:
"Requests passport with require_user_presence=true and expects user_presence_failed.",
request: {
type: "constraints",
constraintKind: "passport",
allowLegacyProofs: false,
},
requireUserPresence: true,
},
{
id: "user_presence_v4_mnc_failed",
title: "WID 4.0 MNC with Face Auth",
expected: IDKitErrorCodes.UserPresenceFailed,
prerequisite:
"Account has a 4.0 MNC credential. Reach the Face Auth step and fail or cancel that step.",
description:
"Requests mnc with require_user_presence=true and expects user_presence_failed.",
request: {
type: "constraints",
constraintKind: "mnc",
allowLegacyProofs: false,
},
requireUserPresence: true,
},
],
},
{
id: "world-id-4-errors",
title: "World ID 4.0 Error Cases",
Expand Down Expand Up @@ -641,6 +724,24 @@ function caseExpectedLabel(testCase: ArenaCaseDefinition): string {
return expectedLabel(testCase.expected, testCase.expectedProtocol);
}

function withSectionAllowLegacyProofs(
request: RequestDefinition,
sectionId: string,
allowLegacyProofs: boolean,
): RequestDefinition {
if (
sectionId !== CONFIGURABLE_ALLOW_LEGACY_SECTION_ID ||
request.type !== "constraints"
) {
return request;
}

return {
...request,
allowLegacyProofs,
};
}

function makeAction(caseId: ArenaCaseId, actionPrefix: string): string {
const base = actionPrefix.trim() || "idkit-arena";
if (caseId === "error_nullifier_replayed") {
Expand Down Expand Up @@ -747,18 +848,33 @@ function buildFlowConfig(activeRun: ActiveRun): FlowConfig {
};
}

function requestLabel(request: RequestDefinition): string {
function requestLabel(
request: RequestDefinition,
requireUserPresence = false,
): string {
const userPresence = requireUserPresence ? "require_user_presence=true" : "";

if (request.type === "preset") {
return `preset: ${PRESET_KIND_TO_NAME[request.presetKind]}`;
return userPresence
? `preset: ${PRESET_KIND_TO_NAME[request.presetKind]} (${userPresence})`
: `preset: ${PRESET_KIND_TO_NAME[request.presetKind]}`;
}

const flags = [
request.allowLegacyProofs
? "allow_legacy_proofs=true"
: "allow_legacy_proofs=false",
];

if (request.genesisIssuedAtMin === "future") {
flags.push("future genesis min");
}

const fallback = request.allowLegacyProofs
? "allow_legacy_proofs=true"
: "allow_legacy_proofs=false";
const genesis =
request.genesisIssuedAtMin === "future" ? ", future genesis min" : "";
if (userPresence) {
flags.push(userPresence);
}

return `${CONSTRAINT_KIND_TO_NAME[request.constraintKind]} (${fallback}${genesis})`;
return `${CONSTRAINT_KIND_TO_NAME[request.constraintKind]} (${flags.join(", ")})`;
}

function resultProtocol(result: IDKitResult): ExpectedProtocol {
Expand Down Expand Up @@ -822,6 +938,8 @@ export function ArenaClient(): ReactElement {
const [useReturnTo, setUseReturnTo] = useState(false);
const [returnTo, setReturnTo] = useState("");
const [actionPrefix, setActionPrefix] = useState("idkit-arena");
const [allowLegacyProofsForV4Success, setAllowLegacyProofsForV4Success] =
useState(false);
const [results, setResults] = useState<
Partial<Record<ArenaCaseId, TestCaseResult>>
>({});
Expand Down Expand Up @@ -963,6 +1081,7 @@ export function ArenaClient(): ReactElement {
rpContext: response.rpContext,
signal: makeSignal(testCase.id, action),
request: testCase.request,
requireUserPresence: testCase.requireUserPresence ?? false,
});
setWidgetOpen(true);

Expand Down Expand Up @@ -1200,12 +1319,35 @@ export function ArenaClient(): ReactElement {
{ARENA_SECTIONS.map((section) => (
<section className="arena-section" key={section.id}>
<div className="arena-section-header">
<h2>{section.title}</h2>
<p>{section.description}</p>
<div>
<h2>{section.title}</h2>
<p>{section.description}</p>
</div>
{section.id === CONFIGURABLE_ALLOW_LEGACY_SECTION_ID && (
<label
className="arena-section-toggle"
htmlFor="arenaV4SuccessAllowLegacyProofs"
>
<input
type="checkbox"
id="arenaV4SuccessAllowLegacyProofs"
checked={allowLegacyProofsForV4Success}
onChange={(event) =>
setAllowLegacyProofsForV4Success(event.target.checked)
}
/>
<span>allow_legacy_proofs</span>
</label>
)}
</div>

<div className="arena-cases">
{section.cases.map((testCase) => {
const request = withSectionAllowLegacyProofs(
testCase.request,
section.id,
allowLegacyProofsForV4Success,
);
const result = results[testCase.id] ?? {
status: "idle",
message: "Not run",
Expand All @@ -1228,7 +1370,12 @@ export function ArenaClient(): ReactElement {
<dl className="test-case-meta">
<div>
<dt>Request</dt>
<dd>{requestLabel(testCase.request)}</dd>
<dd>
{requestLabel(
request,
testCase.requireUserPresence ?? false,
)}
</dd>
</div>
<div>
<dt>Prerequisite</dt>
Expand All @@ -1245,7 +1392,12 @@ export function ArenaClient(): ReactElement {
<div className="test-case-actions">
<button
type="button"
onClick={() => void startCase(testCase)}
onClick={() =>
void startCase({
...testCase,
request,
})
}
disabled={isRunning}
>
{buttonLabel}
Expand Down Expand Up @@ -1288,6 +1440,7 @@ export function ArenaClient(): ReactElement {
? activeRun.request.allowLegacyProofs
: true
}
require_user_presence={activeRun.requireUserPresence}
{...activeFlowConfig}
handleVerify={async (result) => {
if (activeRun.expected !== "success") {
Expand Down
30 changes: 29 additions & 1 deletion js/examples/nextjs/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ button.secondary {
color: var(--text-color);
}

button:disabled {
cursor: not-allowed;
opacity: 0.55;
}

pre {
background: var(--code-bg);
color: var(--code-text);
Expand Down Expand Up @@ -127,17 +132,22 @@ pre {
display: flex;
align-items: center;
gap: 10px;
min-height: 34px;
}

.config-row > label {
min-width: 100px;
width: 200px;
flex-shrink: 0;
opacity: 0.7;
font-size: 14px;
}

.config-row input[type="text"],
.config-row input[type="number"],
.config-row input[type="datetime-local"],
.config-row select {
flex: 1;
height: 34px;
font-family: monospace;
font-size: 14px;
padding: 6px 10px;
Expand Down Expand Up @@ -220,6 +230,10 @@ pre {
}

.arena-section-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}

Expand All @@ -235,6 +249,16 @@ pre {
line-height: 1.4;
}

.arena-section-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
white-space: nowrap;
color: var(--status-color);
font-family: var(--font-mono);
font-size: 12px;
}

.test-case-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
Expand Down Expand Up @@ -333,6 +357,10 @@ pre {
grid-template-columns: 1fr;
}

.arena-section-header {
flex-direction: column;
}

.test-case-actions {
justify-content: space-between;
}
Expand Down
4 changes: 2 additions & 2 deletions js/examples/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export default function HomePage(): ReactElement {
<main>
<h1>IDKit Next.js Example</h1>
<p>
This example shows the widget request flow with the same legacy presets
as the browser example.
This example shows the widget request flow for legacy presets, World ID
4.0 credential requests, and identity checks.
</p>
<p>
<a className="nav-link" href="/arena">
Expand Down
Loading