@vvedantb/native-ui 0.8.0 → 0.10.0

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/src/ai/trace.tsx CHANGED
@@ -7,7 +7,7 @@ import { palette } from "../palette";
7
7
  import { useLargerText } from "../theme";
8
8
  import { tokens } from "../tokens";
9
9
  import { useScheme } from "../useScheme";
10
- import { chatTextSize } from "./chat-text";
10
+ import { chatTextSize, formatElapsed } from "./chat-text";
11
11
  import {
12
12
  Collapsible,
13
13
  CollapsibleChevron,
@@ -232,3 +232,34 @@ export function AgentTraceCollapsibleContent({
232
232
  </CollapsibleContent>
233
233
  );
234
234
  }
235
+
236
+ export type AgentTraceFoldProps = Omit<CollapsibleProps, "children"> & {
237
+ /** How long the turn ran, in seconds. The title reads "Worked for 1m 5s". */
238
+ seconds: number;
239
+ /** The settled turn's steps, shown in an `AgentTrace` when open. */
240
+ children: ReactNode;
241
+ };
242
+
243
+ /**
244
+ * A settled turn folded behind one line, as the web `ActivityTasks` in its
245
+ * folded (Codex-style) mode: "Worked for …" over a hairline
246
+ * (`border-b border-border pb-1.5`), closed by default, and the steps in an
247
+ * `AgentTrace` 8 below when open.
248
+ */
249
+ export function AgentTraceFold({ seconds, children, defaultOpen = false, className, ...props }: AgentTraceFoldProps) {
250
+ const scheme = useScheme();
251
+ return (
252
+ <Collapsible className={cn("w-full min-w-0", className)} defaultOpen={defaultOpen} {...props}>
253
+ <View style={{ borderBottomWidth: 1, borderBottomColor: tokens.color.border[scheme], paddingBottom: 6 }}>
254
+ <AgentTraceCollapsibleTrigger>
255
+ <AgentTraceLine>
256
+ <AgentTraceTitle>{`Worked for ${formatElapsed(seconds)}`}</AgentTraceTitle>
257
+ </AgentTraceLine>
258
+ </AgentTraceCollapsibleTrigger>
259
+ </View>
260
+ <CollapsibleContent>
261
+ <AgentTrace className="mt-2">{children}</AgentTrace>
262
+ </CollapsibleContent>
263
+ </Collapsible>
264
+ );
265
+ }
@@ -1,5 +1,5 @@
1
- import type { ReactNode } from "react";
2
- import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, View } from "react-native";
1
+ import { useEffect, useState, type ReactNode } from "react";
2
+ import { Keyboard, KeyboardAvoidingView, ScrollView, StyleSheet, Text, View } from "react-native";
3
3
  import { cn } from "../cn";
4
4
  import { textStyle, typeStyle } from "../controlStyles";
5
5
  import { palette } from "../palette";
@@ -18,23 +18,51 @@ export type AuthScreenProps = {
18
18
  className?: string;
19
19
  };
20
20
 
21
+ /** Space under the form while the keyboard is open, on top of its height. */
22
+ const KEYBOARD_EXTRA = 24;
23
+
24
+ /** The height of the open soft keyboard, or 0. Edge-to-edge Android ignores `adjustResize`, so the screen pads itself. */
25
+ function useKeyboardHeight(): number {
26
+ const [height, setHeight] = useState(0);
27
+ useEffect(() => {
28
+ const show = (event: { endCoordinates: { height: number } }) => setHeight(event.endCoordinates.height);
29
+ const hide = () => setHeight(0);
30
+ const subs = [
31
+ Keyboard.addListener("keyboardWillShow", show),
32
+ Keyboard.addListener("keyboardDidShow", show),
33
+ Keyboard.addListener("keyboardWillHide", hide),
34
+ Keyboard.addListener("keyboardDidHide", hide),
35
+ ];
36
+ return () => subs.forEach((sub) => sub.remove());
37
+ }, []);
38
+ return height;
39
+ }
40
+
21
41
  /**
22
42
  * The sign-in and sign-up frame: a keyboard-avoiding scroll view on the
23
43
  * background colour, with a title, subtitle and the form in a column of 16.
44
+ * The form is centred until the keyboard opens. Then it aligns to the top and
45
+ * the scroll content gains the keyboard height as bottom space, so the focused
46
+ * field and the submit button scroll above the keyboard.
24
47
  */
