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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"i18n-iso-countries": "^7.14.0",
"i18next": "^25.7.4",
"moti": "^0.30.0",
"persona": "^5.0.0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Persona Web SDK npm v5 Expo React Native compatibility environment vs environmentId init options

💡 Result:

  • persona (Web/Embedded Flow) v5 (npm i persona): use environmentId (an env_... token) when constructing new Persona.Client(...). This is the current, non-deprecated way to choose the Persona API environment. Example shown in the Embedded Flow quickstart. [1]
  • environment option: this is the deprecated legacy field; it used string values like "sandbox" / "production" and is documented as “Deprecated. Use environmentId instead.” [2]

Expo / React Native compatibility

  • The Web SDK is meant for web (DOM) environments, not a native React Native runtime. For a mobile app, Persona recommends using their native mobile SDKs / React Native SDK instead. [3][4]
  • If you must run Persona in an in-app browser, use a WebView/hosted flow and include is-webview=true in the URL (required for WebView flow). [4]

References
[1] Persona Embedded Flow Quickstart (NPM) – environmentId in Persona.Client init: (docs.withpersona.com)
[2] Persona “Parameters” docs – environment deprecated, use environmentId: (docs.withpersona.com)
[3] Persona React Native Integration Guide (native SDK usage): (docs.withpersona.com)
[4] Persona WebView integration – native SDKs recommended; is-webview=true required: (docs.withpersona.com)

Citations:


🏁 Script executed:

# Search for Persona SDK usage in the codebase
rg -A 5 -B 5 "persona" package.json | head -50

Repository: exactly/exa

Length of output: 768


🏁 Script executed:

# Look for Persona initialization code
rg -A 10 "new Persona|Persona\(" --type ts --type tsx --type js --type jsx

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

# Check for any persona API version constants or configuration
rg "PERSONA_API_VERSION|Persona.*init|persona.*init" -A 5 --type ts --type tsx

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

# Search for Persona initialization/client instantiation
rg -i "new Persona|Persona\.Client|import.*persona" -A 3 | head -100

Repository: exactly/exa

Length of output: 4274


🏁 Script executed:

# Look for any persona config or environment setup
rg -i "environmentId|environment.*persona|PERSONA" -B 2 -A 2 | head -100

Repository: exactly/exa

Length of output: 6081


🏁 Script executed:

# Check what files import persona package (non react-native)
rg "from ['\"]persona['\"]|import.*['\"]persona['\"]" -l

Repository: exactly/exa

Length of output: 75


Update Persona v5 initialization: use environmentId instead of deprecated environment field.

The codebase imports Persona v5 but initializes it using the deprecated environment field ("sandbox"/"production"). Persona v5 requires environmentId (an env_... token). Update src/utils/persona.ts to pass environmentId when constructing new Persona.Client(...) to avoid compatibility failures with v5.

🤖 Prompt for AI Agents
In `@package.json` at line 95, The Persona v5 client is being initialized with the
deprecated environment field; update the Persona client construction in
src/utils/persona.ts so that new Persona.Client(...) is passed environmentId
instead of environment. Locate the function or constant that calls new
Persona.Client (look for symbols like PersonaClient, createPersonaClient, or the
file-exported client instance), replace the environment:
process.env.PERSONA_ENVIRONMENT (or similar) with environmentId:
process.env.PERSONA_ENVIRONMENT_ID (or the appropriate env var containing the
env_... token), and remove the deprecated environment property so the client
uses the required environmentId parameter.

"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "^16.5.3",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 63 additions & 32 deletions src/utils/persona.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,72 @@ import queryClient, { type EmbeddingContext } from "./queryClient";
import reportError from "./reportError";
import { getKYCTokens } from "./server";

import type * as PersonaWeb from "persona";

export const environment = (__DEV__ || process.env.EXPO_PUBLIC_ENV === "e2e" ? "sandbox" : "production") as Environment;

export async function startKYC() {
const { otl: oneTimeLink, inquiryId, sessionToken } = await getKYCTokens("basic", await getRedirectURI());

if (Platform.OS === "web") {
if (await sdk.isInMiniApp()) {
await sdk.actions.openUrl(oneTimeLink);
return;
}
const embeddingContext = queryClient.getQueryData<EmbeddingContext>(["embedding-context"]);
if (embeddingContext && !embeddingContext.endsWith("-web")) {
window.location.replace(oneTimeLink);
return;
}
window.open(oneTimeLink, "_blank", "noopener,noreferrer"); // cspell:ignore noopener noreferrer
return;
}
export const startKYC = (
Platform.OS === "web"
? () => {
let activeClient: InstanceType<typeof PersonaWeb.Client> | undefined;

const { Inquiry } = await import("react-native-persona");
Inquiry.fromInquiry(inquiryId)
.sessionToken(sessionToken)
.onCanceled(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
router.replace("/(main)/(home)");
})
.onComplete(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
queryClient.setQueryData(["card-upgrade"], 1);
router.replace("/(main)/(home)");
})
.onError((error) => reportError(error))
.build()
.start();
}
return async () => {
const [{ Client }, { inquiryId, sessionToken }] = await Promise.all([
import("persona"),
getKYCTokens("basic", await getRedirectURI()),
]);

activeClient?.destroy();

activeClient = new Client({
inquiryId,
sessionToken,
environment: environment as "production" | "sandbox", // TODO deprecated - use environmentId instead
// environmentId: "",
onReady: () => activeClient?.open(),
onComplete: () => {
activeClient?.destroy();
activeClient = undefined;
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
queryClient.setQueryData(["card-upgrade"], 1); // TODO probably not needed unless the user is upgrading their card
router.replace("/(main)/(home)");
},
onCancel: () => {
activeClient?.destroy();
activeClient = undefined;
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
router.replace("/(main)/(home)");
},
onError: (error) => {
activeClient?.destroy();
activeClient = undefined;
reportError(error);
},
});
};
Comment on lines +18 to +56

