@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.
- package/lib/screens/auth/AuthPage.d.ts +33 -0
- package/lib/screens/auth/AuthPage.d.ts.map +1 -0
- package/lib/screens/auth/AuthPage.jsx +55 -0
- package/lib/screens/auth/AuthPage.jsx.map +1 -0
- package/lib/screens/auth/AuthScreen.d.ts +13 -1
- package/lib/screens/auth/AuthScreen.d.ts.map +1 -1
- package/lib/screens/auth/AuthScreen.jsx +15 -1
- package/lib/screens/auth/AuthScreen.jsx.map +1 -1
- package/lib/screens/auth/MfaEnrollScreen.d.ts +4 -1
- package/lib/screens/auth/MfaEnrollScreen.d.ts.map +1 -1
- package/lib/screens/auth/MfaEnrollScreen.jsx +85 -124
- package/lib/screens/auth/MfaEnrollScreen.jsx.map +1 -1
- package/lib/screens/auth/VerifyEmailScreen.d.ts +6 -1
- package/lib/screens/auth/VerifyEmailScreen.d.ts.map +1 -1
- package/lib/screens/auth/VerifyEmailScreen.jsx +22 -50
- package/lib/screens/auth/VerifyEmailScreen.jsx.map +1 -1
- package/lib/screens/auth/index.d.ts +5 -4
- package/lib/screens/auth/index.d.ts.map +1 -1
- package/lib/screens/auth/index.js +4 -3
- package/lib/screens/auth/index.js.map +1 -1
- package/package.json +1 -1
- package/src/screens/auth/AuthPage.tsx +128 -0
- package/src/screens/auth/AuthScreen.tsx +32 -0
- package/src/screens/auth/MfaEnrollScreen.tsx +191 -232
- package/src/screens/auth/VerifyEmailScreen.tsx +83 -106
- package/src/screens/auth/index.ts +5 -14
|
@@ -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,
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
<CardDescription className="text-center">
|
|
183
|
-
Add an extra layer of security with two-factor authentication (MFA). You'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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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 & 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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
264
|
+
<Separator />
|
|
272
265
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
/>
|
|
293
|
-
</View>
|
|
294
|
-
</CardContent>
|
|
266
|
+
<View>
|
|
267
|
+
<MutedText className="mb-2 text-center text-xs font-medium">
|
|
268
|
+
Can'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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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'll need them to access your
|
|
335
|
-
account if you lose your authenticator device.
|
|
336
|
-
</CardDescription>
|
|
337
|
-
</CardHeader>
|
|
297
|
+
<Separator />
|
|
338
298
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|