@entropy-softworks/ui 2026.9.9 → 2026.9.11

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.
@@ -0,0 +1,128 @@
1
+ import React from "react";
2
+ import { ScrollView, View } from "react-native";
3
+ import { SafeAreaView } from "react-native-safe-area-context";
4
+ import { StatusBar } from "expo-status-bar";
5
+ import { useColorScheme } from "nativewind";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardFooter,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "../../components/card";
14
+ import { Heading, MutedText } from "../../components/text";
15
+
16
+ /**
17
+ * Which shape an auth screen takes.
18
+ *
19
+ * `card` is a dialog floated in the middle of the viewport: a title, a
20
+ * description, the content and the actions, all inside a bordered panel. It is
21
+ * right on a desktop portal and it is what every consumer of these screens has
22
+ * always had.
23
+ *
24
+ * `screen` is the phone shape — heading at the top, content filling the space
25
+ * below it, actions at the bottom, no panel. A card on a phone is a panel
26
+ * floated over nothing, with its own padding inside the screen's padding, and
27
+ * next to a flow built the other way it reads as something from another app.
28
+ *
29
+ * The default stays `card` so nothing changes for anyone who does not ask.
30
+ */
31
+ export type AuthPageLayout = "card" | "screen";
32
+
33
+ export interface AuthPageProps {
34
+ layout?: AuthPageLayout;
35
+ title: string;
36
+ subtitle?: string | null;
37
+ /** Drawn above the title in the card layout, and nowhere in the screen one. */
38
+ icon?: React.ReactNode;
39
+ /** Top-left of the page, outside the content — a sign-out control, usually. */
40
+ corner?: React.ReactNode;
41
+ children: React.ReactNode;
42
+ footer?: React.ReactNode;
43
+ /** Card width. Ignored by the screen layout, which has the whole width. */
44
+ maxWidth?: number;
45
+ }
46
+
47
+ export function AuthPage({
48
+ layout = "card",
49
+ title,
50
+ subtitle,
51
+ icon,
52
+ corner,
53
+ children,
54
+ footer,
55
+ maxWidth = 440,
56
+ }: AuthPageProps) {
57
+ const { colorScheme } = useColorScheme();
58
+ const isDark = colorScheme === "dark";
59
+
60
+ if (layout === "screen") {
61
+ return (
62
+ <SafeAreaView className="flex-1 bg-background dark:bg-black">
63
+ <StatusBar style={isDark ? "light" : "dark"} />
64
+ <ScrollView
65
+ className="flex-1"
66
+ contentContainerStyle={{ flexGrow: 1 }}
67
+ keyboardShouldPersistTaps="handled"
68
+ >
69
+ <View className="flex-1 px-6 pb-10 pt-4">
70
+ {/* The corner control sits in the 40px above the heading rather
71
+ than absolutely over the content, which is where the card
72
+ layout puts it. Same row every screen in the flow leaves for a
73
+ back control, so the headings all land in the same place. */}
74
+ <View className="mb-4 h-10 flex-row items-center">{corner}</View>
75
+
76
+ <Heading
77
+ className="text-3xl"
78
+ numberOfLines={1}
79
+ adjustsFontSizeToFit
80
+ minimumFontScale={0.7}
81
+ >
82
+ {title}
83
+ </Heading>
84
+ {subtitle ? (
85
+ <MutedText className="mt-2 text-base leading-6">{subtitle}</MutedText>
86
+ ) : null}
87
+
88
+ <View className="flex-1 pt-6">{children}</View>
89
+ {footer ? <View className="gap-2 pt-6">{footer}</View> : null}
90
+ </View>
91
+ </ScrollView>
92
+ </SafeAreaView>
93
+ );
94
+ }
95
+
96
+ return (
97
+ <SafeAreaView className="flex-1 bg-background dark:bg-black">
98
+ <StatusBar style={isDark ? "light" : "dark"} />
99
+ <ScrollView
100
+ className="flex-1"
101
+ contentContainerClassName="flex-1 items-center justify-center p-6"
102
+ keyboardShouldPersistTaps="handled"
103
+ >
104
+ {corner ? <View className="absolute left-6 top-4 mt-4 self-start">{corner}</View> : null}
105
+
106
+ <Card className="w-full" style={{ maxWidth }}>
107
+ <CardHeader className="items-center">
108
+ {icon}
109
+ <CardTitle className="text-center text-2xl">{title}</CardTitle>
110
+ {subtitle ? (
111
+ <CardDescription className="text-center text-base">{subtitle}</CardDescription>
112
+ ) : null}
113
+ </CardHeader>
114
+
115
+ <CardContent className="gap-4 pb-3">{children}</CardContent>
116
+
117
+ {footer ? (
118
+ <CardFooter className="flex-col items-stretch gap-1 border-t-0 pt-0">
119
+ {footer}
120
+ </CardFooter>
121
+ ) : null}
122
+ </Card>
123
+ </ScrollView>
124
+ </SafeAreaView>
125
+ );
126
+ }
127
+
128
+ export default AuthPage;
@@ -279,6 +279,18 @@ export interface AuthScreenProps {
279
279
  * hidden too, since there is no stage behind it.
280
280
  */
281
281
  showWelcome?: boolean;
282
+ /**
283
+ * Card title, e.g. "Log in to Entropy".
284
+ *
285
+ * Undefined renders no heading, which is the right default for an app whose
286
+ * own login the user reached deliberately -- the surrounding brand already
287
+ * says where they are. Set it on an identity provider, where the user is
288
+ * frequently redirected in from a relying application and the card is the
289
+ * only thing on screen that can say whose credentials to type.
290
+ */
291
+ title?: string;
292
+ /** Optional line under `title`. Ignored unless `title` is set. */
293
+ subtitle?: string;
282
294
  }
