Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 4 additions & 1 deletion js/examples/nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import { ErudaProvider } from "./eruda";
import { MiniKitProvider } from "@worldcoin/minikit-js/minikit-provider";
import "./globals.css";

export const metadata: Metadata = {
Expand All @@ -14,7 +15,9 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<ErudaProvider>{children}</ErudaProvider>
<MiniKitProvider>
<ErudaProvider>{children}</ErudaProvider>
</MiniKitProvider>
</body>
</html>
);
Expand Down
4 changes: 3 additions & 1 deletion js/examples/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
},
"dependencies": {
"@worldcoin/idkit": "workspace:*",
"@worldcoin/minikit-js": "^1.11.0",
"eruda": "^3.4.1",
"next": "^15.4.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"viem": "^2.47.2"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

i needed this to make minikit work :/

},
"devDependencies": {
"@types/react": "^18.3.12",
Expand Down
1 change: 1 addition & 0 deletions js/packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type { IDKitErrorCode } from "./types/result";

// Utilities
export { isReactNative, isWeb, isNode } from "./lib/platform";
export { isInWorldApp } from "./transports/native";

// RP Request Signing (server-side only)
export { signRequest } from "./signing";
Expand Down
1 change: 1 addition & 0 deletions js/packages/react/src/__tests__/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ vi.mock("@worldcoin/idkit-core", () => ({
MalformedRequest: "malformed_request",
UnexpectedResponse: "unexpected_response",
},
isInWorldApp: () => false,
}));

const baseRpContext = {
Expand Down
2 changes: 2 additions & 0 deletions js/packages/react/src/hooks/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type HookState<TResult> = {
connectorURI: string | null;
result: TResult | null;
errorCode: IDKitErrorCodes | null;
isInWorldApp: boolean;
};

export function createInitialHookState<TResult>(): HookState<TResult> {
Expand All @@ -22,6 +23,7 @@ export function createInitialHookState<TResult>(): HookState<TResult> {
connectorURI: null,
result: null,
errorCode: null,
isInWorldApp: false,
};
}

Expand Down
19 changes: 14 additions & 5 deletions js/packages/react/src/hooks/useIDKitFlow.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { IDKitErrorCodes, type IDKitRequest } from "@worldcoin/idkit-core";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
IDKitErrorCodes,
isInWorldApp as isInWorldAppCheck,
type IDKitRequest,
} from "@worldcoin/idkit-core";
import type { FlowConfig, IDKitHookResult } from "../types";
import {
createInitialHookState,
Expand All @@ -16,6 +20,8 @@ export function useIDKitFlow<TResult>(
createFlowHandle: () => Promise<IDKitRequest>,
config: FlowConfig,
): IDKitHookResult<TResult> {
const isInWorldApp = useMemo(() => isInWorldAppCheck(), []);

const [state, setState] = useState<HookState<TResult>>(
createInitialHookState,
);
Expand Down Expand Up @@ -49,9 +55,10 @@ export function useIDKitFlow<TResult>(
connectorURI: null,
result: null,
errorCode: null,
isInWorldApp: isInWorldApp,
};
});
}, []);
}, [isInWorldApp]);

useEffect(() => {
if (!state.isOpen) {
Expand Down Expand Up @@ -86,11 +93,12 @@ export function useIDKitFlow<TResult>(
requestId: request.requestId,
});

const connectorURI = isInWorldApp ? null : request.connectorURI;
setState((prev) => {
if (prev.connectorURI === request.connectorURI) {
if (prev.connectorURI === connectorURI) {
return prev;
}
return { ...prev, connectorURI: request.connectorURI };
return { ...prev, connectorURI };
});

const pollInterval = configRef.current.polling?.interval ?? 1000;
Expand Down Expand Up @@ -170,5 +178,6 @@ export function useIDKitFlow<TResult>(
result: state.result,
errorCode: state.errorCode,
isOpen: state.isOpen,
isInWorldApp: isInWorldApp,
};
}
2 changes: 2 additions & 0 deletions js/packages/react/src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export type IDKitHookResult<TResult> = {
result: TResult | null;
errorCode: IDKitErrorCodes | null;
isOpen: boolean;
/** Use `isInWorldApp` to determine if the widget is running inside the World App (mini app context). */
isInWorldApp: boolean;
};
39 changes: 36 additions & 3 deletions js/packages/react/src/widget/IDKitRequestWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,46 @@ export function IDKitRequestWidget({
});
}, [effectiveErrorCode, onError]);

