Any API call can meet a 401 from an expired access token. BookNest wraps fetch(): it sends the saved access token, and on a 401 trades the refresh token for a new pair with refreshAsync() and retries once. The server rotates refresh tokens (each works once), so requests that fail together must share one refresh: refreshOnce() keeps the pending promise in a module variable and hands it to every caller until it settles; on failure it forgets both tokens and resolves null:
export async function authFetch(path: string, init: RequestInit = {}) {
const send = (t: Tokens | null) => fetch(`${issuer}${path}`, {
...init, headers: { ...(init.headers as object), authorization: `Bearer ${t?.accessToken}` },
});
let res = await send(await readTokens());
if (res.status === 401) {
const fresh = await refreshOnce();
if (!fresh) {
signedOut(); // back to the sign-in screen
return res;
}
res = await send(fresh);
}
return res;
}To test it, the practice API has a hook, POST /expire-tokens, that ages every access token. After calling it, Place order on the checkout logged a refresh on the phone and three requests at the server:
I ReactNativeJS: /orders: 401, refreshing 18:57:22 POST /orders 401 18:57:23 POST /token 200 18:57:24 POST /orders 201
The user saw only "Order 1001 placed". After restarting the API, which forgets every token, the refresh got POST /token 400 (refresh failed: The provided authorization grant ... is invalid) and the app returned to /cart.
signedOut() is a listener the session registers to set the user to null. The Stack.Protected guard={user !== null} from Auth Flows then removes checkout and its history, whether the user signed out or the refresh token died. The real protection is the server, which now rejects POST /orders without a valid token.