283
295
 
284
296
  /**
@@ -314,6 +326,8 @@ export function AuthScreen({
314
326
  onSignUp,
315
327
  signUpSuccessMessage = "Account created. Check your email to verify the address, then sign in.",
316
328
  showWelcome = true,
329
+ title,
330
+ subtitle,
317
331
  }: AuthScreenProps = {}) {
318
332
  const {
319
333
  login: authLogin,
@@ -1139,6 +1153,24 @@ export function AuthScreen({
1139
1153
  that otherwise all collects above the footer.
1140
1154
  Applies to both modes — the header is shared. */}
1141
1155
  <CardHeader className="gap-5 pb-3 pt-5">
1156
+ {/* WHOSE login this is.
1157
+ Without it the card is a pair of unlabelled
1158
+ fields under a Sign In / Sign Up toggle, which
1159
+ is fine when the user arrived from the app they
1160
+ are signing into and actively confusing when an
1161
+ OIDC redirect dropped them here from somewhere
1162
+ else -- the case an identity provider is FOR.
1163
+ Rendered above the toggle so it reads as a
1164
+ title for the card rather than a label for
1165
+ whichever mode happens to be selected. */}
1166
+ {title ? (
1167
+ <View className="gap-1">
1168
+ <CardTitle>{title}</CardTitle>
1169
+ {subtitle ? (
1170
+ <MutedText className="text-sm leading-5">{subtitle}</MutedText>
1171
+ ) : null}
1172
+ </View>
1173
+ ) : null}
1142
1174
  {signUpEnabled ? (
1143
1175
  <SegmentedToggle<Mode>
1144
1176
  value={mode}
@@ -1,8 +1,6 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
- import { Pressable, ScrollView, View } from "react-native";
3
- import { SafeAreaView } from "react-native-safe-area-context";
2
+ import { Pressable, View } from "react-native";
4
3
  import { useNavigation } from "../../context/NavigationContext";
5
- import { StatusBar } from "expo-status-bar";
6
4
  import { Ionicons } from "@expo/vector-icons";
7
5
  import { useColorScheme } from "nativewind";
8
6
  import * as Haptics from "expo-haptics";
@@ -11,14 +9,6 @@ import QRCode from "react-native-qrcode-svg";
11
9
  import Toast from "react-native-toast-message";
12
10
  import { MonoText, MutedText } from "../../components/text";
13
11
  import { Button } from "../../components/button";
14
- import {
15
- Card,
16
- CardContent,
17
- CardDescription,
18
- CardFooter,
19
- CardHeader,
20
- CardTitle,
21
- } from "../../components/card";
22
12
  import { withClickTracking } from "../../services/telemetry";
23
13
  import { Input } from "../../components/input";
24
14
  import { Separator } from "../../components/separator";
@@ -26,6 +16,7 @@ import AuthService from "../../services/auth.service";
26
16
  import { useAuth } from "../../hooks/useAuth";
27
17
  import { useAppConfig } from "../../context/AppConfigContext";
28
18
  import { getErrorMessage } from "../../utils/error";
19
+ import { AuthPage, type AuthPageLayout } from "./AuthPage";
29
20
 
30
21
  type Step = "prompt" | "setup" | "verify" | "recovery" | "done";
31
22
 
@@ -36,9 +27,11 @@ export interface MfaEnrollScreenProps {
36
27
  * nothing in that slot.
37
28
  */
38
29
  authenticatorAppPromo?: React.ReactNode;
30
+ /** See `AuthPageLayout`. Defaults to the card, as it always was. */
31
+ layout?: AuthPageLayout;
39
32
  }
40
33
 
41
- export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps = {}) {
34
+ export function MfaEnrollScreen({ authenticatorAppPromo, layout }: MfaEnrollScreenProps = {}) {
42
35
  const { refreshUser } = useAuth();
43
36
  const { colorScheme } = useColorScheme();
44
37
  const isDark = colorScheme === "dark";
@@ -159,236 +152,202 @@ export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps
159
152
  }
160
153
  };
161
154
 
162
- return (
163
- <SafeAreaView className="flex-1 bg-background dark:bg-black">
164
- <StatusBar style={isDark ? "light" : "dark"} />
165
- <ScrollView
166
- className="flex-1"
167
- contentContainerClassName="flex-1 items-center justify-center p-6"
168
- keyboardShouldPersistTaps="handled"
169
- >
170
- <Card className="w-full max-w-[400px]">
171
- {step === "prompt" ? (
172
- <>
173
- <CardHeader className="items-center">
174
- <View className="mb-3 h-16 w-16 items-center justify-center rounded-full bg-secondary dark:bg-neutral-800">
175
- <Ionicons
176
- name="shield-checkmark-outline"
177
- size={32}
178
- color={isDark ? "#FFFFFF" : "#000000"}
179
- />
180
- </View>
181
- <CardTitle className="text-center">Secure your account</CardTitle>
182
- <CardDescription className="text-center">
183
- Add an extra layer of security with two-factor authentication (MFA). You&apos;ll
184
- need an authenticator app to generate verification codes.
185
- </CardDescription>
186
- </CardHeader>
187
-
188
- {authenticatorAppPromo ? (
189
- <CardContent className="gap-3">{authenticatorAppPromo}</CardContent>
190
- ) : null}
191
-
192
- <CardFooter className="flex-col items-stretch gap-3 border-t-0">
193
- <Button
194
- className="w-full"
195
- telemetryId="auth.mfa_enroll.start_setup"
196
- onPress={handleStartSetup}
197
- isLoading={isLoading}
198
- accessibilityLabel="Enable MFA"
199
- accessibilityRole="button"
200
- >
201
- Enable MFA
202
- </Button>
203
- <Button
204
- variant="ghost"
205
- className="w-full"
206
- telemetryId="auth.mfa_enroll.skip"
207
- onPress={handleSkip}
208
- accessibilityLabel="Skip for now"
209
- accessibilityRole="button"
210
- >
211
- Skip for now
212
- </Button>
213
- </CardFooter>
214
- </>
215
- ) : null}
216
-
217
- {step === "setup" && setupData ? (
218
- <>
219
- <CardHeader className="items-center">
220
- <CardTitle className="text-center">Scan QR code</CardTitle>
221
- <CardDescription className="text-center">
222
- Open your authenticator app and scan this QR code, or enter the secret key
223
- manually.
224
- </CardDescription>
225
- </CardHeader>
226
-
227
- <CardContent className="gap-4">
228
- <View className="items-center">
229
- <View className="rounded-xl bg-white p-4">
230
- <QRCode
231
- value={setupData.uri}
232
- size={200}
233
- backgroundColor="#FFFFFF"
234
- color="#000000"
235
- />
236
- </View>
237
- <MutedText className="mt-2 text-center text-xs">
238
- Scan with your authenticator app
239
- </MutedText>
240
- </View>
241
-
242
- <Separator />
155
+ /**
156
+ * What the page says, per step.
157
+ *
158
+ * Held here rather than inline because the frame below takes one title and
159
+ * one subtitle, and three branches each rendering their own header is what
160
+ * made this screen three screens wearing the same card.
161
+ */
162
+ const page =
163
+ step === "prompt"
164
+ ? {
165
+ title: "Secure your account",
166
+ subtitle:
167
+ "Two-factor authentication asks for a code from an authenticator app as well as your password.",
168
+ }
169
+ : step === "setup"
170
+ ? {
171
+ title: "Scan the QR code",
172
+ subtitle: "Open your authenticator app and scan this, or enter the key by hand.",
173
+ }
174
+ : { title: "MFA is on", subtitle: "Save these recovery codes somewhere safe." };
243
175
 
244
- <View>
245
- <MutedText className="mb-2 text-center text-xs font-medium">
246
- Can&apos;t scan? Enter the secret key manually
247
- </MutedText>
248
- <Pressable
249
- onPress={withClickTracking("auth.mfa_enroll.copy_to_clipboard", () => copyToClipboard(setupData.secret, "Secret"))}
250
- className="rounded-lg bg-secondary p-3 active:opacity-70 dark:bg-neutral-800"
251
- accessibilityRole="button"
252
- accessibilityLabel="Copy secret key"
253
- accessibilityHint="Double tap to copy the secret key to clipboard"
254
- >
255
- <MonoText className="text-center text-base font-bold tracking-widest">
256
- {setupData.secret}
257
- </MonoText>
258
- </Pressable>
259
- <MutedText className="mt-1 text-center text-xs">Tap to copy</MutedText>
260
- </View>
176
+ return (
177
+ <AuthPage
178
+ layout={layout}
179
+ title={page.title}
180
+ subtitle={page.subtitle}
181
+ icon={
182
+ step === "setup" ? null : (
183
+ <View className="mb-3 h-16 w-16 items-center justify-center rounded-full bg-secondary dark:bg-neutral-800">
184
+ <Ionicons
185
+ name={step === "prompt" ? "shield-checkmark-outline" : "shield-checkmark"}
186
+ size={32}
187
+ color={isDark ? "#FFFFFF" : "#000000"}
188
+ />
189
+ </View>
190
+ )
191
+ }
192
+ maxWidth={400}
193
+ footer={
194
+ step === "prompt" ? (
195
+ <>
196
+ <Button
197
+ className="w-full"
198
+ telemetryId="auth.mfa_enroll.start_setup"
199
+ onPress={handleStartSetup}
200
+ isLoading={isLoading}
201
+ accessibilityLabel="Enable MFA"
202
+ accessibilityRole="button"
203
+ >
204
+ Enable MFA
205
+ </Button>
206
+ <Button
207
+ variant="ghost"
208
+ className="w-full"
209
+ telemetryId="auth.mfa_enroll.skip"
210
+ onPress={handleSkip}
211
+ accessibilityLabel="Skip for now"
212
+ accessibilityRole="button"
213
+ >
214
+ Skip for now
215
+ </Button>
216
+ </>
217
+ ) : step === "setup" ? (
218
+ <>
219
+ <Button
220
+ className="w-full"
221
+ telemetryId="auth.mfa_enroll.verify_code"
222
+ onPress={handleVerifyCode}
223
+ isLoading={isLoading}
224
+ disabled={code.length !== 6}
225
+ accessibilityLabel="Verify and enable MFA"
226
+ accessibilityRole="button"
227
+ >
228
+ Verify &amp; Enable
229
+ </Button>
230
+ <Button
231
+ variant="ghost"
232
+ className="w-full"
233
+ telemetryId="auth.mfa_enroll.skip_2"
234
+ onPress={handleSkip}
235
+ accessibilityLabel="Skip for now"
236
+ accessibilityRole="button"
237
+ >
238
+ Skip for now
239
+ </Button>
240
+ </>
241
+ ) : (
242
+ <Button
243
+ className="w-full"
244
+ telemetryId="auth.mfa_enroll.finish"
245
+ onPress={handleFinish}
246
+ accessibilityLabel={`Continue to ${appName}`}
247
+ accessibilityRole="button"
248
+ >
249
+ Continue to {appName}
250
+ </Button>
251
+ )
252
+ }
253
+ >
254
+ {step === "prompt" ? authenticatorAppPromo : null}
261
255
 
262
- <Pressable
263
- onPress={withClickTracking("auth.mfa_enroll.copy_to_clipboard_2", () => copyToClipboard(setupData.uri, "URI"))}
264
- className="rounded-lg bg-secondary px-3 py-2 active:opacity-70 dark:bg-neutral-800"
265
- accessibilityRole="button"
266
- accessibilityLabel="Copy full OTP URI"
267
- >
268
- <MutedText className="text-center text-xs">Copy full OTP URI</MutedText>
269
- </Pressable>
256
+ {step === "setup" && setupData ? (
257
+ <>
258
+ <View className="items-center">
259
+ <View className="rounded-xl bg-white p-4">
260
+ <QRCode value={setupData.uri} size={200} backgroundColor="#FFFFFF" color="#000000" />
261
+ </View>
262
+ </View>
270
263
 
271
- <Separator />
264
+ <Separator />
272
265
 
273
- <View>
274
- <MutedText className="mb-1.5 text-center text-xs font-medium">
275
- Enter the 6-digit code from your authenticator
276
- </MutedText>
277
- <Input
278
- value={code}
279
- onChangeText={(text) => {
280
- setCode(text.replace(/[^0-9]/g, "").slice(0, 6));
281
- if (error) {
282
- setError("");
283
- }
284
- }}
285
- error={error}
286
- placeholder="000000"
287
- keyboardType="number-pad"
288
- className="text-center font-mono text-xl tracking-widest"
289
- returnKeyType="done"
290
- onSubmitEditing={handleVerifyCode}
291
- accessibilityLabel="Authenticator code"
292
- />
293
- </View>
294
- </CardContent>
266
+ <View>
267
+ <MutedText className="mb-2 text-center text-xs font-medium">
268
+ Can&apos;t scan? Enter the key by hand
269
+ </MutedText>
270
+ <Pressable
271
+ onPress={withClickTracking("auth.mfa_enroll.copy_to_clipboard", () =>
272
+ copyToClipboard(setupData.secret, "Secret")
273
+ )}
274
+ className="rounded-lg bg-secondary p-3 active:opacity-70 dark:bg-neutral-800"
275
+ accessibilityRole="button"
276
+ accessibilityLabel="Copy secret key"
277
+ accessibilityHint="Double tap to copy the secret key to clipboard"
278
+ >
279
+ <MonoText className="text-center text-base font-bold tracking-widest">
280
+ {setupData.secret}
281
+ </MonoText>
282
+ </Pressable>
283
+ <MutedText className="mt-1 text-center text-xs">Tap to copy</MutedText>
284
+ </View>
295
285
 
296
- <CardFooter className="flex-col items-stretch gap-3 border-t-0">
297
- <Button
298
- className="w-full"
299
- telemetryId="auth.mfa_enroll.verify_code"
300
- onPress={handleVerifyCode}
301
- isLoading={isLoading}
302
- disabled={code.length !== 6}
303
- accessibilityLabel="Verify and enable MFA"
304
- accessibilityRole="button"
305
- >
306
- Verify & Enable
307
- </Button>
308
- <Button
309
- variant="ghost"
310
- className="w-full"
311
- telemetryId="auth.mfa_enroll.skip_2"
312
- onPress={handleSkip}
313
- accessibilityLabel="Skip for now"
314
- accessibilityRole="button"
315
- >
316
- Skip for now
317
- </Button>
318
- </CardFooter>
319
- </>
320
- ) : null}
286
+ <Pressable
287
+ onPress={withClickTracking("auth.mfa_enroll.copy_to_clipboard_2", () =>
288
+ copyToClipboard(setupData.uri, "URI")
289
+ )}
290
+ className="rounded-lg bg-secondary px-3 py-2 active:opacity-70 dark:bg-neutral-800"
291
+ accessibilityRole="button"
292
+ accessibilityLabel="Copy full OTP URI"
293
+ >
294
+ <MutedText className="text-center text-xs">Copy full OTP URI</MutedText>
295
+ </Pressable>
321
296
 
322
- {step === "recovery" && setupData ? (
323
- <>
324
- <CardHeader className="items-center">
325
- <View className="mb-3 h-16 w-16 items-center justify-center rounded-full bg-secondary dark:bg-neutral-800">
326
- <Ionicons
327
- name="shield-checkmark"
328
- size={32}
329
- color={isDark ? "#FFFFFF" : "#000000"}
330
- />
331
- </View>
332
- <CardTitle className="text-center">MFA enabled!</CardTitle>
333
- <CardDescription className="text-center">
334
- Save these recovery codes in a safe place. You&apos;ll need them to access your
335
- account if you lose your authenticator device.
336
- </CardDescription>
337
- </CardHeader>
297
+ <Separator />
338
298
 
339
- <CardContent className="gap-3">
340
- <View className="rounded-lg border border-border bg-secondary p-4 dark:border-neutral-700 dark:bg-neutral-800">
341
- {setupData.recovery_codes.map((rc) => (
342
- <MonoText key={rc} className="text-center text-sm leading-6">
343
- {rc}
344
- </MonoText>
345
- ))}
346
- </View>
299
+ <View>
300
+ <MutedText className="mb-1.5 text-center text-xs font-medium">
301
+ Enter the 6-digit code from your authenticator
302
+ </MutedText>
303
+ <Input
304
+ value={code}
305
+ onChangeText={(text) => {
306
+ setCode(text.replace(/[^0-9]/g, "").slice(0, 6));
307
+ if (error) {
308
+ setError("");
309
+ }
310
+ }}
311
+ error={error}
312
+ placeholder="000000"
313
+ keyboardType="number-pad"
314
+ className="text-center font-mono text-xl tracking-widest"
315
+ returnKeyType="done"
316
+ onSubmitEditing={handleVerifyCode}
317
+ accessibilityLabel="Authenticator code"
318
+ />
319
+ </View>
320
+ </>
321
+ ) : null}
347
322
 
348
- <Button
349
- variant="outline"
350
- className="w-full"
351
- telemetryId="auth.mfa_enroll.copy_to_clipboard_3"
352
- onPress={() =>
353
- copyToClipboard(setupData.recovery_codes.join("\n"), "Recovery codes")
354
- }
355
- accessibilityLabel="Copy all recovery codes"
356
- accessibilityRole="button"
357
- >
358
- {copied === "Recovery codes" ? "Copied!" : "Copy all codes"}
359
- </Button>
323
+ {step === "recovery" && setupData ? (
324
+ <>
325
+ <View className="rounded-lg bg-secondary p-4 dark:bg-neutral-800">
326
+ {setupData.recovery_codes.map((rc) => (
327
+ <MonoText key={rc} className="text-center text-sm leading-6">
328
+ {rc}
329
+ </MonoText>
330
+ ))}
331
+ </View>
360
332
 
361
- <View className="rounded-lg bg-secondary p-3 dark:bg-neutral-800">
362
- <View className="flex-row items-start gap-2">
363
- <Ionicons
364
- name="warning-outline"
365
- size={16}
366
- color={isDark ? "#F59E0B" : "#D97706"}
367
- />
368
- <MutedText className="flex-1 text-xs leading-4">
369
- Each code can only be used once. Store them securely — they cannot be
370
- recovered if lost.
371
- </MutedText>
372
- </View>
373
- </View>
374
- </CardContent>
333
+ <Button
334
+ variant="outline"
335
+ className="w-full"
336
+ telemetryId="auth.mfa_enroll.copy_to_clipboard_3"
337
+ onPress={() => copyToClipboard(setupData.recovery_codes.join("\n"), "Recovery codes")}
338
+ accessibilityLabel="Copy all recovery codes"
339
+ accessibilityRole="button"
340
+ >
341
+ {copied === "Recovery codes" ? "Copied!" : "Copy all codes"}
342
+ </Button>
375
343
 
376
- <CardFooter className="border-t-0">
377
- <Button
378
- className="w-full"
379
- telemetryId="auth.mfa_enroll.finish"
380
- onPress={handleFinish}
381
- accessibilityLabel={`Continue to ${appName}`}
382
- accessibilityRole="button"
383
- >
384
- Continue to {appName}
385
- </Button>
386
- </CardFooter>
387
- </>
388
- ) : null}
389
- </Card>
390
- </ScrollView>
391
- </SafeAreaView>
344
+ <View className="flex-row items-start gap-2 rounded-lg bg-secondary p-3 dark:bg-neutral-800">
345
+ <Ionicons name="warning-outline" size={16} color={isDark ? "#F59E0B" : "#D97706"} />
346
+ <MutedText className="flex-1 text-xs leading-4">Each code works once.</MutedText>
347
+ </View>
348
+ </>
349
+ ) : null}
350
+ </AuthPage>
392
351
  );
393
352
  }
394
353