Skip to content

setJwt() does not rehydrate JWT on navigation / reload (Crossmint React SDK loses token across page transitions) #1503

@FaisalAli19

Description

@FaisalAli19

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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions