@vvedantb/native-ui 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vvedantb/native-ui",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "license": "MIT",
5
5
  "description": "Shared React Native / Expo UI primitives. Floating surfaces, forms, overlays, toasts, settings chrome, tab bar, AI chat parts, the family app marks, a NativeWind preset, the Expo web root and Clerk + Convex auth screens.",
6
6
  "type": "module",
@@ -214,6 +214,7 @@
214
214
  }
215
215
  },
216
216
  "devDependencies": {
217
+ "@clerk/clerk-expo": "2.20.2",
217
218
  "@types/react": "^19.1.0",
218
219
  "react": "^19.1.0",
219
220
  "typescript": "^5.9.0",
@@ -222,6 +223,7 @@
222
223
  "scripts": {
223
224
  "build": "node -e \"require('node:fs').accessSync('src/index.ts')\"",
224
225
  "test": "vitest run",
225
- "typecheck": "tsc -p tsconfig.typecheck.json"
226
+ "typecheck": "tsc -p tsconfig.typecheck.json && pnpm run typecheck:consumer",
227
+ "typecheck:consumer": "tsc -p tsconfig.consumer.json"
226
228
  }
227
229
  }
@@ -9,7 +9,13 @@ import { PasswordInput } from "../PasswordInput";
9
9
  import { toast } from "../toastStore";
10
10
  import { useScheme } from "../useScheme";
11
11
  import { AuthScreen, FieldError, OrDivider } from "./AuthScreen";
12
- import { authErrorMessage, logAuthFailure } from "./authErrors";
12
+ import {
13
+ authErrorMessage,
14
+ DEFAULT_STAGE_TIMEOUT_MS,
15
+ isAuthTimeoutError,
16
+ logAuthFailure,
17
+ withAuthTimeout,
18
+ } from "./authErrors";
13
19
  import { SecondFactorScreen } from "./SecondFactorScreen";
14
20
  import {
15
21
  pickFirstFactorStrategy,
@@ -139,6 +145,12 @@ export type SignInScreenProps = AuthNavigationOptions & {
139
145
  * `console.warn`. Ignored in `sign-up`.
140
146
  */
141
147
  onIncomplete?: (signIn: IncompleteSignIn) => void;
148
+ /**
149
+ * Time for each Clerk call (create, prepare, attempt, reset password,
150
+ * set active), in milliseconds. A call that takes longer fails with code
151
+ * `auth_timeout`. Default 20000.
152
+ */
153
+ stageTimeoutMs?: number;
142
154
  };
143
155
 
144
156
  /**
@@ -167,6 +179,7 @@ export function SignInScreen({
167
179
  header,
168
180
  footer,
169
181
  onIncomplete,
182
+ stageTimeoutMs = DEFAULT_STAGE_TIMEOUT_MS,
170
183
  }: SignInScreenProps) {
171
184
  const signInState = useSignIn();
172
185
  const signUpState = useSignUp();
@@ -187,7 +200,7 @@ export function SignInScreen({
187
200
  const [pendingNewPassword, setPendingNewPassword] = useState(false);
188
201
  const [newPassword, setNewPassword] = useState("");
189
202
  const [confirmPassword, setConfirmPassword] = useState("");
190
- const secondFactor = useSecondFactor();
203
+ const secondFactor = useSecondFactor({ stageTimeoutMs });
191
204
  const [error, setError] = useState("");
192
205
  const [submitting, setSubmitting] = useState(false);
193
206
  const signUp = mode === "sign-up";
@@ -249,6 +262,7 @@ export function SignInScreen({
249
262
  signIn,
250
263
  strategy,
251
264
  factor: list.find((factor) => factor.strategy === strategy),
265
+ timeoutMs: stageTimeoutMs,
252
266
  });
253
267
  if (!sent.ok) setFirstError(sent.error);
254
268
  setFirstLoading(false);
@@ -268,7 +282,7 @@ export function SignInScreen({
268
282
  // Clerk's `SignInStatus` omits `needs_client_trust`: widen before comparing.
269
283
  const status: string | null = attempt.status;
270
284
  if (status === "complete" && attempt.createdSessionId && setActive) {
271
- await setActive({ session: attempt.createdSessionId });
285
+ await withAuthTimeout(setActive({ session: attempt.createdSessionId }), "set-active", stageTimeoutMs);
272
286
  done();
273
287
  } else if (status === "needs_new_password") {
274
288
  setPendingFirstFactor(false);
@@ -288,7 +302,12 @@ export function SignInScreen({
288
302
  if (!isLoaded || !signIn || firstLoading) return;
289
303
  setFirstLoading(true);
290
304
  setFirstError("");
291
- const result = await submitFirstFactor({ signIn, code: firstCode, strategy: firstStrategy });
305
+ const result = await submitFirstFactor({
306
+ signIn,
307
+ code: firstCode,
308
+ strategy: firstStrategy,
309
+ timeoutMs: stageTimeoutMs,
310
+ });
292
311
  if (!result.ok) {
293
312
  setFirstError(result.error);
294
313
  setFirstLoading(false);
@@ -297,6 +316,7 @@ export function SignInScreen({
297
316
  try {
298
317
  await routeAttempt(result.attempt, false);
299
318
  } catch (caught) {
319
+ if (isAuthTimeoutError(caught)) logAuthFailure("set-active", caught);
300
320
  setFirstError(authErrorMessage(caught, AUTH_MESSAGES.verifyFailed));
301
321
  } finally {
302
322
  setFirstLoading(false);
@@ -316,9 +336,13 @@ export function SignInScreen({
316
336
  setSubmitting(true);
317
337
  setError("");
318
338
  try {
319
- const result = await signIn.resetPassword({ password: newPassword });
339
+ const result = await withAuthTimeout(
340
+ signIn.resetPassword({ password: newPassword }),
341
+ "reset-password",
342
+ stageTimeoutMs,
343
+ );
320
344
  if (result.status === "complete" && result.createdSessionId) {
321
- await setActive({ session: result.createdSessionId });
345
+ await withAuthTimeout(setActive({ session: result.createdSessionId }), "set-active", stageTimeoutMs);
322
346
  done();
323
347
  } else if (onIncomplete) {
324
348
  onIncomplete(result);
@@ -327,6 +351,7 @@ export function SignInScreen({
327
351
  fail(AUTH_MESSAGES.resetPasswordFailed, AUTH_MESSAGES.needsAnotherStep);
328
352
  }
329
353
  } catch (caught) {
354
+ if (isAuthTimeoutError(caught)) logAuthFailure("reset-password", caught);
330
355
  fail(AUTH_MESSAGES.resetPasswordFailed, authErrorMessage(caught, AUTH_MESSAGES.resetPasswordFailed));
331
356
  } finally {
332
357
  setSubmitting(false);
@@ -340,7 +365,11 @@ export function SignInScreen({
340
365
  setSubmitting(true);
341
366
  setError("");
342
367
  try {
343
- const attempt = await signIn.create({ identifier: email, password });
368
+ const attempt = await withAuthTimeout(
369
+ signIn.create({ identifier: email, password }),
370
+ "create",
371
+ stageTimeoutMs,
372
+ );
344
373
  await routeAttempt(attempt, true);
345
374
  } catch (caught) {
346
375
  logAuthFailure("create", caught);
@@ -49,13 +49,61 @@ function firstError(parsed: ParsedAuthError | null) {
49
49
  };
50
50
  }
51
51
 
52
+ /** The code of a timed-out auth stage. */
53
+ export const AUTH_TIMEOUT_CODE = "auth_timeout";
54
+
55
+ /** Default time for one Clerk call, in milliseconds. */
56
+ export const DEFAULT_STAGE_TIMEOUT_MS = 20000;
57
+
58
+ /**
59
+ * The error `withAuthTimeout` rejects with. It is Clerk-shaped (`errors[0].code`
60
+ * is `auth_timeout`), so `authErrorMessage` and `logAuthFailure` read it.
61
+ */
62
+ export class AuthTimeoutError extends Error {
63
+ readonly code = AUTH_TIMEOUT_CODE;
64
+ readonly stage: string;
65
+ readonly errors: { code: string; message: string }[];
66
+ constructor(stage: string, ms: number) {
67
+ const message = `The ${stage} step timed out after ${Math.round(ms / 1000)} seconds. Try again.`;
68
+ super(message);
69
+ this.name = "AuthTimeoutError";
70
+ this.stage = stage;
71
+ this.errors = [{ code: AUTH_TIMEOUT_CODE, message }];
72
+ }
73
+ }
74
+
75
+ export function isAuthTimeoutError(error: unknown): error is AuthTimeoutError {
76
+ return error instanceof AuthTimeoutError;
77
+ }
78
+
79
+ /**
80
+ * Rejects with `AuthTimeoutError` (code `auth_timeout`, message names `stage`)
81
+ * when `promise` has not settled after `ms`. The timer is cleared on settle.
82
+ */
83
+ export function withAuthTimeout<T>(promise: Promise<T>, stage: string, ms: number = DEFAULT_STAGE_TIMEOUT_MS): Promise<T> {
84
+ return new Promise<T>((resolve, reject) => {
85
+ const timer = setTimeout(() => reject(new AuthTimeoutError(stage, ms)), ms);
86
+ promise.then(
87
+ (value) => {
88
+ clearTimeout(timer);
89
+ resolve(value);
90
+ },
91
+ (error) => {
92
+ clearTimeout(timer);
93
+ reject(error);
94
+ },
95
+ );
96
+ });
97
+ }
98
+
52
99
  /**
53
100
  * Logs a failed auth call as one structured `console.error`. It holds the
54
101
  * Clerk code, message, longMessage and meta.paramName, plus stage, errorName,
55
- * status and clerkTraceId when present. Only the error object is read, so a
102
+ * status and clerkTraceId when present. For `AuthTimeoutError` the logged stage is the error's own stage. Only the error object is read, so a
56
103
  * password or one-time code is never logged.
57
104
  */
58
105
  export function logAuthFailure(stage: string, error: unknown): void {
106
+ if (isAuthTimeoutError(error)) stage = error.stage;
59
107
  const parsed = parseAuthError(error);
60
108
  const { code, message, longMessage, paramName } = firstError(parsed);
61
109
  const errorName = parsed?.errorName ?? (error instanceof Error ? error.name : undefined);
package/src/auth/index.ts CHANGED
@@ -6,7 +6,16 @@ export { AuthProviders } from "./AuthProviders";
6
6
  export type { AuthProvidersProps } from "./AuthProviders";
7
7
  export { AuthScreen, OrDivider, FieldError } from "./AuthScreen";
8
8
  export type { AuthScreenProps } from "./AuthScreen";
9
- export { authErrorMessage, logAuthFailure, CREDENTIAL_ERROR_CODES } from "./authErrors";
9
+ export {
10
+ authErrorMessage,
11
+ logAuthFailure,
12
+ withAuthTimeout,
13
+ AuthTimeoutError,
14
+ isAuthTimeoutError,
15
+ AUTH_TIMEOUT_CODE,
16
+ DEFAULT_STAGE_TIMEOUT_MS,
17
+ CREDENTIAL_ERROR_CODES,
18
+ } from "./authErrors";
10
19
  export type { AuthErrorOptions } from "./authErrors";
11
20
  export {
12
21
  GoogleSignInButton,
@@ -43,6 +52,8 @@ export type {
43
52
  FirstFactorResult,
44
53
  FirstFactorSignIn,
45
54
  FirstFactorStrategy,
55
+ PrepareFirstFactorParams,
56
+ PrepareSecondFactorParams,
46
57
  SecondFactor,
47
58
  SecondFactorSignIn,
48
59
  SecondFactorStrategy,
package/src/auth/totp.ts CHANGED
@@ -1,6 +1,17 @@
1
1
  import { useState } from "react";
2
2
  import { useSignIn } from "@clerk/clerk-expo";
3
- import { authErrorMessage } from "./authErrors";
3
+ import {
4
+ authErrorMessage,
5
+ DEFAULT_STAGE_TIMEOUT_MS,
6
+ isAuthTimeoutError,
7
+ logAuthFailure,
8
+ withAuthTimeout,
9
+ } from "./authErrors";
10
+
11
+ /** Logs a timed-out stage. Other failures are not logged here. */
12
+ const logTimeout = (stage: string, error: unknown) => {
13
+ if (isAuthTimeoutError(error)) logAuthFailure(stage, error);
14
+ };
4
15
 
5
16
  /** The messages the second-factor step can show. Generic: none names a provider. */
6
17
  export const TOTP_MESSAGES = {
@@ -32,6 +43,24 @@ export type SecondFactor = {
32
43
 
33
44
  type AttemptResult = { status: string | null; createdSessionId: string | null };
34
45
 
46
+ /**
47
+ * The params of `prepareSecondFactor`. A union on `strategy`, as in Clerk's
48
+ * `PhoneCodeSecondFactorConfig | EmailCodeSecondFactorConfig`. One object with
49
+ * `strategy: "phone_code" | "email_code"` does not accept Clerk's
50
+ * `SignInResource` under `strictFunctionTypes`.
51
+ */
52
+ export type PrepareSecondFactorParams =
53
+ | { strategy: "phone_code"; phoneNumberId?: string }
54
+ | { strategy: "email_code"; emailAddressId?: string };
55
+
56
+ /**
57
+ * The params of `prepareFirstFactor`. A union on `strategy`, as in Clerk's
58
+ * `EmailCodeConfig | PhoneCodeConfig`, where the id is required.
59
+ */
60
+ export type PrepareFirstFactorParams =
61
+ | { strategy: "email_code"; emailAddressId: string }
62
+ | { strategy: "phone_code"; phoneNumberId: string };
63
+
35
64
  /** The part of the sign-in resource that TOTP needs. Tests pass a mock. */
36
65
  export type TotpSignIn = {
37
66
  attemptSecondFactor(params: { strategy: "totp"; code: string }): Promise<AttemptResult>;
@@ -40,11 +69,7 @@ export type TotpSignIn = {
40
69
  /** The part of the sign-in resource that any second factor needs. Tests pass a mock. */
41
70
  export type SecondFactorSignIn = {
42
71
  supportedSecondFactors?: SecondFactor[] | null;
43
- prepareSecondFactor?(params: {
44
- strategy: "phone_code" | "email_code";
45
- phoneNumberId?: string;
46
- emailAddressId?: string;
47
- }): Promise<unknown>;
72
+ prepareSecondFactor?(params: PrepareSecondFactorParams): Promise<unknown>;
48
73
  attemptSecondFactor(params: { strategy: SecondFactorStrategy; code: string }): Promise<AttemptResult>;
49
74
  };
50
75
 
@@ -76,25 +101,37 @@ export async function prepareSecondFactorIfNeeded(args: {
76
101
  signIn: Pick<SecondFactorSignIn, "supportedSecondFactors" | "prepareSecondFactor">;
77
102
  strategy: SecondFactorStrategy;
78
103
  factor?: SecondFactor;
104
+ /** Time for the Clerk call. Default 20000 ms. */
105
+ timeoutMs?: number;
79
106
  }): Promise<TotpResult> {
80
107
  const { signIn, strategy } = args;
108
+ const ms = args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
81
109
  if (strategy !== "phone_code" && strategy !== "email_code") return { ok: true };
82
110
  if (!signIn.prepareSecondFactor) return { ok: false, error: TOTP_MESSAGES.sendFailed };
83
111
  const factor = args.factor ?? signIn.supportedSecondFactors?.find((item) => item.strategy === strategy);
84
112
  try {
85
113
  if (strategy === "phone_code") {
86
- await signIn.prepareSecondFactor({
87
- strategy,
88
- ...(factor?.phoneNumberId ? { phoneNumberId: factor.phoneNumberId } : {}),
89
- });
114
+ await withAuthTimeout(
115
+ signIn.prepareSecondFactor({
116
+ strategy,
117
+ ...(factor?.phoneNumberId ? { phoneNumberId: factor.phoneNumberId } : {}),
118
+ }),
119
+ "prepare-second-factor",
120
+ ms,
121
+ );
90
122
  } else {
91
- await signIn.prepareSecondFactor({
92
- strategy,
93
- ...(factor?.emailAddressId ? { emailAddressId: factor.emailAddressId } : {}),
94
- });
123
+ await withAuthTimeout(
124
+ signIn.prepareSecondFactor({
125
+ strategy,
126
+ ...(factor?.emailAddressId ? { emailAddressId: factor.emailAddressId } : {}),
127
+ }),
128
+ "prepare-second-factor",
129
+ ms,
130
+ );
95
131
  }
96
132
  return { ok: true };
97
133
  } catch (caught) {
134
+ logTimeout("prepare-second-factor", caught);
98
135
  return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.sendFailed) };
99
136
  }
100
137
  }
@@ -111,15 +148,23 @@ export async function submitSecondFactor(args: {
111
148
  setActive: TotpSetActive;
112
149
  code: string;
113
150
  strategy: SecondFactorStrategy;
151
+ /** Time for each Clerk call. Default 20000 ms. */
152
+ timeoutMs?: number;
114
153
  }): Promise<TotpResult> {
154
+ const ms = args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
115
155
  try {
116
- const attempt = await args.signIn.attemptSecondFactor({ strategy: args.strategy, code: args.code });
156
+ const attempt = await withAuthTimeout(
157
+ args.signIn.attemptSecondFactor({ strategy: args.strategy, code: args.code }),
158
+ "attempt-second-factor",
159
+ ms,
160
+ );
117
161
  if (attempt.status === "complete" && attempt.createdSessionId) {
118
- await args.setActive({ session: attempt.createdSessionId });
162
+ await withAuthTimeout(args.setActive({ session: attempt.createdSessionId }), "set-active", ms);
119
163
  return { ok: true };
120
164
  }
121
165
  return { ok: false, error: TOTP_MESSAGES.needsAnotherStep };
122
166
  } catch (caught) {
167
+ logTimeout("attempt-second-factor", caught);
123
168
  const fallback = args.strategy === "totp" ? TOTP_MESSAGES.invalidCode : TOTP_MESSAGES.invalidCodeGeneric;
124
169
  return { ok: false, error: authErrorMessage(caught, fallback) };
125
170
  }
@@ -142,11 +187,7 @@ export type FirstFactor = {
142
187
  /** The part of the sign-in resource that a first-factor code needs. Tests pass a mock. */
143
188
  export type FirstFactorSignIn = {
144
189
  supportedFirstFactors?: FirstFactor[] | null;
145
- prepareFirstFactor?(params: {
146
- strategy: FirstFactorStrategy;
147
- phoneNumberId?: string;
148
- emailAddressId?: string;
149
- }): Promise<unknown>;
190
+ prepareFirstFactor?(params: PrepareFirstFactorParams): Promise<unknown>;
150
191
  attemptFirstFactor(params: { strategy: FirstFactorStrategy; code: string }): Promise<AttemptResult>;
151
192
  };
152
193
 
@@ -187,20 +228,32 @@ export async function prepareFirstFactorIfNeeded(args: {
187
228
  signIn: Pick<FirstFactorSignIn, "supportedFirstFactors" | "prepareFirstFactor">;
188
229
  strategy: FirstFactorStrategy;
189
230
  factor?: FirstFactor;
231
+ /** Time for the Clerk call. Default 20000 ms. */
232
+ timeoutMs?: number;
190
233
  }): Promise<TotpResult> {
191
234
  const { signIn, strategy } = args;
235
+ const ms = args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
192
236
  if (!signIn.prepareFirstFactor) return { ok: false, error: TOTP_MESSAGES.sendFailed };
193
237
  const factor = args.factor ?? signIn.supportedFirstFactors?.find((item) => item.strategy === strategy);
194
238
  try {
195
239
  if (strategy === "phone_code") {
196
240
  if (!factor?.phoneNumberId) return { ok: false, error: TOTP_MESSAGES.sendFailed };
197
- await signIn.prepareFirstFactor({ strategy, phoneNumberId: factor.phoneNumberId });
241
+ await withAuthTimeout(
242
+ signIn.prepareFirstFactor({ strategy, phoneNumberId: factor.phoneNumberId }),
243
+ "prepare-first-factor",
244
+ ms,
245
+ );
198
246
  } else {
199
247
  if (!factor?.emailAddressId) return { ok: false, error: TOTP_MESSAGES.sendFailed };
200
- await signIn.prepareFirstFactor({ strategy, emailAddressId: factor.emailAddressId });
248
+ await withAuthTimeout(
249
+ signIn.prepareFirstFactor({ strategy, emailAddressId: factor.emailAddressId }),
250
+ "prepare-first-factor",
251
+ ms,
252
+ );
201
253
  }
202
254
  return { ok: true };
203
255
  } catch (caught) {
256
+ logTimeout("prepare-first-factor", caught);
204
257
  return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.sendFailed) };
205
258
  }
206
259
  }
@@ -214,11 +267,18 @@ export async function submitFirstFactor(args: {
214
267
  signIn: Pick<FirstFactorSignIn, "attemptFirstFactor">;
215
268
  code: string;
216
269
  strategy: FirstFactorStrategy;
270
+ /** Time for the Clerk call. Default 20000 ms. */
271
+ timeoutMs?: number;
217
272
  }): Promise<FirstFactorResult> {
218
273
  try {
219
- const attempt = await args.signIn.attemptFirstFactor({ strategy: args.strategy, code: args.code });
274
+ const attempt = await withAuthTimeout(
275
+ args.signIn.attemptFirstFactor({ strategy: args.strategy, code: args.code }),
276
+ "attempt-first-factor",
277
+ args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS,
278
+ );
220
279
  return { ok: true, attempt };
221
280
  } catch (caught) {
281
+ logTimeout("attempt-first-factor", caught);
222
282
  return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.invalidCodeGeneric) };
223
283
  }
224
284
  }
@@ -228,13 +288,22 @@ export function submitTotpSecondFactor(args: {
228
288
  signIn: TotpSignIn;
229
289
  setActive: TotpSetActive;
230
290
  code: string;
291
+ timeoutMs?: number;
231
292
  }): Promise<TotpResult> {
232
- return submitSecondFactor({ signIn: args.signIn, setActive: args.setActive, code: args.code, strategy: "totp" });
293
+ return submitSecondFactor({
294
+ signIn: args.signIn,
295
+ setActive: args.setActive,
296
+ code: args.code,
297
+ strategy: "totp",
298
+ timeoutMs: args.timeoutMs,
299
+ });
233
300
  }
234
301
 
235
302
  export type UseSecondFactorOptions = {
236
303
  /** The strategy `submit` uses until `setStrategy` changes it. Default `"totp"`. */
237
304
  strategy?: SecondFactorStrategy;
305
+ /** Time for each Clerk call, in milliseconds. Default 20000. */
306
+ stageTimeoutMs?: number;
238
307
  };
239
308
 
240
309
  export type UseSecondFactorResult = {
@@ -265,6 +334,7 @@ export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecond
265
334
  const strategy = chosen ?? options.strategy ?? "totp";
266
335
  const [loading, setLoading] = useState(false);
267
336
  const [error, setError] = useState("");
337
+ const timeoutMs = options.stageTimeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
268
338
 
269
339
  const submit = async (code: string): Promise<TotpResult> => {
270
340
  if (loading) return { ok: false, error };
@@ -274,7 +344,7 @@ export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecond
274
344
  }
275
345
  setLoading(true);
276
346
  setError("");
277
- const result = await submitSecondFactor({ signIn, setActive, code, strategy });
347
+ const result = await submitSecondFactor({ signIn, setActive, code, strategy, timeoutMs });
278
348
  if (!result.ok) setError(result.error);
279
349
  setLoading(false);
280
350
  return result;
@@ -290,7 +360,7 @@ export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecond
290
360
  }
291
361
  setLoading(true);
292
362
  setError("");
293
- const result = await prepareSecondFactorIfNeeded({ signIn, strategy: prepareStrategy, factor });
363
+ const result = await prepareSecondFactorIfNeeded({ signIn, strategy: prepareStrategy, factor, timeoutMs });
294
364
  if (!result.ok) setError(result.error);
295
365
  setLoading(false);
296
366
  return result;