// Auto-close on success
// In World App context there's no UI to render HostAppVerificationState,
// so invoke handleVerify programmatically when the proof arrives.
useEffect(() => {
if (isSuccess && autoClose) {
if (
!flow.isInWorldApp ||
!isHostVerifying ||
!flow.result ||
!handleVerify
) {
return;
}

let cancelled = false;
void Promise.resolve(handleVerify(flow.result))
.then(() => {
if (!cancelled) setHostVerifyResult("passed");
})
.catch(() => {
if (!cancelled) setHostVerifyResult("failed");
});
return () => {
cancelled = true;
};
}, [flow.isInWorldApp, isHostVerifying, flow.result, handleVerify]);

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 Prevent duplicate handleVerify calls in World App mode

In World App mode this effect re-runs whenever the handleVerify prop gets a new function identity, so a parent re-render during isHostVerifying can invoke host verification multiple times for the same flow.result. Because the cancelled flag only suppresses setHostVerifyResult and does not cancel the already-started promise, duplicate backend side effects (e.g., repeated verification submissions) are still triggered; this is especially likely when integrators pass inline callbacks as shown in the widget usage examples.

Useful? React with 👍 / 👎.


// In World App there's no visible UI, so auto-close immediately on success or error.
// In bridge flow, only auto-close on success after the 2.5s delay (errors show retry UI).
useEffect(() => {
if (flow.isInWorldApp && (isSuccess || isError)) {
onOpenChange(false);
} else if (isSuccess && autoClose) {
const timer = setTimeout(() => onOpenChange(false), 2500);
return () => clearTimeout(timer);
}
}, [isSuccess, autoClose, onOpenChange]);
}, [isSuccess, isError, autoClose, onOpenChange, flow.isInWorldApp]);

// In World App context, the host app handles all UI — render nothing.
if (flow.isInWorldApp) {
return null;
}

const stage = getVisualStage(isSuccess, isError, isHostVerifying);
const showSimulatorCallout = config.environment === "staging";
Expand Down
39 changes: 36 additions & 3 deletions js/packages/react/src/widget/IDKitSessionWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,46 @@ export function IDKitSessionWidget({
});
}, [effectiveErrorCode, onError]);

// Auto-close on success
// In World App context there's no UI to render HostAppVerificationState,
// so invoke handleVerify programmatically when the proof arrives.
useEffect(() => {
if (isSuccess && autoClose) {
if (
!flow.isInWorldApp ||
!isHostVerifying ||
!flow.result ||
!handleVerify
) {
return;
}

let cancelled = false;
void Promise.resolve(handleVerify(flow.result))
.then(() => {
if (!cancelled) setHostVerifyResult("passed");
})
.catch(() => {
if (!cancelled) setHostVerifyResult("failed");
});
return () => {
cancelled = true;
};
}, [flow.isInWorldApp, isHostVerifying, flow.result, handleVerify]);

// In World App there's no visible UI, so auto-close immediately on success or error.
// In bridge flow, only auto-close on success after the 2.5s delay (errors show retry UI).
useEffect(() => {
if (flow.isInWorldApp && (isSuccess || isError)) {
onOpenChange(false);
} else if (isSuccess && autoClose) {
const timer = setTimeout(() => onOpenChange(false), 2500);
return () => clearTimeout(timer);
}
}, [isSuccess, autoClose, onOpenChange]);
}, [isSuccess, isError, autoClose, onOpenChange, flow.isInWorldApp]);

// In World App context, the host app handles all UI — render nothing.
if (flow.isInWorldApp) {
return null;
}

const stage = getVisualStage(isSuccess, isError, isHostVerifying);
const showSimulatorCallout = config.environment === "staging";
Expand Down
Loading
Loading