-
Notifications
You must be signed in to change notification settings - Fork 31
Description
Summary
setJwt() inside @crossmint/client-sdk-react-ui does not reliably update the Crossmint context after the initial login.
As a result:
- After a navigation or page reload
crossmint.jwtbecomesundefinedgetOrCreateWallet()stops making API callsuseWallet()returns{ wallet: undefined }- And the user appears logged out even though JWT exists in my app store/localStorage/cookie
This happens because setJwt() mutates internal state without triggering the Proxy trap that updates the provider context.
Environment
- React: 19
- Next.js: 15.3.1
- Package:
@crossmint/client-sdk-react-ui(latest as of Nov 2025) - Using custom auth (BYO JWT flow)
- JWT is persisted in my app's store and/or cookie, but SDK does not rehydrate it properly.
Detailed Description
1. Expected behavior
When the app provides a valid JWT on mount:
setJwt(storedJwt)
getOrCreateWallet({ chain, signer })The SDK should:
- Update
crossmint.jwt - Re-render React context consumers
- Trigger wallet restoration API call
- Populate
useWallet().wallet
2. Actual behavior
After first login:
setJwt()works only the first time- After route changes, the JWT inside the SDK resets to
undefined - Re-running
setJwt(storedJwt)does not update because the setter logic early-returns getOrCreateWalletnever triggers an API call (no authorized header)- Wallet remains
undefined
This seems to be caused by the current implementation of setJwt:
const setJwt = useCallback((jwt: string | undefined) => {
if (crossmintRef.current == null) throw new Error("CrossmintProvider is not initialized");
if (jwt !== crossmintRef.current.jwt) {
crossmintRef.current.jwt = jwt;
if (crossmintRef.current.experimental_customAuth == null) {
crossmintRef.current.experimental_customAuth = { jwt };
} else {
// MUTATES in-place → Proxy "set" trap does NOT fire
crossmintRef.current.experimental_customAuth.jwt = jwt;
}
}
}, []);3. Why this breaks
The provider wraps the Crossmint instance with a Proxy that increments version so React context re-renders:
if (prop === "jwt") setVersion(v => v + 1);
if (prop === "experimental_customAuth") setVersion(v => v + 1);But mutating experimental_customAuth.jwt does NOT trigger the Proxy trap, so:
versiondoes not bumpuseMemo-wrapped context does not update- Consumers never re-render
- Wallet initialization never occurs again
crossmint.jwtappears unchanged even after callingsetJwt
This is why the SDK "loses" JWT on navigation.
Minimal Reproduction
1. Login user
setJwt(jwtFromServer)
await getOrCreateWallet({ signer: { type: "email", email } })2. Navigate to another page
router.push("/dashboard")3. On new page
console.log(crossmint.jwt) // undefined4. Try to rehydrate
setJwt(storedJwt)
await getOrCreateWallet(...)❌ Expected: wallet restored
❌ Actual:
getOrCreateWalletdoes not send any API request- No “authorization” header present
walletstaysundefined
Root Cause Summary
setJwt() mutates experimental_customAuth.jwt in-place, which bypasses the Proxy trap and prevents React context updates.
Thus:
- JWT is set inside the SDK object
- But all consuming hooks (
useWallet, etc.) continue seeing stale JWT via old context snapshot - Wallet API calls fail because they read old
jwt
Proposed Fix
Replace mutation with assignment of a NEW object:
crossmintRef.current.experimental_customAuth = {
...(crossmintRef.current.experimental_customAuth || {}),
jwt,
};OR manually bump version after jwt updates:
setVersion(v => v + 1)Either will restore correct reactivity.
Impact
- Users appear logged out after navigating
- Wallet creation / restoration API never runs
- Embedded wallets become unusable until a hard refresh
- Production apps cannot rely on navigation flows
- We cannot ship this experience to end-users without a fix