diff --git a/package-lock.json b/package-lock.json index a0bbaa5..629007e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "arctic": "^1.2.0", "cookie": "^1.0.1", "cspell": "^8.17.2", + "is-network-error": "^1.1.0", "jose": "^5.2.2", "jwt-decode": "^4.0.0", "lucia": "^3.2.0", @@ -5523,6 +5524,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", diff --git a/package.json b/package.json index 819087e..ec1582a 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "arctic": "^1.2.0", "cookie": "^1.0.1", "cspell": "^8.17.2", + "is-network-error": "^1.1.0", "jose": "^5.2.2", "jwt-decode": "^4.0.0", "lucia": "^3.2.0", diff --git a/src/react/client.tsx b/src/react/client.tsx index 1ee4512..3535366 100644 --- a/src/react/client.tsx +++ b/src/react/client.tsx @@ -18,6 +18,11 @@ import type { ConvexAuthActionsContext as ConvexAuthActionsContextType, TokenStorage, } from "./index.js"; +import isNetworkError from "is-network-error"; + +// Retry after this much time, based on the retry number. +const RETRY_BACKOFF = [500, 2000]; // In ms +const RETRY_JITTER = 100; // In ms export const ConvexAuthActionsContext = createContext(undefined as any); @@ -72,6 +77,7 @@ export function AuthProvider({ (message: string) => { if (verbose) { console.debug(`${new Date().toISOString()} ${message}`); + client.logger?.logVerbose(message); } }, [verbose], @@ -164,16 +170,47 @@ export function AuthProvider({ return () => browserRemoveEventListener("storage", listener); }, [setToken]); + const verifyCode = useCallback( + async ( + args: { code: string; verifier?: string } | { refreshToken: string }, + ) => { + let lastError; + // Retry the call if it fails due to a network error. + // This is especially common in mobile apps where an app is backgrounded + // while making a call and hits a network error, but will succeed with a + // retry once the app is brought to the foreground. + let retry = 0; + while (retry < RETRY_BACKOFF.length) { + try { + return await client.unauthenticatedCall( + "auth:signIn" as unknown as SignInAction, + "code" in args + ? { params: { code: args.code }, verifier: args.verifier } + : args, + ); + } catch (e) { + lastError = e; + if (!isNetworkError(e)) { + break; + } + const wait = RETRY_BACKOFF[retry] + RETRY_JITTER * Math.random(); + retry++; + logVerbose( + `verifyCode failed with network error, retry ${retry} of ${RETRY_BACKOFF.length} in ${wait}ms`, + ); + await new Promise((resolve) => setTimeout(resolve, wait)); + } + } + throw lastError; + }, + [client], + ); + const verifyCodeAndSetToken = useCallback( async ( args: { code: string; verifier?: string } | { refreshToken: string }, ) => { - const { tokens } = await client.unauthenticatedCall( - "auth:signIn" as unknown as SignInAction, - "code" in args - ? { params: { code: args.code }, verifier: args.verifier } - : args, - ); + const { tokens } = await verifyCode(args); logVerbose(`retrieved tokens, is null: ${tokens === null}`); await setToken({ shouldStore: true, tokens: tokens ?? null }); return tokens !== null; diff --git a/src/react/clientType.ts b/src/react/clientType.ts index a46c8cc..abd5de1 100644 --- a/src/react/clientType.ts +++ b/src/react/clientType.ts @@ -1,3 +1,4 @@ +import { ConvexReactClient } from "convex/react"; import { FunctionReference, OptionalRestArgs } from "convex/server"; export type AuthClient = { @@ -12,4 +13,5 @@ export type AuthClient = { ...args: OptionalRestArgs ): Promise; verbose: boolean | undefined; + logger?: ConvexReactClient["logger"]; }; diff --git a/src/react/index.tsx b/src/react/index.tsx index 3063d0a..6ebdd3f 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -100,12 +100,12 @@ export function ConvexAuthProvider(props: { return client.action(action, args); }, unauthenticatedCall(action, args) { - return new ConvexHttpClient((client as any).address).action( - action, - args, - ); + return new ConvexHttpClient((client as any).address, { + logger: client.logger, + }).action(action, args); }, verbose: (client as any).options?.verbose, + logger: client.logger, }) satisfies AuthClient, [client], );