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.jwt becomes undefined
getOrCreateWallet() stops making API calls
useWallet() 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
getOrCreateWallet never 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:
version does not bump
useMemo-wrapped context does not update
- Consumers never re-render
- Wallet initialization never occurs again
crossmint.jwt appears unchanged even after calling setJwt
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) // undefined
4. Try to rehydrate
setJwt(storedJwt)
await getOrCreateWallet(...)
❌ Expected: wallet restored
❌ Actual:
getOrCreateWallet does not send any API request
- No “authorization” header present
wallet stays undefined
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:
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
Summary
setJwt()inside@crossmint/client-sdk-react-uidoes not reliably update the Crossmint context after the initial login.As a result:
crossmint.jwtbecomesundefinedgetOrCreateWallet()stops making API callsuseWallet()returns{ wallet: undefined }This happens because
setJwt()mutates internal state without triggering the Proxy trap that updates the provider context.Environment
@crossmint/client-sdk-react-ui(latest as of Nov 2025)Detailed Description
1. Expected behavior
When the app provides a valid JWT on mount:
The SDK should:
crossmint.jwtuseWallet().wallet2. Actual behavior
After first login:
setJwt()works only the first timeundefinedsetJwt(storedJwt)does not update because the setter logic early-returnsgetOrCreateWalletnever triggers an API call (no authorized header)undefinedThis seems to be caused by the current implementation of
setJwt:3. Why this breaks
The provider wraps the Crossmint instance with a
Proxythat incrementsversionso React context re-renders:But mutating
experimental_customAuth.jwtdoes NOT trigger the Proxy trap, so:versiondoes not bumpuseMemo-wrapped context does not updatecrossmint.jwtappears unchanged even after callingsetJwtThis is why the SDK "loses" JWT on navigation.
Minimal Reproduction
1. Login user
2. Navigate to another page
3. On new page
4. Try to rehydrate
❌ Expected: wallet restored
❌ Actual:
getOrCreateWalletdoes not send any API requestwalletstaysundefinedRoot Cause Summary
setJwt()mutatesexperimental_customAuth.jwtin-place, which bypasses the Proxy trap and prevents React context updates.Thus:
useWallet, etc.) continue seeing stale JWT via old context snapshotjwtProposed Fix
Replace mutation with assignment of a NEW object:
OR manually bump
versionafter jwt updates:Either will restore correct reactivity.
Impact