@ramonclaudio/create-vexpo 0.1.0 → 0.1.1
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/README.md +10 -10
- package/dist/index.js +8 -7
- package/dist/templates/default/.eas/workflows/asc-events.yml +9 -6
- package/dist/templates/default/.eas/workflows/deploy-production.yml +28 -15
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +3 -2
- package/dist/templates/default/.eas/workflows/pr-preview.yml +12 -21
- package/dist/templates/default/.eas/workflows/release.yml +3 -7
- package/dist/templates/default/.eas/workflows/rollback.yml +54 -28
- package/dist/templates/default/.eas/workflows/rollout.yml +27 -33
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +1 -5
- package/dist/templates/default/.eas/workflows/testflight.yml +3 -7
- package/dist/templates/default/.github/workflows/check.yml +20 -12
- package/dist/templates/default/.maestro/launch.yaml +19 -10
- package/dist/templates/default/AGENTS.md +25 -8
- package/dist/templates/default/DESIGN.md +14 -10
- package/dist/templates/default/README.md +83 -78
- package/dist/templates/default/SETUP.md +159 -152
- package/dist/templates/default/__tests__/convex/_auth-harness.test.ts +112 -0
- package/dist/templates/default/__tests__/convex/_harness.ts +132 -0
- package/dist/templates/default/__tests__/convex/appAttest.test.ts +172 -0
- package/dist/templates/default/__tests__/convex/appAttestStore.test.ts +48 -0
- package/dist/templates/default/__tests__/convex/pushTokens-remove.test.ts +106 -0
- package/dist/templates/default/__tests__/convex/pushTokens-upsert.test.ts +146 -0
- package/dist/templates/default/__tests__/convex/users-deleteAccount.test.ts +140 -0
- package/dist/templates/default/__tests__/convex/users-deleteAvatar.test.ts +104 -0
- package/dist/templates/default/__tests__/convex/users-getMe.test.ts +98 -0
- package/dist/templates/default/__tests__/convex/users-getUser.test.ts +120 -0
- package/dist/templates/default/__tests__/convex/users-hardDeleteExpired.test.ts +67 -0
- package/dist/templates/default/__tests__/convex/users-restoreAccount.test.ts +96 -0
- package/dist/templates/default/__tests__/convex/users-updateAvatar.test.ts +92 -0
- package/dist/templates/default/__tests__/convex/users-updateProfile.test.ts +126 -0
- package/dist/templates/default/__tests__/convex/webhook.test.ts +31 -0
- package/dist/templates/default/__tests__/lib/deep-link.test.ts +51 -6
- package/dist/templates/default/__tests__/lib/schemas.test.ts +205 -0
- package/dist/templates/default/__tests__/lib/text-style.test.ts +31 -0
- package/dist/templates/default/_env.example +7 -7
- package/dist/templates/default/_gitattributes +1 -1
- package/dist/templates/default/_gitignore +17 -2
- package/dist/templates/default/_npmrc +7 -0
- package/dist/templates/default/_oxlintrc.json +1 -1
- package/dist/templates/default/app-store/accessibility.config.json +20 -0
- package/dist/templates/default/app-store/privacy.config.json +27 -0
- package/dist/templates/default/app.config.ts +105 -33
- package/dist/templates/default/app.json +1 -9
- package/dist/templates/default/convex/_generated/api.d.ts +12 -0
- package/dist/templates/default/convex/admin.ts +0 -13
- package/dist/templates/default/convex/appAttest.ts +467 -0
- package/dist/templates/default/convex/appAttestStore.ts +141 -0
- package/dist/templates/default/convex/apple.ts +53 -0
- package/dist/templates/default/convex/auth.ts +6 -45
- package/dist/templates/default/convex/constants.ts +2 -7
- package/dist/templates/default/convex/crons.ts +12 -5
- package/dist/templates/default/convex/email.ts +4 -24
- package/dist/templates/default/convex/env.ts +0 -4
- package/dist/templates/default/convex/errors.ts +0 -7
- package/dist/templates/default/convex/functions.ts +0 -26
- package/dist/templates/default/convex/http.ts +3 -5
- package/dist/templates/default/convex/log.ts +2 -25
- package/dist/templates/default/convex/pushSender.ts +145 -0
- package/dist/templates/default/convex/pushTokens.ts +110 -13
- package/dist/templates/default/convex/rateLimit.ts +8 -39
- package/dist/templates/default/convex/schema.ts +48 -5
- package/dist/templates/default/convex/tsconfig.json +1 -0
- package/dist/templates/default/convex/users.ts +143 -61
- package/dist/templates/default/convex/validators.ts +1 -38
- package/dist/templates/default/convex/webhook.ts +1 -31
- package/dist/templates/default/convex.json +1 -2
- package/dist/templates/default/metro.config.js +9 -1
- package/dist/templates/default/package.json +67 -70
- package/dist/templates/default/plugins/README.md +5 -1
- package/dist/templates/default/scripts/README.md +9 -9
- package/dist/templates/default/scripts/_run.mjs +3 -20
- package/dist/templates/default/scripts/clean.ts +81 -69
- package/dist/templates/default/scripts/gen-update-cert.mjs +98 -0
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home)/index.tsx +21 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home,search)/_layout.tsx +9 -8
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(search)/index.tsx +26 -24
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/_layout.tsx +3 -4
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/_layout.tsx +10 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/index.tsx +81 -51
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/preferences.tsx +72 -12
- package/dist/templates/default/src/app/(app)/_layout.tsx +147 -0
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/_layout.tsx +4 -5
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/forgot-password.tsx +15 -9
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/reset-password.tsx +88 -14
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-in.tsx +65 -35
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-up.tsx +131 -196
- package/dist/templates/default/src/app/(app)/debug.tsx +479 -0
- package/dist/templates/default/{app → src/app}/(app)/help.tsx +76 -64
- package/dist/templates/default/{app → src/app}/(app)/linked.tsx +21 -27
- package/dist/templates/default/{app → src/app}/(app)/privacy.tsx +35 -8
- package/dist/templates/default/src/app/(app)/profile/change-password.tsx +264 -0
- package/dist/templates/default/{app/(app)/profile.tsx → src/app/(app)/profile/index.tsx} +179 -255
- package/dist/templates/default/src/app/(app)/restore-account.tsx +192 -0
- package/dist/templates/default/src/app/(app)/sessions.tsx +287 -0
- package/dist/templates/default/src/app/(app)/welcome.tsx +194 -0
- package/dist/templates/default/src/app/+native-intent.tsx +25 -0
- package/dist/templates/default/src/app/+not-found.tsx +43 -0
- package/dist/templates/default/{app → src/app}/_layout.tsx +28 -37
- package/dist/templates/default/src/components/auth/apple-button.tsx +51 -0
- package/dist/templates/default/{components → src/components}/auth/otp-verification.tsx +79 -58
- package/dist/templates/default/{components → src/components}/auth/password-field.tsx +74 -18
- package/dist/templates/default/src/components/auth/segmented-toggle.tsx +71 -0
- package/dist/templates/default/src/components/ui/content-unavailable.tsx +81 -0
- package/dist/templates/default/src/components/ui/convex-error.tsx +21 -0
- package/dist/templates/default/src/components/ui/error-boundary.tsx +89 -0
- package/dist/templates/default/{components → src/components}/ui/loading-screen.tsx +5 -4
- package/dist/templates/default/{components → src/components}/ui/material.tsx +50 -17
- package/dist/templates/default/src/components/ui/offline-banner.tsx +59 -0
- package/dist/templates/default/{components → src/components}/ui/prominent-button.tsx +8 -11
- package/dist/templates/default/{components → src/components}/ui/skeleton.tsx +31 -13
- package/dist/templates/default/src/components/ui/status-text.tsx +64 -0
- package/dist/templates/default/src/components/ui/update-banner.tsx +85 -0
- package/dist/templates/default/{constants → src/constants}/layout.ts +0 -6
- package/dist/templates/default/{constants → src/constants}/theme.ts +49 -64
- package/dist/templates/default/{constants → src/constants}/ui.ts +13 -4
- package/dist/templates/default/src/hooks/use-debounce.ts +12 -0
- package/dist/templates/default/src/hooks/use-deep-link.ts +51 -0
- package/dist/templates/default/src/hooks/use-delete-account.ts +35 -0
- package/dist/templates/default/src/hooks/use-motion-screen-options.ts +13 -0
- package/dist/templates/default/src/hooks/use-network.ts +34 -0
- package/dist/templates/default/{hooks → src/hooks}/use-notifications.ts +39 -30
- package/dist/templates/default/src/hooks/use-reduce-transparency.ts +30 -0
- package/dist/templates/default/{hooks → src/hooks}/use-theme.ts +0 -5
- package/dist/templates/default/src/lib/appAttest.ts +78 -0
- package/dist/templates/default/src/lib/assets.ts +9 -0
- package/dist/templates/default/src/lib/deep-link.ts +82 -0
- package/dist/templates/default/{lib → src/lib}/dev-menu.ts +0 -4
- package/dist/templates/default/{lib → src/lib}/device.ts +1 -13
- package/dist/templates/default/{lib → src/lib}/dynamic-font.ts +13 -10
- package/dist/templates/default/src/lib/dynamic-symbol-size.ts +33 -0
- package/dist/templates/default/src/lib/masks.ts +21 -0
- package/dist/templates/default/src/lib/native-state.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/notifications.ts +7 -45
- package/dist/templates/default/{lib → src/lib}/preferences.ts +0 -2
- package/dist/templates/default/{lib → src/lib}/schemas.ts +19 -16
- package/dist/templates/default/src/lib/text-style.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/updates.ts +0 -7
- package/dist/templates/default/store.config.json +1 -1
- package/dist/templates/default/tsconfig.json +3 -1
- package/dist/templates/default/vitest.config.ts +8 -1
- package/package.json +5 -5
- package/dist/templates/default/app/(app)/_layout.tsx +0 -73
- package/dist/templates/default/app/(app)/debug.tsx +0 -389
- package/dist/templates/default/app/(app)/sessions.tsx +0 -191
- package/dist/templates/default/app/(app)/welcome.tsx +0 -140
- package/dist/templates/default/app/+native-intent.tsx +0 -14
- package/dist/templates/default/app/+not-found.tsx +0 -51
- package/dist/templates/default/bun.lock +0 -1860
- package/dist/templates/default/components/auth/segmented-toggle.tsx +0 -47
- package/dist/templates/default/components/ui/convex-error.tsx +0 -32
- package/dist/templates/default/components/ui/error-boundary.tsx +0 -57
- package/dist/templates/default/components/ui/offline-banner.tsx +0 -58
- package/dist/templates/default/components/ui/status-text.tsx +0 -49
- package/dist/templates/default/components/ui/update-banner.tsx +0 -82
- package/dist/templates/default/fingerprint.config.js +0 -9
- package/dist/templates/default/hooks/use-debounce.ts +0 -20
- package/dist/templates/default/hooks/use-deep-link.ts +0 -43
- package/dist/templates/default/hooks/use-network.ts +0 -11
- package/dist/templates/default/lib/assets.ts +0 -17
- package/dist/templates/default/lib/deep-link.ts +0 -71
- package/dist/templates/default/patches/PR-368.patch +0 -91
- package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-navigation-tracking.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-onboarding.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-reduced-motion.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-updates.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/a11y.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/app.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/auth-client.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/convex-auth.tsx +0 -0
- /package/dist/templates/default/{lib → src/lib}/env.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/haptics.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/storage.ts +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Stack } from "expo-router";
|
|
2
|
+
import { useQuery } from "convex/react";
|
|
3
|
+
|
|
4
|
+
import { api } from "@/convex/_generated/api";
|
|
5
|
+
import { authClient } from "@/lib/auth-client";
|
|
6
|
+
import { useDeepLinkHandler } from "@/hooks/use-deep-link";
|
|
7
|
+
import { useColors } from "@/hooks/use-theme";
|
|
8
|
+
import { useMotionScreenOptions } from "@/hooks/use-motion-screen-options";
|
|
9
|
+
import { useReducedMotion } from "@/hooks/use-reduced-motion";
|
|
10
|
+
import { FontFamily } from "@/constants/layout";
|
|
11
|
+
import { LoadingScreen } from "@/components/ui/loading-screen";
|
|
12
|
+
|
|
13
|
+
export { AppErrorBoundary as ErrorBoundary } from "@/components/ui/error-boundary";
|
|
14
|
+
|
|
15
|
+
export function SuspenseFallback() {
|
|
16
|
+
return <LoadingScreen testID="app-content-loading" />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Anchor the back-stack root so a guard flip lands on the tabs root, not
|
|
20
|
+
// whichever screen declares first.
|
|
21
|
+
export const unstable_settings = { anchor: "(tabs)" } as const;
|
|
22
|
+
|
|
23
|
+
export default function AppLayout() {
|
|
24
|
+
const { data: session } = authClient.useSession();
|
|
25
|
+
const isAuthenticated = !!session?.session;
|
|
26
|
+
|
|
27
|
+
// Skipped while unauthed because Convex queries need a live JWT.
|
|
28
|
+
const me = useQuery(api.users.getMe, isAuthenticated ? {} : "skip");
|
|
29
|
+
const isAccountDeleted = !!me?.deletedAt;
|
|
30
|
+
|
|
31
|
+
useDeepLinkHandler();
|
|
32
|
+
|
|
33
|
+
const colors = useColors();
|
|
34
|
+
const reduceMotion = useReducedMotion();
|
|
35
|
+
const motion = useMotionScreenOptions("slide_from_right", 300);
|
|
36
|
+
const headerTint = colors.foreground as string;
|
|
37
|
+
const titleStyle = { color: headerTint, fontFamily: FontFamily.semiBold };
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Stack
|
|
41
|
+
screenOptions={{
|
|
42
|
+
...motion,
|
|
43
|
+
headerShown: false,
|
|
44
|
+
contentStyle: { backgroundColor: colors.background as string },
|
|
45
|
+
headerBackTitle: "Back",
|
|
46
|
+
headerTintColor: headerTint,
|
|
47
|
+
headerShadowVisible: false,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<Stack.Protected guard={isAuthenticated && !isAccountDeleted}>
|
|
51
|
+
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
52
|
+
|
|
53
|
+
<Stack.Screen
|
|
54
|
+
name="welcome"
|
|
55
|
+
options={{
|
|
56
|
+
headerShown: false,
|
|
57
|
+
presentation: "fullScreenModal",
|
|
58
|
+
gestureEnabled: false,
|
|
59
|
+
animation: reduceMotion ? "none" : "fade",
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<Stack.Screen name="debug">
|
|
64
|
+
<Stack.Header transparent />
|
|
65
|
+
<Stack.Screen.Title style={titleStyle}>Debug</Stack.Screen.Title>
|
|
66
|
+
<Stack.Screen.BackButton withMenu>Settings</Stack.Screen.BackButton>
|
|
67
|
+
</Stack.Screen>
|
|
68
|
+
|
|
69
|
+
<Stack.Screen name="help">
|
|
70
|
+
<Stack.Header transparent />
|
|
71
|
+
<Stack.Screen.Title style={titleStyle}>Help</Stack.Screen.Title>
|
|
72
|
+
<Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
|
|
73
|
+
</Stack.Screen>
|
|
74
|
+
|
|
75
|
+
<Stack.Screen name="privacy">
|
|
76
|
+
<Stack.Header transparent />
|
|
77
|
+
<Stack.Screen.Title style={titleStyle}>Privacy</Stack.Screen.Title>
|
|
78
|
+
<Stack.Screen.BackButton displayMode="minimal" withMenu>
|
|
79
|
+
Settings
|
|
80
|
+
</Stack.Screen.BackButton>
|
|
81
|
+
</Stack.Screen>
|
|
82
|
+
|
|
83
|
+
<Stack.Screen
|
|
84
|
+
name="linked"
|
|
85
|
+
options={{
|
|
86
|
+
headerShown: true,
|
|
87
|
+
title: "Linked",
|
|
88
|
+
headerTitleStyle: titleStyle,
|
|
89
|
+
presentation: "formSheet",
|
|
90
|
+
sheetAllowedDetents: [0.5, 1],
|
|
91
|
+
sheetGrabberVisible: true,
|
|
92
|
+
sheetCornerRadius: 24,
|
|
93
|
+
sheetLargestUndimmedDetentIndex: 0,
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
<Stack.Screen name="profile/index" options={{ headerShown: true }}>
|
|
98
|
+
<Stack.Header transparent />
|
|
99
|
+
<Stack.Screen.Title style={titleStyle}>Profile</Stack.Screen.Title>
|
|
100
|
+
<Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
|
|
101
|
+
</Stack.Screen>
|
|
102
|
+
|
|
103
|
+
<Stack.Screen
|
|
104
|
+
name="profile/change-password"
|
|
105
|
+
options={{ headerShown: true, presentation: "modal" }}
|
|
106
|
+
>
|
|
107
|
+
<Stack.Header transparent />
|
|
108
|
+
<Stack.Screen.Title style={titleStyle}>Password</Stack.Screen.Title>
|
|
109
|
+
<Stack.Screen.BackButton>Profile</Stack.Screen.BackButton>
|
|
110
|
+
</Stack.Screen>
|
|
111
|
+
|
|
112
|
+
<Stack.Screen name="sessions" options={{ headerShown: true }}>
|
|
113
|
+
<Stack.Header transparent />
|
|
114
|
+
<Stack.Screen.Title style={titleStyle}>Sessions</Stack.Screen.Title>
|
|
115
|
+
<Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
|
|
116
|
+
</Stack.Screen>
|
|
117
|
+
</Stack.Protected>
|
|
118
|
+
|
|
119
|
+
{/* Own Stack.Protected so the whole authed tree above un-mounts when
|
|
120
|
+
`deletedAt` is set and re-mounts on restore. */}
|
|
121
|
+
<Stack.Protected guard={isAuthenticated && isAccountDeleted}>
|
|
122
|
+
<Stack.Screen
|
|
123
|
+
name="restore-account"
|
|
124
|
+
options={{
|
|
125
|
+
headerShown: false,
|
|
126
|
+
presentation: "modal",
|
|
127
|
+
gestureEnabled: false,
|
|
128
|
+
animation: reduceMotion ? "fade" : "default",
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
</Stack.Protected>
|
|
132
|
+
|
|
133
|
+
<Stack.Protected guard={!isAuthenticated}>
|
|
134
|
+
<Stack.Screen
|
|
135
|
+
name="auth"
|
|
136
|
+
options={{
|
|
137
|
+
headerShown: false,
|
|
138
|
+
presentation: "fullScreenModal",
|
|
139
|
+
gestureEnabled: false,
|
|
140
|
+
animation: reduceMotion ? "fade" : "fade_from_bottom",
|
|
141
|
+
animationDuration: reduceMotion ? 150 : 250,
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
</Stack.Protected>
|
|
145
|
+
</Stack>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import { Stack } from "expo-router";
|
|
2
2
|
|
|
3
3
|
import { useColors } from "@/hooks/use-theme";
|
|
4
|
-
import {
|
|
4
|
+
import { useMotionScreenOptions } from "@/hooks/use-motion-screen-options";
|
|
5
5
|
import { LoadingScreen } from "@/components/ui/loading-screen";
|
|
6
6
|
|
|
7
7
|
export { AppErrorBoundary as ErrorBoundary } from "@/components/ui/error-boundary";
|
|
8
8
|
|
|
9
9
|
export function SuspenseFallback() {
|
|
10
|
-
return <LoadingScreen />;
|
|
10
|
+
return <LoadingScreen testID="auth-loading" />;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export default function AuthLayout() {
|
|
14
14
|
const colors = useColors();
|
|
15
|
-
const
|
|
15
|
+
const motion = useMotionScreenOptions("fade_from_bottom", 250);
|
|
16
16
|
return (
|
|
17
17
|
<Stack
|
|
18
18
|
screenOptions={{
|
|
19
|
+
...motion,
|
|
19
20
|
headerShown: false,
|
|
20
21
|
contentStyle: { backgroundColor: colors.background as string },
|
|
21
|
-
animation: reduceMotion ? "fade" : "fade_from_bottom",
|
|
22
|
-
animationDuration: reduceMotion ? 150 : 250,
|
|
23
22
|
}}
|
|
24
23
|
>
|
|
25
24
|
<Stack.Screen name="sign-in" />
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
keyboardType,
|
|
16
16
|
onSubmit,
|
|
17
17
|
submitLabel,
|
|
18
|
+
textContentType,
|
|
18
19
|
textFieldStyle,
|
|
19
20
|
textInputAutocapitalization,
|
|
20
21
|
padding,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
tint,
|
|
26
27
|
} from "@expo/ui/swift-ui/modifiers";
|
|
27
28
|
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
28
|
-
import { Button as ButtonTokens } from "@/constants/layout";
|
|
29
|
+
import { Button as ButtonTokens, TouchTarget } from "@/constants/layout";
|
|
29
30
|
|
|
30
31
|
import { authClient } from "@/lib/auth-client";
|
|
31
32
|
import { assets } from "@/lib/assets";
|
|
@@ -52,7 +53,7 @@ export default function ForgotPasswordScreen() {
|
|
|
52
53
|
// mode, but a deeplinked navigation could still land here.
|
|
53
54
|
useEffect(() => {
|
|
54
55
|
if (providers !== undefined && providers.emailFeatures === false) {
|
|
55
|
-
router.replace("/sign-in");
|
|
56
|
+
router.replace("/auth/sign-in");
|
|
56
57
|
}
|
|
57
58
|
}, [providers]);
|
|
58
59
|
|
|
@@ -66,8 +67,6 @@ export default function ForgotPasswordScreen() {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
try {
|
|
69
|
-
// OTP-based reset: server emails a 6-digit code, the next screen
|
|
70
|
-
// collects code + new password and calls emailOtp.resetPassword.
|
|
71
70
|
const response = await authClient.emailOtp.sendVerificationOtp({
|
|
72
71
|
email: parsed.data.email,
|
|
73
72
|
type: "forget-password",
|
|
@@ -79,7 +78,7 @@ export default function ForgotPasswordScreen() {
|
|
|
79
78
|
}
|
|
80
79
|
haptics.success();
|
|
81
80
|
announce("Reset code sent");
|
|
82
|
-
router.push({ pathname: "/reset-password", params: { email: parsed.data.email } });
|
|
81
|
+
router.push({ pathname: "/auth/reset-password", params: { email: parsed.data.email } });
|
|
83
82
|
return {};
|
|
84
83
|
} catch {
|
|
85
84
|
haptics.error();
|
|
@@ -88,7 +87,7 @@ export default function ForgotPasswordScreen() {
|
|
|
88
87
|
}, initialState);
|
|
89
88
|
|
|
90
89
|
return (
|
|
91
|
-
<Host style={{ flex: 1, backgroundColor: colors.background }}>
|
|
90
|
+
<Host testID="forgot-password-screen" style={{ flex: 1, backgroundColor: colors.background }}>
|
|
92
91
|
<ScrollView
|
|
93
92
|
modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
|
|
94
93
|
>
|
|
@@ -107,7 +106,9 @@ export default function ForgotPasswordScreen() {
|
|
|
107
106
|
</RNHostView>
|
|
108
107
|
|
|
109
108
|
<VStack spacing={6} alignment="leading">
|
|
110
|
-
<Text modifiers={[dfont({ size: 28, weight: "bold" })]}>
|
|
109
|
+
<Text testID="forgot-password-title" modifiers={[dfont({ size: 28, weight: "bold" })]}>
|
|
110
|
+
Reset your password
|
|
111
|
+
</Text>
|
|
111
112
|
<Text
|
|
112
113
|
modifiers={[dfont({ size: 16 }), foregroundStyle(colors.mutedForeground as string)]}
|
|
113
114
|
>
|
|
@@ -115,23 +116,25 @@ export default function ForgotPasswordScreen() {
|
|
|
115
116
|
</Text>
|
|
116
117
|
</VStack>
|
|
117
118
|
|
|
118
|
-
{state.error && <ErrorText>{state.error}</ErrorText>}
|
|
119
|
+
{state.error && <ErrorText testID="forgot-password-error">{state.error}</ErrorText>}
|
|
119
120
|
|
|
120
121
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
121
122
|
<Text modifiers={[dfont({ size: 17, weight: "semibold" })]}>Email</Text>
|
|
122
123
|
<TextField
|
|
124
|
+
testID="forgot-password-email"
|
|
123
125
|
placeholder="you@example.com"
|
|
124
126
|
onTextChange={setEmail}
|
|
125
127
|
modifiers={[
|
|
126
128
|
textFieldStyle("plain"),
|
|
127
129
|
padding({ horizontal: 16 }),
|
|
128
|
-
frame({ maxWidth: Infinity,
|
|
130
|
+
frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
|
|
129
131
|
background(colors.muted as string),
|
|
130
132
|
clipShape("capsule"),
|
|
131
133
|
dfont({ size: 16 }),
|
|
132
134
|
keyboardType("email-address"),
|
|
133
135
|
autocorrectionDisabled(),
|
|
134
136
|
textInputAutocapitalization("never"),
|
|
137
|
+
textContentType("username"),
|
|
135
138
|
onSubmit(() => startTransition(() => submit())),
|
|
136
139
|
disabled(isPending),
|
|
137
140
|
submitLabel("send"),
|
|
@@ -142,6 +145,7 @@ export default function ForgotPasswordScreen() {
|
|
|
142
145
|
</VStack>
|
|
143
146
|
|
|
144
147
|
<ProminentButton
|
|
148
|
+
testID="forgot-password-submit"
|
|
145
149
|
label={isPending ? "Sending..." : "Send reset code"}
|
|
146
150
|
onPress={() => startTransition(() => submit())}
|
|
147
151
|
disabled={isPending}
|
|
@@ -149,11 +153,13 @@ export default function ForgotPasswordScreen() {
|
|
|
149
153
|
|
|
150
154
|
<VStack alignment="center" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
151
155
|
<Button
|
|
156
|
+
testID="forgot-password-back"
|
|
152
157
|
label="Back to sign in"
|
|
153
158
|
modifiers={[
|
|
154
159
|
buttonStyle("plain"),
|
|
155
160
|
foregroundStyle(colors.mutedForeground as string),
|
|
156
161
|
dfont({ size: 14, weight: "semibold" }),
|
|
162
|
+
frame({ minHeight: TouchTarget.min }),
|
|
157
163
|
]}
|
|
158
164
|
onPress={() => {
|
|
159
165
|
haptics.light();
|
|
@@ -12,13 +12,16 @@ import {
|
|
|
12
12
|
Spacer,
|
|
13
13
|
RNHostView,
|
|
14
14
|
ConfirmationDialog,
|
|
15
|
+
useNativeState,
|
|
15
16
|
} from "@expo/ui/swift-ui";
|
|
16
17
|
import {
|
|
17
18
|
foregroundStyle,
|
|
18
19
|
buttonStyle,
|
|
19
20
|
background,
|
|
20
21
|
clipShape,
|
|
22
|
+
defaultScrollAnchorForRole,
|
|
21
23
|
disabled,
|
|
24
|
+
dynamicTypeSize,
|
|
22
25
|
keyboardType,
|
|
23
26
|
textFieldStyle,
|
|
24
27
|
padding,
|
|
@@ -28,16 +31,23 @@ import {
|
|
|
28
31
|
monospacedDigit,
|
|
29
32
|
kerning,
|
|
30
33
|
submitLabel,
|
|
34
|
+
textContentType,
|
|
35
|
+
accessibilityHidden,
|
|
31
36
|
accessibilityLabel,
|
|
32
37
|
accessibilityHint,
|
|
33
38
|
tint,
|
|
34
39
|
} from "@expo/ui/swift-ui/modifiers";
|
|
35
40
|
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
41
|
+
import { useSymbolSize } from "@/lib/dynamic-symbol-size";
|
|
36
42
|
import { Button as ButtonTokens } from "@/constants/layout";
|
|
43
|
+
import { DynamicType } from "@/constants/ui";
|
|
44
|
+
|
|
45
|
+
import { runOnJS } from "react-native-worklets";
|
|
37
46
|
|
|
38
47
|
import { authClient } from "@/lib/auth-client";
|
|
39
48
|
import { assets } from "@/lib/assets";
|
|
40
49
|
import { haptics } from "@/lib/haptics";
|
|
50
|
+
import { maskOtp } from "@/lib/masks";
|
|
41
51
|
import { firstError, resetPasswordSchema } from "@/lib/schemas";
|
|
42
52
|
import { PasswordField } from "@/components/auth/password-field";
|
|
43
53
|
import { ProminentButton } from "@/components/ui/prominent-button";
|
|
@@ -53,6 +63,7 @@ const initialState: ResetState = {};
|
|
|
53
63
|
|
|
54
64
|
export default function ResetPasswordScreen() {
|
|
55
65
|
const dfont = useDynamicFont();
|
|
66
|
+
const symbolSize = useSymbolSize();
|
|
56
67
|
const colors = useColors();
|
|
57
68
|
const brandIcon = useThemedAsset(assets.brandIconLight, assets.brandIconDark);
|
|
58
69
|
const { email = "" } = useLocalSearchParams<{ email: string }>();
|
|
@@ -61,13 +72,17 @@ export default function ResetPasswordScreen() {
|
|
|
61
72
|
// in lite mode (`REQUIRE_EMAIL_VERIFICATION` unset).
|
|
62
73
|
useEffect(() => {
|
|
63
74
|
if (providers !== undefined && providers.emailFeatures === false) {
|
|
64
|
-
router.replace("/sign-in");
|
|
75
|
+
router.replace("/auth/sign-in");
|
|
65
76
|
}
|
|
66
77
|
}, [providers]);
|
|
67
78
|
|
|
79
|
+
const otpState = useNativeState("");
|
|
68
80
|
const [otp, setOtp] = useState("");
|
|
69
81
|
const [password, setPassword] = useState("");
|
|
70
82
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
83
|
+
// Hidden carrier so iOS keychain pairs the new password with this email
|
|
84
|
+
// when Strong Password offers a suggestion and the user accepts.
|
|
85
|
+
const emailIdentityState = useNativeState(email);
|
|
71
86
|
|
|
72
87
|
const [state, submit, isPending] = useActionState<ResetState, void>(async () => {
|
|
73
88
|
haptics.light();
|
|
@@ -126,7 +141,7 @@ export default function ResetPasswordScreen() {
|
|
|
126
141
|
const inputModifiers = [
|
|
127
142
|
textFieldStyle("plain"),
|
|
128
143
|
padding({ horizontal: 16 }),
|
|
129
|
-
frame({ maxWidth: Infinity,
|
|
144
|
+
frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
|
|
130
145
|
background(colors.muted as string),
|
|
131
146
|
clipShape("capsule"),
|
|
132
147
|
dfont({ size: 16 }),
|
|
@@ -134,15 +149,29 @@ export default function ResetPasswordScreen() {
|
|
|
134
149
|
|
|
135
150
|
if (state.ok) {
|
|
136
151
|
return (
|
|
137
|
-
<Host
|
|
152
|
+
<Host
|
|
153
|
+
testID="reset-password-success-screen"
|
|
154
|
+
style={{ flex: 1, backgroundColor: colors.background }}
|
|
155
|
+
>
|
|
138
156
|
<VStack
|
|
139
157
|
spacing={16}
|
|
140
158
|
alignment="center"
|
|
141
159
|
modifiers={[padding({ horizontal: 24 }), tint(colors.primary as string)]}
|
|
142
160
|
>
|
|
143
161
|
<Spacer />
|
|
144
|
-
<Image
|
|
145
|
-
|
|
162
|
+
<Image
|
|
163
|
+
testID="reset-password-success-icon"
|
|
164
|
+
systemName="checkmark.circle.fill"
|
|
165
|
+
size={symbolSize(56)}
|
|
166
|
+
color={colors.success}
|
|
167
|
+
modifiers={[accessibilityHidden(true)]}
|
|
168
|
+
/>
|
|
169
|
+
<Text
|
|
170
|
+
testID="reset-password-success-title"
|
|
171
|
+
modifiers={[dfont({ size: 28, weight: "bold" })]}
|
|
172
|
+
>
|
|
173
|
+
Password reset!
|
|
174
|
+
</Text>
|
|
146
175
|
<Text
|
|
147
176
|
modifiers={[
|
|
148
177
|
dfont({ size: 15 }),
|
|
@@ -153,10 +182,11 @@ export default function ResetPasswordScreen() {
|
|
|
153
182
|
Your password has been reset. You can now sign in with your new password.
|
|
154
183
|
</Text>
|
|
155
184
|
<ProminentButton
|
|
185
|
+
testID="reset-password-success-sign-in"
|
|
156
186
|
label="Sign in"
|
|
157
187
|
onPress={() => {
|
|
158
188
|
haptics.light();
|
|
159
|
-
router.replace("/sign-in");
|
|
189
|
+
router.replace("/auth/sign-in");
|
|
160
190
|
}}
|
|
161
191
|
/>
|
|
162
192
|
<Spacer />
|
|
@@ -168,9 +198,16 @@ export default function ResetPasswordScreen() {
|
|
|
168
198
|
const labelModifiers = [dfont({ size: 17, weight: "semibold" })];
|
|
169
199
|
|
|
170
200
|
return (
|
|
171
|
-
<Host style={{ flex: 1, backgroundColor: colors.background }}>
|
|
201
|
+
<Host testID="reset-password-screen" style={{ flex: 1, backgroundColor: colors.background }}>
|
|
172
202
|
<ScrollView
|
|
173
|
-
modifiers={[
|
|
203
|
+
modifiers={[
|
|
204
|
+
scrollDismissesKeyboard("interactively"),
|
|
205
|
+
tint(colors.primary as string),
|
|
206
|
+
// An invalid-code error appears between the account row and the OTP
|
|
207
|
+
// field, pushing the password fields down. Pin the visible center so
|
|
208
|
+
// the user stays on the field they were filling. No-op below iOS 18.
|
|
209
|
+
defaultScrollAnchorForRole("center", "sizeChanges"),
|
|
210
|
+
]}
|
|
174
211
|
>
|
|
175
212
|
<VStack
|
|
176
213
|
spacing={20}
|
|
@@ -187,7 +224,9 @@ export default function ResetPasswordScreen() {
|
|
|
187
224
|
</RNHostView>
|
|
188
225
|
|
|
189
226
|
<VStack spacing={6} alignment="leading">
|
|
190
|
-
<Text modifiers={[dfont({ size: 28, weight: "bold" })]}>
|
|
227
|
+
<Text testID="reset-password-title" modifiers={[dfont({ size: 28, weight: "bold" })]}>
|
|
228
|
+
Reset password
|
|
229
|
+
</Text>
|
|
191
230
|
<Text
|
|
192
231
|
modifiers={[dfont({ size: 16 }), foregroundStyle(colors.mutedForeground as string)]}
|
|
193
232
|
>
|
|
@@ -195,16 +234,32 @@ export default function ResetPasswordScreen() {
|
|
|
195
234
|
</Text>
|
|
196
235
|
</VStack>
|
|
197
236
|
|
|
237
|
+
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
238
|
+
<Text modifiers={labelModifiers}>Account</Text>
|
|
239
|
+
<TextField
|
|
240
|
+
testID="reset-password-account"
|
|
241
|
+
text={emailIdentityState}
|
|
242
|
+
modifiers={[
|
|
243
|
+
...inputModifiers,
|
|
244
|
+
foregroundStyle(colors.mutedForeground as string),
|
|
245
|
+
textContentType("username"),
|
|
246
|
+
disabled(true),
|
|
247
|
+
accessibilityLabel("Account email"),
|
|
248
|
+
]}
|
|
249
|
+
/>
|
|
250
|
+
</VStack>
|
|
251
|
+
|
|
198
252
|
{error && (
|
|
199
253
|
<VStack spacing={8} alignment="leading">
|
|
200
|
-
<ErrorText>{error}</ErrorText>
|
|
254
|
+
<ErrorText testID="reset-password-error">{error}</ErrorText>
|
|
201
255
|
{isExpiredError && (
|
|
202
256
|
<Button
|
|
257
|
+
testID="reset-password-request-code"
|
|
203
258
|
label="Request a new code"
|
|
204
259
|
modifiers={[buttonStyle("plain"), dfont({ size: 14 })]}
|
|
205
260
|
onPress={() => {
|
|
206
261
|
haptics.light();
|
|
207
|
-
router.push("/forgot-password");
|
|
262
|
+
router.push("/auth/forgot-password");
|
|
208
263
|
}}
|
|
209
264
|
/>
|
|
210
265
|
)}
|
|
@@ -214,16 +269,27 @@ export default function ResetPasswordScreen() {
|
|
|
214
269
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
215
270
|
<Text modifiers={labelModifiers}>Verification code</Text>
|
|
216
271
|
<TextField
|
|
272
|
+
testID="reset-password-code"
|
|
273
|
+
text={otpState}
|
|
217
274
|
placeholder="000000"
|
|
218
|
-
onTextChange={(text) =>
|
|
275
|
+
onTextChange={(text) => {
|
|
276
|
+
"worklet";
|
|
277
|
+
const digits = maskOtp(text);
|
|
278
|
+
otpState.value = digits;
|
|
279
|
+
runOnJS(setOtp)(digits);
|
|
280
|
+
}}
|
|
219
281
|
autoFocus
|
|
220
282
|
modifiers={[
|
|
221
283
|
...inputModifiers,
|
|
222
284
|
keyboardType("numeric"),
|
|
285
|
+
textContentType("oneTimeCode"),
|
|
223
286
|
dfont({ size: 24, design: "monospaced" }),
|
|
224
287
|
monospacedDigit(),
|
|
225
288
|
kerning(8),
|
|
226
289
|
multilineTextAlignment("center"),
|
|
290
|
+
// upstream expo/expo#46540: six monospaced glyphs in a capsule
|
|
291
|
+
// that can't wrap, cap Dynamic Type so they don't overflow.
|
|
292
|
+
dynamicTypeSize({ max: DynamicType.otp }),
|
|
227
293
|
submitLabel("next"),
|
|
228
294
|
disabled(isPending),
|
|
229
295
|
accessibilityLabel("Verification code"),
|
|
@@ -235,7 +301,9 @@ export default function ResetPasswordScreen() {
|
|
|
235
301
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
236
302
|
<Text modifiers={labelModifiers}>New password</Text>
|
|
237
303
|
<PasswordField
|
|
304
|
+
testID="reset-password-new"
|
|
238
305
|
onTextChange={setPassword}
|
|
306
|
+
contentType="newPassword"
|
|
239
307
|
disabled={isPending}
|
|
240
308
|
submitLabelType="next"
|
|
241
309
|
accessibilityLabel="New password"
|
|
@@ -251,8 +319,10 @@ export default function ResetPasswordScreen() {
|
|
|
251
319
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
252
320
|
<Text modifiers={labelModifiers}>Confirm password</Text>
|
|
253
321
|
<PasswordField
|
|
322
|
+
testID="reset-password-confirm"
|
|
254
323
|
onTextChange={setConfirmPassword}
|
|
255
324
|
onSubmit={() => startTransition(() => submit())}
|
|
325
|
+
contentType="newPassword"
|
|
256
326
|
disabled={isPending}
|
|
257
327
|
accessibilityLabel="Confirm new password"
|
|
258
328
|
accessibilityHint="Re-enter the new password to confirm"
|
|
@@ -260,6 +330,7 @@ export default function ResetPasswordScreen() {
|
|
|
260
330
|
</VStack>
|
|
261
331
|
|
|
262
332
|
<ProminentButton
|
|
333
|
+
testID="reset-password-submit"
|
|
263
334
|
label={isPending ? "Resetting..." : "Reset password"}
|
|
264
335
|
onPress={() => startTransition(() => submit())}
|
|
265
336
|
disabled={isPending}
|
|
@@ -267,6 +338,7 @@ export default function ResetPasswordScreen() {
|
|
|
267
338
|
|
|
268
339
|
<VStack alignment="center" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
269
340
|
<Button
|
|
341
|
+
testID="reset-password-back-to-sign-in"
|
|
270
342
|
label="Back to sign in"
|
|
271
343
|
modifiers={[
|
|
272
344
|
buttonStyle("plain"),
|
|
@@ -275,7 +347,7 @@ export default function ResetPasswordScreen() {
|
|
|
275
347
|
]}
|
|
276
348
|
onPress={() => {
|
|
277
349
|
haptics.light();
|
|
278
|
-
router.push("/sign-in");
|
|
350
|
+
router.push("/auth/sign-in");
|
|
279
351
|
}}
|
|
280
352
|
/>
|
|
281
353
|
</VStack>
|
|
@@ -295,15 +367,17 @@ export default function ResetPasswordScreen() {
|
|
|
295
367
|
</ConfirmationDialog.Trigger>
|
|
296
368
|
<ConfirmationDialog.Actions>
|
|
297
369
|
<Button
|
|
370
|
+
testID="reset-password-discard"
|
|
298
371
|
label="Discard"
|
|
299
372
|
role="destructive"
|
|
300
373
|
onPress={() => {
|
|
374
|
+
haptics.warning();
|
|
301
375
|
const action = pendingNavAction;
|
|
302
376
|
setPendingNavAction(null);
|
|
303
377
|
if (action) navigation.dispatch(action);
|
|
304
378
|
}}
|
|
305
379
|
/>
|
|
306
|
-
<Button label="Keep Editing" role="cancel" />
|
|
380
|
+
<Button testID="reset-password-keep-editing" label="Keep Editing" role="cancel" />
|
|
307
381
|
</ConfirmationDialog.Actions>
|
|
308
382
|
<ConfirmationDialog.Message>
|
|
309
383
|
<Text modifiers={[dfont({ size: 16 })]}>Your password entries will be lost.</Text>
|