Choose a reason for hiding this comment

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

🔴 KYC flow broken for Farcaster mini apps and embedded web contexts

The refactored startKYC function removes special handling for mini apps and embedded contexts that was present in the old implementation.

Click to expand

Previous Behavior

The old implementation at src/utils/persona.ts had explicit handling for different web contexts:

if (Platform.OS === "web") {
  if (await sdk.isInMiniApp()) {
    await sdk.actions.openUrl(oneTimeLink);  // Mini app specific handling
    return;
  }
  const embeddingContext = queryClient.getQueryData<EmbeddingContext>(["embedding-context"]);
  if (embeddingContext && !embeddingContext.endsWith("-web")) {
    window.location.replace(oneTimeLink);  // Embedded context handling
    return;
  }
  window.open(oneTimeLink, "_blank", "noopener,noreferrer");
}

New Behavior

The new implementation uses the Persona Web SDK's Client directly for all web contexts without checking if the user is in a mini app or embedded context.

Impact

  1. Farcaster mini apps: The Persona Web SDK likely cannot open its modal/iframe properly in mini app environments where popup/iframe capabilities are restricted. The old code used sdk.actions.openUrl() to properly navigate within the mini app context.

  2. Non-web embedded contexts (e.g., farcaster, base, metamask, phantom): These contexts previously used window.location.replace() to navigate to the one-time link, which is now not handled at all.

Users in these contexts will likely experience a broken KYC flow where the Persona modal fails to open or displays incorrectly.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
: () => {
// eslint-disable-next-line unicorn/consistent-function-scoping
return async () => {
const { inquiryId, sessionToken } = await getKYCTokens("basic", await getRedirectURI());
const { Inquiry } = await import("react-native-persona");
Inquiry.fromInquiry(inquiryId)
.sessionToken(sessionToken)
.onCanceled(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
router.replace("/(main)/(home)");
})
.onComplete(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
queryClient.setQueryData(["card-upgrade"], 1);
router.replace("/(main)/(home)");
Comment on lines +37 to +72
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n --type=ts --type=tsx 'startKYC\s*\(' -A 3 -B 3

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

rg -n 'startKYC\s*\(' -A 3 -B 3

Repository: exactly/exa

Length of output: 1494


🏁 Script executed:

cat src/utils/persona.ts

Repository: exactly/exa

Length of output: 3642


🏁 Script executed:

cat src/utils/useBeginKYC.ts

Repository: exactly/exa

Length of output: 1258


🏁 Script executed:

cat src/components/card/Card.tsx | head -210 | tail -30

Repository: exactly/exa

Length of output: 848


🏁 Script executed:

head -50 src/components/card/Card.tsx

Repository: exactly/exa

Length of output: 2010


🏁 Script executed:

rg -n "card|Card" src/components/card/Card.tsx | head -5

Repository: exactly/exa

Length of output: 351


🏁 Script executed:

rg -n "^export\s+(default\s+)?.*Card|^function Card|^const Card" src/components/card/Card.tsx

Repository: exactly/exa

Length of output: 90


🏁 Script executed:

rg -n "from.*card/Card|import.*Card.*from.*card/Card" --type js --type ts

Repository: exactly/exa

Length of output: 144


🏁 Script executed:

rg -n "GettingStarted|getting-started" --max-count 20

Repository: exactly/exa

Length of output: 973


🏁 Script executed:

cat src/components/home/card-upgrade/VerifyIdentity.tsx

Repository: exactly/exa

Length of output: 3084


Move setQueryData(["card-upgrade"], 1) calls out of the shared startKYC utility.

The shared utility unconditionally sets ["card-upgrade"] on completion in both web (line 41) and native (line 71) branches, affecting all callers: useBeginKYC (onboarding), Card (general card KYC), and VerifyIdentity (card-upgrade). This mutation should only apply to card-upgrade flows. The existing TODO comment ("probably not needed unless the user is upgrading their card") reflects this concern. Move these setQueryData calls into the card-upgrade-specific caller (VerifyIdentity.tsx) or accept the mutation through a parameter/context to explicitly gate it to relevant flows.

🤖 Prompt for AI Agents
In `@src/utils/persona.ts` around lines 37 - 72, The shared startKYC utility
currently mutates queryClient.setQueryData(["card-upgrade"], 1) in both web and
native branches (see the onComplete handlers in persona.ts), which wrongly
affects all callers; remove those setQueryData calls from persona.ts and instead
apply the mutation only in the card-upgrade flow by adding the call inside the
card-upgrade-specific caller (VerifyIdentity component) after the startKYC
promise resolves (or alternatively add an explicit boolean parameter to startKYC
like applyCardUpgrade that defaults to false and only setQueryData when true),
ensuring other callers (useBeginKYC, Card) are unaffected.

})
.onError((error) => reportError(error))
.build()
.start();
};
}
)();

async function getRedirectURI() {
const miniappContext = (await sdk.context) as unknown as undefined | { client: { appUrl?: string } };
Expand Down