25
48
  export function AuthScreen({ title, subtitle, children, header, footer, testID, className }: AuthScreenProps) {
26
49
  const scheme = useScheme();
50
+ const keyboardHeight = useKeyboardHeight();
51
+ const keyboardOpen = keyboardHeight > 0;
27
52
  return (
28
53
  <KeyboardAvoidingView
29
- behavior={Platform.OS === "ios" ? "padding" : "height"}
54
+ // Padding on both platforms, as OverlayModal and PromptInput: Android `height` does nothing edge-to-edge.
55
+ behavior="padding"
30
56
  className="flex-1"
31
57
  style={{ backgroundColor: tokens.color.background[scheme] }}
32
58
  >
33
59
  <ScrollView
34
60
  testID={testID}
35
61
  className="flex-1"
36
- contentContainerClassName={cn("grow justify-center px-6 py-12", className)}
62
+ contentContainerClassName={cn("grow px-6 py-12", keyboardOpen ? "justify-start" : "justify-center", className)}
63
+ contentContainerStyle={keyboardOpen ? { paddingBottom: keyboardHeight + KEYBOARD_EXTRA } : undefined}
37
64
  keyboardShouldPersistTaps="handled"
65
+ keyboardDismissMode="on-drag"
38
66
  >
39
67
  <View className="w-full max-w-sm self-center gap-6">
40
68
  {header}
@@ -0,0 +1,87 @@
1
+ // Clerk-free: a one-time code step. The caller owns the verification call and
2
+ // its errors (`useSecondFactor` does both for Clerk).
3
+ import { useState, type ReactNode } from "react";
4
+ import { Button } from "../Button";
5
+ import { Input } from "../Input";
6
+ import { AuthScreen, FieldError } from "./AuthScreen";
7
+
8
+ export type SecondFactorScreenProps = {
9
+ title: string;
10
+ description?: string;
11
+ /** Digits in the code. Default 6. */
12
+ codeLength?: number;
13
+ /** Called with the full code. A rejected promise is the caller's to handle. */
14
+ onSubmit: (code: string) => Promise<void> | void;
15
+ /** Inline error under the field, such as "Incorrect code". */
16
+ error?: string;
17
+ /** Shows the Verify spinner and blocks a second submit. */
18
+ loading?: boolean;
19
+ /** Shows a ghost back button when set. */
20
+ onBack?: () => void;
21
+ /** Default "Back". */
22
+ backLabel?: string;
23
+ /** Above the title. App brand chrome goes here. */
24
+ header?: ReactNode;
25
+ /** Prefix for every `testID`. Default "second-factor". */
26
+ testIDPrefix?: string;
27
+ /** Below the buttons, such as a "Use a backup code" switch. Passed to `AuthScreen`. */
28
+ footer?: ReactNode;
29
+ /** Default `true`: digits only, number pad. `false` takes any characters (backup codes). */
30
+ digitsOnly?: boolean;
31
+ /** Placeholder when `digitsOnly` is `false`. Default "Code". */
32
+ placeholder?: string;
33
+ };
34
+
35
+ /** A one-time code step: digits only, a Verify button, an inline error and a back link. */
36
+ export function SecondFactorScreen({
37
+ title,
38
+ description,
39
+ codeLength = 6,
40
+ onSubmit,
41
+ error,
42
+ loading = false,
43
+ onBack,
44
+ backLabel = "Back",
45
+ header,
46
+ testIDPrefix = "second-factor",
47
+ footer,
48
+ digitsOnly = true,
49
+ placeholder,
50
+ }: SecondFactorScreenProps) {
51
+ const [code, setCode] = useState("");
52
+ const ready = code.length === codeLength && !loading;
53
+
54
+ const submit = () => {
55
+ if (ready) void onSubmit(code);
56
+ };
57
+
58
+ return (
59
+ <AuthScreen testID={`${testIDPrefix}-screen`} title={title} subtitle={description} header={header} footer={footer}>
60
+ <Input
61
+ testID={`${testIDPrefix}-code`}
62
+ accessibilityLabel="Verification code"
63
+ value={code}
64
+ placeholder={digitsOnly ? "0".repeat(codeLength) : (placeholder ?? "Code")}
65
+ keyboardType={digitsOnly ? "number-pad" : "default"}
66
+ autoCapitalize="none"
67
+ autoCorrect={false}
68
+ autoComplete="one-time-code"
69
+ textContentType="oneTimeCode"
70
+ maxLength={codeLength}
71
+ autoFocus
72
+ returnKeyType="go"
73
+ onChangeText={(text) => setCode(digitsOnly ? text.replace(/\D/g, "") : text.trim())}
74
+ onSubmitEditing={submit}
75
+ />
76
+ <FieldError testID={`${testIDPrefix}-error`} message={error} />
77
+ <Button testID={`${testIDPrefix}-verify`} loading={loading} disabled={!ready} onPress={submit}>
78
+ Verify
79
+ </Button>
80
+ {onBack ? (
81
+ <Button testID={`${testIDPrefix}-back`} variant="ghost" onPress={onBack}>
82
+ {backLabel}
83
+ </Button>
84
+ ) : null}
85
+ </AuthScreen>
86
+ );
87
+ }
@@ -9,6 +9,14 @@ import { toast } from "../toastStore";
9
9
  import { useScheme } from "../useScheme";
10
10
  import { AuthScreen, FieldError, OrDivider } from "./AuthScreen";
11
11
  import { authErrorMessage } from "./authErrors";
12
+ import { SecondFactorScreen } from "./SecondFactorScreen";
13
+ import {
14
+ pickSecondFactorStrategy,
15
+ supportedSecondFactorStrategies,
16
+ useSecondFactor,
17
+ type SecondFactor,
18
+ type SecondFactorStrategy,
19
+ } from "./totp";
12
20
  import { GoogleSignInButton, type AuthNavigationOptions } from "./GoogleSignInButton";
13
21
 
14
22
  export type SignInMode = "sign-in" | "sign-up";
@@ -22,8 +30,66 @@ export const AUTH_MESSAGES = {
22
30
  verifyFailed: "Invalid code",
23
31
  missingCode: "Enter the code from the email",
24
32
  needsAnotherStep: "This account needs another step to sign in. Finish it on the web.",
33
+ secondFactorTitle: "Two-step verification",
34
+ secondFactorDescription: "Enter the code from your authenticator app.",
35
+ emailCodeTitle: "Check your email",
36
+ emailCodeDescription: "Enter the code we sent to your email.",
37
+ phoneCodeTitle: "Check your phone",
38
+ phoneCodeDescription: "Enter the code we sent by text message.",
39
+ backupCodeTitle: "Enter a backup code",
40
+ backupCodeDescription: "Use one of the backup codes you saved when you set up two-step verification.",
41
+ clientTrustTitle: "Verify it's you",
42
+ clientTrustEmailDescription: "This device is new. Enter the code we sent to your email.",
43
+ clientTrustPhoneDescription: "This device is new. Enter the code we sent by text message.",
44
+ useBackupCode: "Use a backup code",
45
+ useAuthenticator: "Use authenticator app",
46
+ useEmailCode: "Email me a code",
47
+ usePhoneCode: "Text me a code",
48
+ newPasswordTitle: "Set a new password",
49
+ newPasswordDescription: "Your password must change before you can sign in.",
50
+ newPasswordLabel: "New password",
51
+ confirmPasswordLabel: "Confirm new password",
52
+ passwordMismatch: "The passwords do not match",
53
+ passwordTooShort: "Use a longer password",
54
+ resetPasswordFailed: "Could not set the new password",
55
+ setPassword: "Set password",
25
56
  } as const;
26
57
 
58
+ /**
59
+ * A sign-in that is not complete, as `signIn.create` returns it. Passed to
60
+ * `onIncomplete` so the app can run the step itself.
61
+ */
62
+ export type IncompleteSignIn = {
63
+ status: string | null;
64
+ createdSessionId: string | null;
65
+ supportedFirstFactors?: Array<{ strategy: string; [key: string]: unknown }> | null;
66
+ supportedSecondFactors?: SecondFactor[] | null;
67
+ };
68
+
69
+ const METHOD_LABEL: Record<SecondFactorStrategy, string> = {
70
+ totp: AUTH_MESSAGES.useAuthenticator,
71
+ email_code: AUTH_MESSAGES.useEmailCode,
72
+ phone_code: AUTH_MESSAGES.usePhoneCode,
73
+ backup_code: AUTH_MESSAGES.useBackupCode,
74
+ };
75
+
76
+ function secondFactorCopy(strategy: SecondFactorStrategy, clientTrust: boolean) {
77
+ if (strategy === "email_code") {
78
+ return clientTrust
79
+ ? { title: AUTH_MESSAGES.clientTrustTitle, description: AUTH_MESSAGES.clientTrustEmailDescription }
80
+ : { title: AUTH_MESSAGES.emailCodeTitle, description: AUTH_MESSAGES.emailCodeDescription };
81
+ }
82
+ if (strategy === "phone_code") {
83
+ return clientTrust
84
+ ? { title: AUTH_MESSAGES.clientTrustTitle, description: AUTH_MESSAGES.clientTrustPhoneDescription }
85
+ : { title: AUTH_MESSAGES.phoneCodeTitle, description: AUTH_MESSAGES.phoneCodeDescription };
86
+ }
87
+ if (strategy === "backup_code") {
88
+ return { title: AUTH_MESSAGES.backupCodeTitle, description: AUTH_MESSAGES.backupCodeDescription };
89
+ }
90
+ return { title: AUTH_MESSAGES.secondFactorTitle, description: AUTH_MESSAGES.secondFactorDescription };
91
+ }
92
+
27
93
  export type SignInScreenProps = AuthNavigationOptions & {
28
94
  /** Default `"sign-in"`. `"sign-up"` creates the account and checks an emailed code. */
29
95
  mode?: SignInMode;
@@ -53,6 +119,17 @@ export type SignInScreenProps = AuthNavigationOptions & {
53
119
  header?: ReactNode;
54
120
  /** Below the footer link row, such as an app credit. */
55
121
  footer?: ReactNode;
122
+ /**
123
+ * Called with the sign-in when the built-in UI cannot finish it:
124
+ * `needs_identifier`, `needs_first_factor`, any unknown status, and
125
+ * `needs_second_factor` or `needs_client_trust` when no supported strategy
126
+ * (totp, backup code, phone code, email code) exists. The built-in screen
127
+ * always runs `needs_second_factor`, `needs_client_trust` and
128
+ * `needs_new_password` itself. When called, the web-fallback message is not
129
+ * shown. Without it, the screen shows the web fallback and logs
130
+ * `console.warn`. Ignored in `sign-up`.
131
+ */
132
+ onIncomplete?: (signIn: IncompleteSignIn) => void;
56
133
  };
57
134
 
58
135
  /**
@@ -80,6 +157,7 @@ export function SignInScreen({
80
157
  testIDPrefix,
81
158
  header,
82
159
  footer,
160
+ onIncomplete,
83
161
  }: SignInScreenProps) {
84
162
  const signInState = useSignIn();
85
163
  const signUpState = useSignUp();
@@ -89,6 +167,13 @@ export function SignInScreen({
89
167
  const [password, setPassword] = useState("");
90
168
  const [code, setCode] = useState("");
91
169
  const [pendingVerification, setPendingVerification] = useState(false);
170
+ const [pendingSecondFactor, setPendingSecondFactor] = useState(false);
171
+ const [factors, setFactors] = useState<SecondFactor[]>([]);
172
+ const [clientTrust, setClientTrust] = useState(false);
173
+ const [pendingNewPassword, setPendingNewPassword] = useState(false);
174
+ const [newPassword, setNewPassword] = useState("");
175
+ const [confirmPassword, setConfirmPassword] = useState("");
176
+ const secondFactor = useSecondFactor();
92
177
  const [error, setError] = useState("");
93
178
  const [submitting, setSubmitting] = useState(false);
94
179
  const signUp = mode === "sign-up";
@@ -104,6 +189,71 @@ export function SignInScreen({
104
189
  else router.replace(afterAuthHref, replaceOptions);
105
190
  };
106
191
 
192
+ const warnIncomplete = (attempt: IncompleteSignIn) => {
193
+ console.warn(
194
+ `[native-ui] Sign-in not complete: status=${attempt.status}, ` +
195
+ `secondFactors=${JSON.stringify((attempt.supportedSecondFactors ?? []).map((f) => f.strategy))}, ` +
196
+ `firstFactors=${JSON.stringify((attempt.supportedFirstFactors ?? []).map((f) => f.strategy))}. ` +
197
+ "Pass onIncomplete to handle it in the app.",
198
+ );
199
+ };
200
+
201
+ // A status the built-in UI cannot finish: the app's `onIncomplete`, or the web fallback.
202
+ const unhandled = (attempt: IncompleteSignIn) => {
203
+ if (onIncomplete) return onIncomplete(attempt);
204
+ warnIncomplete(attempt);
205
+ fail(AUTH_MESSAGES.signInFailed, AUTH_MESSAGES.needsAnotherStep);
206
+ };
207
+
208
+ const chooseStrategy = async (strategy: SecondFactorStrategy, list: SecondFactor[]) => {
209
+ secondFactor.clearError();
210
+ secondFactor.setStrategy(strategy);
211
+ setPendingSecondFactor(true);
212
+ await secondFactor.prepare(
213
+ strategy,
214
+ list.find((factor) => factor.strategy === strategy),
215
+ );
216
+ };
217
+
218
+ const startSecondFactor = async (attempt: IncompleteSignIn, trust: boolean) => {
219
+ const list = attempt.supportedSecondFactors ?? [];
220
+ const strategy = pickSecondFactorStrategy(list);
221
+ if (!strategy) return unhandled(attempt);
222
+ setFactors(list);
223
+ setClientTrust(trust);
224
+ await chooseStrategy(strategy, list);
225
+ };
226
+
227
+ const onNewPassword = async () => {
228
+ const { isLoaded, signIn, setActive } = signInState;
229
+ if (!isLoaded || !signIn || !setActive || submitting) return;
230
+ if (!newPassword) return fail(AUTH_MESSAGES.resetPasswordFailed, AUTH_MESSAGES.missingFields);
231
+ if (minPasswordLength && newPassword.length < minPasswordLength) {
232
+ return fail(AUTH_MESSAGES.resetPasswordFailed, `Use at least ${minPasswordLength} characters for the password`);
233
+ }
234
+ if (newPassword !== confirmPassword) {
235
+ return fail(AUTH_MESSAGES.resetPasswordFailed, AUTH_MESSAGES.passwordMismatch);
236
+ }
237
+ setSubmitting(true);
238
+ setError("");
239
+ try {
240
+ const result = await signIn.resetPassword({ password: newPassword });
241
+ if (result.status === "complete" && result.createdSessionId) {
242
+ await setActive({ session: result.createdSessionId });
243
+ done();
244
+ } else if (onIncomplete) {
245
+ onIncomplete(result);
246
+ } else {
247
+ warnIncomplete(result);
248
+ fail(AUTH_MESSAGES.resetPasswordFailed, AUTH_MESSAGES.needsAnotherStep);
249
+ }
250
+ } catch (caught) {
251
+ fail(AUTH_MESSAGES.resetPasswordFailed, authErrorMessage(caught, AUTH_MESSAGES.resetPasswordFailed));
252
+ } finally {
253
+ setSubmitting(false);
254
+ }
255
+ };
256
+
107
257
  const onSignIn = async () => {
108
258
  const { isLoaded, signIn, setActive } = signInState;
109
259
  if (!isLoaded || !signIn || !setActive || submitting) return;
@@ -115,8 +265,12 @@ export function SignInScreen({
115
265
  if (attempt.status === "complete" && attempt.createdSessionId) {
116
266
  await setActive({ session: attempt.createdSessionId });
117
267
  done();
268
+ } else if (attempt.status === "needs_new_password") {
269
+ setPendingNewPassword(true);
270
+ } else if (attempt.status === "needs_second_factor" || attempt.status === "needs_client_trust") {
271
+ await startSecondFactor(attempt, attempt.status === "needs_client_trust");
118
272
  } else {
119
- fail(AUTH_MESSAGES.signInFailed, AUTH_MESSAGES.needsAnotherStep);
273
+ unhandled(attempt);
120
274
  }
121
275
  } catch (caught) {
122
276
  fail(AUTH_MESSAGES.signInFailed, authErrorMessage(caught, AUTH_MESSAGES.invalidCredentials));
@@ -125,6 +279,22 @@ export function SignInScreen({
125
279
  }
126
280
  };
127
281
 
282
+ const onSecondFactor = async (secondFactorCode: string) => {
283
+ const result = await secondFactor.submit(secondFactorCode);
284
+ if (result.ok) done();
285
+ };
286
+
287
+ const onSecondFactorBack = () => {
288
+ // The pending attempt stays; the next `signIn.create` replaces it.
289
+ setPendingSecondFactor(false);
290
+ setPendingNewPassword(false);
291
+ setNewPassword("");
292
+ setConfirmPassword("");
293
+ setPassword("");
294
+ setError("");
295
+ secondFactor.clearError();
296
+ };
297
+
128
298
  const onSignUp = async () => {
129
299
  const { isLoaded, signUp: attempt } = signUpState;
130
300
  if (!isLoaded || !attempt || submitting) return;
@@ -190,6 +360,97 @@ export function SignInScreen({
190
360
  );
191
361
  const hasFooter = Boolean((linkHref && !pendingVerification) || footer);
192
362
 
363
+ if (pendingNewPassword) {
364
+ return (
365
+ <AuthScreen
366
+ testID={id("new-password-screen")}
367
+ title={AUTH_MESSAGES.newPasswordTitle}
368
+ subtitle={AUTH_MESSAGES.newPasswordDescription}
369
+ header={header}
370
+ >
371
+ <Input
372
+ testID={id("new-password")}
373
+ accessibilityLabel={AUTH_MESSAGES.newPasswordLabel}
374
+ autoCapitalize="none"
375
+ autoComplete="new-password"
376
+ textContentType="newPassword"
377
+ secureTextEntry
378
+ autoFocus
379
+ returnKeyType="next"
380
+ value={newPassword}
381
+ placeholder={AUTH_MESSAGES.newPasswordLabel}
382
+ onChangeText={setNewPassword}
383
+ />
384
+ <Input
385
+ testID={id("confirm-password")}
386
+ accessibilityLabel={AUTH_MESSAGES.confirmPasswordLabel}
387
+ autoCapitalize="none"
388
+ autoComplete="new-password"
389
+ textContentType="newPassword"
390
+ secureTextEntry
391
+ returnKeyType="go"
392
+ value={confirmPassword}
393
+ placeholder={AUTH_MESSAGES.confirmPasswordLabel}
394
+ onChangeText={setConfirmPassword}
395
+ onSubmitEditing={() => void onNewPassword()}
396
+ />
397
+ <FieldError testID={id("error")} message={error} />
398
+ <Button testID={id("new-password-submit")} loading={submitting} onPress={() => void onNewPassword()}>
399
+ {AUTH_MESSAGES.setPassword}
400
+ </Button>
401
+ <Button testID={id("new-password-back")} variant="ghost" onPress={onSecondFactorBack}>
402
+ Use a different account
403
+ </Button>
404
+ </AuthScreen>
405
+ );
406
+ }
407
+
408
+ if (pendingSecondFactor) {
409
+ const active = secondFactor.strategy;
410
+ const available = supportedSecondFactorStrategies(factors);
411
+ const others = available.filter((strategy) => strategy !== active);
412
+ // From a backup code, go back to the default non-backup method. Otherwise offer a backup code.
413
+ const switchTo =
414
+ active === "backup_code"
415
+ ? pickSecondFactorStrategy(factors.filter((factor) => factor.strategy !== "backup_code"))
416
+ : others.includes("backup_code")
417
+ ? "backup_code"
418
+ : null;
419
+ const copy = secondFactorCopy(active, clientTrust);
420
+ const backup = active === "backup_code";
421
+ return (
422
+ <SecondFactorScreen
423
+ key={active}
424
+ title={copy.title}
425
+ description={copy.description}
426
+ codeLength={backup ? 8 : 6}
427
+ digitsOnly={!backup}
428
+ placeholder="Backup code"
429
+ footer={
430
+ switchTo ? (
431
+ <Button
432
+ testID={id("second-factor-switch")}
433
+ variant="link"
434
+ size="sm"
435
+ fullWidth={false}
436
+ disabled={secondFactor.loading}
437
+ onPress={() => void chooseStrategy(switchTo, factors)}
438
+ >
439
+ {METHOD_LABEL[switchTo]}
440
+ </Button>
441
+ ) : undefined
442
+ }
443
+ onSubmit={onSecondFactor}
444
+ error={secondFactor.error}
445
+ loading={secondFactor.loading}
446
+ onBack={onSecondFactorBack}
447
+ backLabel="Use a different account"
448
+ header={header}
449
+ testIDPrefix={id("second-factor")}
450
+ />
451
+ );
452
+ }
453
+
193
454
  if (pendingVerification) {
194
455
  return (
195
456
  <AuthScreen
package/src/auth/index.ts CHANGED
@@ -19,4 +19,26 @@ export type {
19
19
  GoogleSignInButtonProps,
20
20
  } from "./GoogleSignInButton";
21
21
  export { SignInScreen, SignUpScreen, AUTH_MESSAGES } from "./SignInScreen";
22
- export type { SignInMode, SignInScreenProps, SignUpScreenProps } from "./SignInScreen";
22
+ export type { IncompleteSignIn, SignInMode, SignInScreenProps, SignUpScreenProps } from "./SignInScreen";
23
+ export { SecondFactorScreen } from "./SecondFactorScreen";
24
+ export type { SecondFactorScreenProps } from "./SecondFactorScreen";
25
+ export {
26
+ submitTotpSecondFactor,
27
+ submitSecondFactor,
28
+ prepareSecondFactorIfNeeded,
29
+ pickSecondFactorStrategy,
30
+ supportedSecondFactorStrategies,
31
+ SECOND_FACTOR_PREFERENCE,
32
+ useSecondFactor,
33
+ TOTP_MESSAGES,
34
+ } from "./totp";
35
+ export type {
36
+ SecondFactor,
37
+ SecondFactorSignIn,
38
+ SecondFactorStrategy,
39
+ TotpResult,
40
+ TotpSignIn,
41
+ TotpSetActive,
42
+ UseSecondFactorOptions,
43
+ UseSecondFactorResult,
44
+ } from "./totp";