@entropy-softworks/ui 2026.9.8 → 2026.9.10
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/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 -44
- 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/MfaEnrollScreen.tsx +191 -232
- package/src/screens/auth/VerifyEmailScreen.tsx +84 -101
- package/src/screens/auth/index.ts +5 -14
|
@@ -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
|
|