@ramonclaudio/create-vexpo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +183 -0
  3. package/dist/templates/default/.eas/workflows/asc-events.yml +84 -0
  4. package/dist/templates/default/.eas/workflows/deploy-production.yml +129 -0
  5. package/dist/templates/default/.eas/workflows/development-builds.yml +19 -0
  6. package/dist/templates/default/.eas/workflows/e2e-tests.yml +42 -0
  7. package/dist/templates/default/.eas/workflows/pr-preview.yml +98 -0
  8. package/dist/templates/default/.eas/workflows/release.yml +44 -0
  9. package/dist/templates/default/.eas/workflows/rollback.yml +86 -0
  10. package/dist/templates/default/.eas/workflows/rollout.yml +84 -0
  11. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +42 -0
  12. package/dist/templates/default/.eas/workflows/testflight.yml +57 -0
  13. package/dist/templates/default/.github/workflows/check.yml +28 -0
  14. package/dist/templates/default/.maestro/launch.yaml +18 -0
  15. package/dist/templates/default/AGENTS.md +79 -0
  16. package/dist/templates/default/DESIGN.md +331 -0
  17. package/dist/templates/default/LICENSE +21 -0
  18. package/dist/templates/default/README.md +153 -0
  19. package/dist/templates/default/SETUP.md +618 -0
  20. package/dist/templates/default/__tests__/convex/constants.test.ts +49 -0
  21. package/dist/templates/default/__tests__/convex/validators.test.ts +23 -0
  22. package/dist/templates/default/__tests__/convex/webhook.test.ts +343 -0
  23. package/dist/templates/default/__tests__/lib/deep-link.test.ts +67 -0
  24. package/dist/templates/default/_easignore +22 -0
  25. package/dist/templates/default/_editorconfig +9 -0
  26. package/dist/templates/default/_env.example +34 -0
  27. package/dist/templates/default/_fingerprintignore +24 -0
  28. package/dist/templates/default/_gitattributes +7 -0
  29. package/dist/templates/default/_gitignore +69 -0
  30. package/dist/templates/default/_oxfmtrc.json +3 -0
  31. package/dist/templates/default/_oxlintrc.json +34 -0
  32. package/dist/templates/default/app/(app)/(tabs)/(home)/index.tsx +50 -0
  33. package/dist/templates/default/app/(app)/(tabs)/(home,search)/_layout.tsx +44 -0
  34. package/dist/templates/default/app/(app)/(tabs)/(search)/index.tsx +247 -0
  35. package/dist/templates/default/app/(app)/(tabs)/_layout.tsx +77 -0
  36. package/dist/templates/default/app/(app)/(tabs)/settings/_layout.tsx +37 -0
  37. package/dist/templates/default/app/(app)/(tabs)/settings/index.tsx +362 -0
  38. package/dist/templates/default/app/(app)/(tabs)/settings/preferences.tsx +184 -0
  39. package/dist/templates/default/app/(app)/_layout.tsx +73 -0
  40. package/dist/templates/default/app/(app)/debug.tsx +389 -0
  41. package/dist/templates/default/app/(app)/help.tsx +254 -0
  42. package/dist/templates/default/app/(app)/linked.tsx +116 -0
  43. package/dist/templates/default/app/(app)/privacy.tsx +159 -0
  44. package/dist/templates/default/app/(app)/profile.tsx +915 -0
  45. package/dist/templates/default/app/(app)/sessions.tsx +191 -0
  46. package/dist/templates/default/app/(app)/welcome.tsx +140 -0
  47. package/dist/templates/default/app/(auth)/_layout.tsx +31 -0
  48. package/dist/templates/default/app/(auth)/forgot-password.tsx +168 -0
  49. package/dist/templates/default/app/(auth)/reset-password.tsx +314 -0
  50. package/dist/templates/default/app/(auth)/sign-in.tsx +453 -0
  51. package/dist/templates/default/app/(auth)/sign-up.tsx +563 -0
  52. package/dist/templates/default/app/+native-intent.tsx +14 -0
  53. package/dist/templates/default/app/+not-found.tsx +51 -0
  54. package/dist/templates/default/app/_layout.tsx +102 -0
  55. package/dist/templates/default/app-store/screenshots/.gitkeep +0 -0
  56. package/dist/templates/default/app-store/screenshots/README.md +13 -0
  57. package/dist/templates/default/app.config.ts +201 -0
  58. package/dist/templates/default/app.json +11 -0
  59. package/dist/templates/default/assets/brand-icon-dark.png +0 -0
  60. package/dist/templates/default/assets/brand-icon-light.png +0 -0
  61. package/dist/templates/default/assets/fonts/Geist-Black.ttf +0 -0
  62. package/dist/templates/default/assets/fonts/Geist-BlackItalic.ttf +0 -0
  63. package/dist/templates/default/assets/fonts/Geist-Bold.ttf +0 -0
  64. package/dist/templates/default/assets/fonts/Geist-BoldItalic.ttf +0 -0
  65. package/dist/templates/default/assets/fonts/Geist-ExtraBold.ttf +0 -0
  66. package/dist/templates/default/assets/fonts/Geist-ExtraBoldItalic.ttf +0 -0
  67. package/dist/templates/default/assets/fonts/Geist-ExtraLight.ttf +0 -0
  68. package/dist/templates/default/assets/fonts/Geist-ExtraLightItalic.ttf +0 -0
  69. package/dist/templates/default/assets/fonts/Geist-Italic.ttf +0 -0
  70. package/dist/templates/default/assets/fonts/Geist-Light.ttf +0 -0
  71. package/dist/templates/default/assets/fonts/Geist-LightItalic.ttf +0 -0
  72. package/dist/templates/default/assets/fonts/Geist-Medium.ttf +0 -0
  73. package/dist/templates/default/assets/fonts/Geist-MediumItalic.ttf +0 -0
  74. package/dist/templates/default/assets/fonts/Geist-Regular.ttf +0 -0
  75. package/dist/templates/default/assets/fonts/Geist-SemiBold.ttf +0 -0
  76. package/dist/templates/default/assets/fonts/Geist-SemiBoldItalic.ttf +0 -0
  77. package/dist/templates/default/assets/fonts/Geist-Thin.ttf +0 -0
  78. package/dist/templates/default/assets/fonts/Geist-ThinItalic.ttf +0 -0
  79. package/dist/templates/default/assets/fonts/Geist-Variable-Italic.ttf +0 -0
  80. package/dist/templates/default/assets/fonts/Geist-Variable.ttf +0 -0
  81. package/dist/templates/default/assets/fonts/GeistMono-Bold.ttf +0 -0
  82. package/dist/templates/default/assets/fonts/GeistMono-BoldItalic.ttf +0 -0
  83. package/dist/templates/default/assets/fonts/GeistMono-Italic.ttf +0 -0
  84. package/dist/templates/default/assets/fonts/GeistMono-Medium.ttf +0 -0
  85. package/dist/templates/default/assets/fonts/GeistMono-MediumItalic.ttf +0 -0
  86. package/dist/templates/default/assets/fonts/GeistMono-Regular.ttf +0 -0
  87. package/dist/templates/default/assets/fonts/GeistPixel-Square.ttf +0 -0
  88. package/dist/templates/default/assets/icon.png +0 -0
  89. package/dist/templates/default/assets/sounds/notification.wav +0 -0
  90. package/dist/templates/default/assets/splash-image-dark.png +0 -0
  91. package/dist/templates/default/assets/splash-image-light.png +0 -0
  92. package/dist/templates/default/bun.lock +1860 -0
  93. package/dist/templates/default/components/auth/otp-verification.tsx +255 -0
  94. package/dist/templates/default/components/auth/password-field.tsx +121 -0
  95. package/dist/templates/default/components/auth/segmented-toggle.tsx +47 -0
  96. package/dist/templates/default/components/ui/convex-error.tsx +32 -0
  97. package/dist/templates/default/components/ui/error-boundary.tsx +57 -0
  98. package/dist/templates/default/components/ui/loading-screen.tsx +31 -0
  99. package/dist/templates/default/components/ui/material.tsx +94 -0
  100. package/dist/templates/default/components/ui/offline-banner.tsx +58 -0
  101. package/dist/templates/default/components/ui/prominent-button.tsx +71 -0
  102. package/dist/templates/default/components/ui/skeleton.tsx +107 -0
  103. package/dist/templates/default/components/ui/status-text.tsx +49 -0
  104. package/dist/templates/default/components/ui/update-banner.tsx +82 -0
  105. package/dist/templates/default/constants/layout.ts +102 -0
  106. package/dist/templates/default/constants/theme.ts +401 -0
  107. package/dist/templates/default/constants/ui.ts +77 -0
  108. package/dist/templates/default/convex/_generated/api.d.ts +77 -0
  109. package/dist/templates/default/convex/_generated/api.js +23 -0
  110. package/dist/templates/default/convex/_generated/dataModel.d.ts +60 -0
  111. package/dist/templates/default/convex/_generated/server.d.ts +143 -0
  112. package/dist/templates/default/convex/_generated/server.js +93 -0
  113. package/dist/templates/default/convex/admin.ts +102 -0
  114. package/dist/templates/default/convex/auth.config.ts +6 -0
  115. package/dist/templates/default/convex/auth.ts +335 -0
  116. package/dist/templates/default/convex/constants.ts +46 -0
  117. package/dist/templates/default/convex/convex.config.ts +11 -0
  118. package/dist/templates/default/convex/crons.ts +42 -0
  119. package/dist/templates/default/convex/email.ts +109 -0
  120. package/dist/templates/default/convex/env.ts +31 -0
  121. package/dist/templates/default/convex/errors.ts +33 -0
  122. package/dist/templates/default/convex/functions.ts +54 -0
  123. package/dist/templates/default/convex/http.ts +176 -0
  124. package/dist/templates/default/convex/log.ts +81 -0
  125. package/dist/templates/default/convex/pushTokens.ts +114 -0
  126. package/dist/templates/default/convex/rateLimit.ts +92 -0
  127. package/dist/templates/default/convex/schema.ts +28 -0
  128. package/dist/templates/default/convex/tsconfig.json +18 -0
  129. package/dist/templates/default/convex/users.ts +279 -0
  130. package/dist/templates/default/convex/validators.ts +74 -0
  131. package/dist/templates/default/convex/webhook.ts +193 -0
  132. package/dist/templates/default/convex.json +6 -0
  133. package/dist/templates/default/eas.json +56 -0
  134. package/dist/templates/default/fingerprint.config.js +9 -0
  135. package/dist/templates/default/hooks/use-debounce.ts +20 -0
  136. package/dist/templates/default/hooks/use-deep-link.ts +43 -0
  137. package/dist/templates/default/hooks/use-navigation-tracking.ts +15 -0
  138. package/dist/templates/default/hooks/use-network.ts +11 -0
  139. package/dist/templates/default/hooks/use-notifications.ts +107 -0
  140. package/dist/templates/default/hooks/use-onboarding.ts +15 -0
  141. package/dist/templates/default/hooks/use-reduced-motion.ts +11 -0
  142. package/dist/templates/default/hooks/use-theme.ts +53 -0
  143. package/dist/templates/default/hooks/use-updates.ts +86 -0
  144. package/dist/templates/default/lib/a11y.ts +5 -0
  145. package/dist/templates/default/lib/app.ts +14 -0
  146. package/dist/templates/default/lib/assets.ts +17 -0
  147. package/dist/templates/default/lib/auth-client.ts +21 -0
  148. package/dist/templates/default/lib/convex-auth.tsx +79 -0
  149. package/dist/templates/default/lib/deep-link.ts +71 -0
  150. package/dist/templates/default/lib/dev-menu.ts +119 -0
  151. package/dist/templates/default/lib/device.ts +40 -0
  152. package/dist/templates/default/lib/dynamic-font.ts +49 -0
  153. package/dist/templates/default/lib/env.ts +10 -0
  154. package/dist/templates/default/lib/haptics.ts +24 -0
  155. package/dist/templates/default/lib/notifications.ts +276 -0
  156. package/dist/templates/default/lib/preferences.ts +45 -0
  157. package/dist/templates/default/lib/schemas.ts +137 -0
  158. package/dist/templates/default/lib/storage.ts +47 -0
  159. package/dist/templates/default/lib/updates.ts +107 -0
  160. package/dist/templates/default/metro.config.js +14 -0
  161. package/dist/templates/default/package.json +129 -0
  162. package/dist/templates/default/patches/PR-368.patch +91 -0
  163. package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
  164. package/dist/templates/default/plugins/README.md +9 -0
  165. package/dist/templates/default/plugins/with-auto-signing.js +45 -0
  166. package/dist/templates/default/plugins/with-pod-deployment-target.js +35 -0
  167. package/dist/templates/default/scripts/README.md +36 -0
  168. package/dist/templates/default/scripts/_run.mjs +77 -0
  169. package/dist/templates/default/scripts/clean.ts +543 -0
  170. package/dist/templates/default/scripts/rotate-apple-jwt.mjs +80 -0
  171. package/dist/templates/default/store.config.json +58 -0
  172. package/dist/templates/default/tsconfig.json +13 -0
  173. package/dist/templates/default/vitest.config.ts +21 -0
  174. package/package.json +69 -0
@@ -0,0 +1,191 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ Host,
4
+ ScrollView,
5
+ Button,
6
+ Text,
7
+ VStack,
8
+ HStack,
9
+ Spacer,
10
+ ContentUnavailableView,
11
+ } from "@expo/ui/swift-ui";
12
+ import {
13
+ background,
14
+ buttonStyle,
15
+ cornerRadius,
16
+ foregroundStyle,
17
+ frame,
18
+ multilineTextAlignment,
19
+ padding,
20
+ textSelection,
21
+ tint,
22
+ } from "@expo/ui/swift-ui/modifiers";
23
+
24
+ import { SkeletonSessions } from "@/components/ui/skeleton";
25
+ import { useDynamicFont } from "@/lib/dynamic-font";
26
+
27
+ import { authClient } from "@/lib/auth-client";
28
+ import { haptics } from "@/lib/haptics";
29
+ import { announce } from "@/lib/a11y";
30
+ import { useColors } from "@/hooks/use-theme";
31
+
32
+ type SessionRow = {
33
+ id: string;
34
+ token: string;
35
+ ipAddress?: string | null;
36
+ userAgent?: string | null;
37
+ createdAt: Date;
38
+ expiresAt: Date;
39
+ };
40
+
41
+ function formatRelative(date: Date): string {
42
+ const now = Date.now();
43
+ const delta = Math.max(0, now - date.getTime());
44
+ const seconds = Math.floor(delta / 1000);
45
+ if (seconds < 60) return `${seconds}s ago`;
46
+ const minutes = Math.floor(seconds / 60);
47
+ if (minutes < 60) return `${minutes}m ago`;
48
+ const hours = Math.floor(minutes / 60);
49
+ if (hours < 24) return `${hours}h ago`;
50
+ const days = Math.floor(hours / 24);
51
+ if (days < 30) return `${days}d ago`;
52
+ const months = Math.floor(days / 30);
53
+ if (months < 12) return `${months}mo ago`;
54
+ return `${Math.floor(months / 12)}y ago`;
55
+ }
56
+
57
+ function deviceLabel(userAgent?: string | null): string {
58
+ if (!userAgent) return "Unknown device";
59
+ if (/iPhone/i.test(userAgent)) return "iPhone";
60
+ if (/iPad/i.test(userAgent)) return "iPad";
61
+ if (/Mac/i.test(userAgent)) return "Mac";
62
+ if (/Android/i.test(userAgent)) return "Android";
63
+ if (/Windows/i.test(userAgent)) return "Windows";
64
+ if (/Linux/i.test(userAgent)) return "Linux";
65
+ return userAgent.slice(0, 40);
66
+ }
67
+
68
+ export default function SessionsScreen() {
69
+ const dfont = useDynamicFont();
70
+ const colors = useColors();
71
+ const [sessions, setSessions] = useState<SessionRow[] | null>(null);
72
+ const [revoking, setRevoking] = useState<string | null>(null);
73
+
74
+ const load = async () => {
75
+ const res = await authClient.listSessions();
76
+ if (res.error) {
77
+ setSessions([]);
78
+ return;
79
+ }
80
+ const rows = (res.data ?? []).map((s) => ({
81
+ id: s.id,
82
+ token: s.token,
83
+ ipAddress: s.ipAddress ?? null,
84
+ userAgent: s.userAgent ?? null,
85
+ createdAt: new Date(s.createdAt),
86
+ expiresAt: new Date(s.expiresAt),
87
+ }));
88
+ setSessions(rows);
89
+ };
90
+
91
+ useEffect(() => {
92
+ load();
93
+ }, []);
94
+
95
+ const revoke = async (token: string) => {
96
+ haptics.medium();
97
+ setRevoking(token);
98
+ try {
99
+ await authClient.revokeSession({ token });
100
+ haptics.success();
101
+ announce("Session revoked");
102
+ await load();
103
+ } catch {
104
+ haptics.error();
105
+ } finally {
106
+ setRevoking(null);
107
+ }
108
+ };
109
+
110
+ return (
111
+ <Host style={{ flex: 1, backgroundColor: colors.background }}>
112
+ {sessions === null ? (
113
+ <SkeletonSessions />
114
+ ) : sessions.length === 0 ? (
115
+ <ContentUnavailableView
116
+ title="No active sessions"
117
+ systemImage="list.bullet.rectangle.portrait"
118
+ description="You have no other active sessions."
119
+ />
120
+ ) : (
121
+ <ScrollView modifiers={[tint(colors.primary as string)]}>
122
+ <VStack
123
+ spacing={12}
124
+ alignment="leading"
125
+ modifiers={[padding({ horizontal: 24, top: 24, bottom: 40 })]}
126
+ >
127
+ <Text
128
+ modifiers={[
129
+ dfont({ size: 13, weight: "semibold" }),
130
+ foregroundStyle(colors.mutedForeground as string),
131
+ ]}
132
+ >
133
+ ACTIVE SESSIONS
134
+ </Text>
135
+ {sessions.map((s) => (
136
+ <HStack
137
+ key={s.id}
138
+ spacing={12}
139
+ alignment="center"
140
+ modifiers={[
141
+ frame({ maxWidth: 10000 }),
142
+ background(colors.muted as string),
143
+ cornerRadius(20),
144
+ padding({ horizontal: 20, vertical: 14 }),
145
+ ]}
146
+ >
147
+ <VStack alignment="leading" spacing={2}>
148
+ <Text modifiers={[dfont({ size: 16, weight: "semibold" }), textSelection(true)]}>
149
+ {deviceLabel(s.userAgent)}
150
+ </Text>
151
+ <Text
152
+ modifiers={[
153
+ dfont({ size: 13 }),
154
+ foregroundStyle(colors.mutedForeground as string),
155
+ textSelection(true),
156
+ ]}
157
+ >
158
+ {s.ipAddress ?? "Unknown IP"} · {formatRelative(s.createdAt)}
159
+ </Text>
160
+ </VStack>
161
+ <Spacer />
162
+ <Button modifiers={[buttonStyle("plain")]} onPress={() => revoke(s.token)}>
163
+ <Text
164
+ modifiers={[
165
+ dfont({ size: 14, weight: "medium" }),
166
+ foregroundStyle(colors.destructive as string),
167
+ ]}
168
+ >
169
+ Revoke
170
+ </Text>
171
+ </Button>
172
+ </HStack>
173
+ ))}
174
+ {revoking ? (
175
+ <Text
176
+ modifiers={[
177
+ dfont({ size: 13 }),
178
+ foregroundStyle(colors.mutedForeground as string),
179
+ multilineTextAlignment("center"),
180
+ frame({ maxWidth: 10000 }),
181
+ ]}
182
+ >
183
+ Revoking session...
184
+ </Text>
185
+ ) : null}
186
+ </VStack>
187
+ </ScrollView>
188
+ )}
189
+ </Host>
190
+ );
191
+ }
@@ -0,0 +1,140 @@
1
+ import { useState } from "react";
2
+ import { Image as ExpoImage } from "expo-image";
3
+ import { router } from "expo-router";
4
+ import {
5
+ Host,
6
+ VStack,
7
+ Text,
8
+ Button,
9
+ Spacer,
10
+ Image,
11
+ ProgressView,
12
+ RNHostView,
13
+ } from "@expo/ui/swift-ui";
14
+ import {
15
+ foregroundStyle,
16
+ buttonStyle,
17
+ multilineTextAlignment,
18
+ progressViewStyle,
19
+ frame,
20
+ padding,
21
+ kerning,
22
+ tint,
23
+ accessibilityLabel,
24
+ accessibilityValue,
25
+ } from "@expo/ui/swift-ui/modifiers";
26
+ import { useDynamicFont } from "@/lib/dynamic-font";
27
+ import { Button as ButtonTokens } from "@/constants/layout";
28
+ import { ProminentButton } from "@/components/ui/prominent-button";
29
+
30
+ import { assets } from "@/lib/assets";
31
+ import { haptics } from "@/lib/haptics";
32
+ import { useColors, useThemedAsset } from "@/hooks/use-theme";
33
+ import { useOnboarding } from "@/hooks/use-onboarding";
34
+
35
+ type WelcomeStep =
36
+ | { brand: true; title: string; subtitle: string }
37
+ | { icon: "hammer.fill" | "checkmark.circle.fill"; title: string; subtitle: string };
38
+
39
+ const STEPS: readonly WelcomeStep[] = [
40
+ { brand: true, title: "Welcome", subtitle: "Your new app starts here." },
41
+ { icon: "hammer.fill", title: "Built with Expo", subtitle: "Universal, fast, native." },
42
+ {
43
+ icon: "checkmark.circle.fill",
44
+ title: "Ready to Go",
45
+ subtitle: "Start building something great.",
46
+ },
47
+ ] as const;
48
+
49
+ export default function WelcomeScreen() {
50
+ const dfont = useDynamicFont();
51
+ const colors = useColors();
52
+ const brandIcon = useThemedAsset(assets.brandIconLight, assets.brandIconDark);
53
+ const [step, setStep] = useState(0);
54
+ const { markSeen } = useOnboarding();
55
+
56
+ const handleContinue = async () => {
57
+ haptics.medium();
58
+ await markSeen();
59
+ router.replace("/");
60
+ };
61
+
62
+ const handleNext = () => {
63
+ haptics.light();
64
+ setStep((s) => Math.min(s + 1, STEPS.length - 1));
65
+ };
66
+
67
+ const current = STEPS[step];
68
+ const isLast = step === STEPS.length - 1;
69
+
70
+ return (
71
+ <Host style={{ flex: 1 }}>
72
+ <VStack spacing={0} modifiers={[padding({ horizontal: 24 }), tint(colors.primary as string)]}>
73
+ <VStack spacing={12} modifiers={[padding({ top: 24 })]}>
74
+ <ProgressView
75
+ value={(step + 1) / STEPS.length}
76
+ modifiers={[
77
+ progressViewStyle("linear"),
78
+ accessibilityLabel("Onboarding progress"),
79
+ accessibilityValue(`Step ${step + 1} of ${STEPS.length}`),
80
+ ]}
81
+ />
82
+ </VStack>
83
+
84
+ <Spacer />
85
+
86
+ <VStack key={step} spacing={12} alignment="center">
87
+ {"brand" in current ? (
88
+ <RNHostView matchContents>
89
+ <ExpoImage
90
+ source={brandIcon}
91
+ style={{ width: 96, height: 96 }}
92
+ contentFit="contain"
93
+ accessibilityLabel="App icon"
94
+ />
95
+ </RNHostView>
96
+ ) : (
97
+ <Image
98
+ systemName={current.icon}
99
+ size={48}
100
+ color={colors.primary as string}
101
+ modifiers={[frame({ width: 80, height: 80 })]}
102
+ />
103
+ )}
104
+ <Text modifiers={[dfont({ size: 34, weight: "bold" }), kerning(-0.5)]}>
105
+ {current.title}
106
+ </Text>
107
+ <Text
108
+ modifiers={[
109
+ dfont({ size: 17 }),
110
+ foregroundStyle(colors.mutedForeground as string),
111
+ multilineTextAlignment("center"),
112
+ ]}
113
+ >
114
+ {current.subtitle}
115
+ </Text>
116
+ </VStack>
117
+
118
+ <Spacer />
119
+
120
+ <VStack spacing={12} modifiers={[padding({ bottom: 24 })]}>
121
+ <ProminentButton
122
+ label={isLast ? "Get Started" : "Next"}
123
+ onPress={isLast ? handleContinue : handleNext}
124
+ />
125
+ {!isLast && (
126
+ <Button
127
+ label="Skip"
128
+ modifiers={[
129
+ buttonStyle("plain"),
130
+ dfont({ size: ButtonTokens.fontSize, weight: ButtonTokens.secondaryFontWeight }),
131
+ foregroundStyle(colors.mutedForeground as string),
132
+ ]}
133
+ onPress={handleContinue}
134
+ />
135
+ )}
136
+ </VStack>
137
+ </VStack>
138
+ </Host>
139
+ );
140
+ }
@@ -0,0 +1,31 @@
1
+ import { Stack } from "expo-router";
2
+
3
+ import { useColors } from "@/hooks/use-theme";
4
+ import { useReducedMotion } from "@/hooks/use-reduced-motion";
5
+ import { LoadingScreen } from "@/components/ui/loading-screen";
6
+
7
+ export { AppErrorBoundary as ErrorBoundary } from "@/components/ui/error-boundary";
8
+
9
+ export function SuspenseFallback() {
10
+ return <LoadingScreen />;
11
+ }
12
+
13
+ export default function AuthLayout() {
14
+ const colors = useColors();
15
+ const reduceMotion = useReducedMotion();
16
+ return (
17
+ <Stack
18
+ screenOptions={{
19
+ headerShown: false,
20
+ contentStyle: { backgroundColor: colors.background as string },
21
+ animation: reduceMotion ? "fade" : "fade_from_bottom",
22
+ animationDuration: reduceMotion ? 150 : 250,
23
+ }}
24
+ >
25
+ <Stack.Screen name="sign-in" />
26
+ <Stack.Screen name="sign-up" />
27
+ <Stack.Screen name="forgot-password" />
28
+ <Stack.Screen name="reset-password" />
29
+ </Stack>
30
+ );
31
+ }
@@ -0,0 +1,168 @@
1
+ import { startTransition, useActionState, useEffect, useState } from "react";
2
+ import { useQuery } from "convex/react";
3
+ import { Image as ExpoImage } from "expo-image";
4
+ import { router } from "expo-router";
5
+
6
+ import { api } from "@/convex/_generated/api";
7
+ import { Host, ScrollView, VStack, TextField, Button, Text, RNHostView } from "@expo/ui/swift-ui";
8
+ import {
9
+ autocorrectionDisabled,
10
+ foregroundStyle,
11
+ buttonStyle,
12
+ background,
13
+ clipShape,
14
+ disabled,
15
+ keyboardType,
16
+ onSubmit,
17
+ submitLabel,
18
+ textFieldStyle,
19
+ textInputAutocapitalization,
20
+ padding,
21
+ frame,
22
+ scrollDismissesKeyboard,
23
+ accessibilityLabel,
24
+ accessibilityHint,
25
+ tint,
26
+ } from "@expo/ui/swift-ui/modifiers";
27
+ import { useDynamicFont } from "@/lib/dynamic-font";
28
+ import { Button as ButtonTokens } from "@/constants/layout";
29
+
30
+ import { authClient } from "@/lib/auth-client";
31
+ import { assets } from "@/lib/assets";
32
+ import { haptics } from "@/lib/haptics";
33
+ import { firstError, forgotPasswordSchema } from "@/lib/schemas";
34
+ import { ProminentButton } from "@/components/ui/prominent-button";
35
+ import { ErrorText } from "@/components/ui/status-text";
36
+ import { announce } from "@/lib/a11y";
37
+ import { useColors, useThemedAsset } from "@/hooks/use-theme";
38
+
39
+ type ForgotState = { error?: string };
40
+ const initialState: ForgotState = {};
41
+
42
+ export default function ForgotPasswordScreen() {
43
+ const dfont = useDynamicFont();
44
+ const colors = useColors();
45
+ const brandIcon = useThemedAsset(assets.brandIconLight, assets.brandIconDark);
46
+ const [email, setEmail] = useState("");
47
+ const providers = useQuery(api.auth.getEnabledProviders);
48
+
49
+ // Password reset requires the email-OTP flow which requires Resend. When
50
+ // `REQUIRE_EMAIL_VERIFICATION` is unset on the Convex deployment, redirect
51
+ // away. the sign-in screen also hides the "Forgot password?" link in that
52
+ // mode, but a deeplinked navigation could still land here.
53
+ useEffect(() => {
54
+ if (providers !== undefined && providers.emailFeatures === false) {
55
+ router.replace("/sign-in");
56
+ }
57
+ }, [providers]);
58
+
59
+ const [state, submit, isPending] = useActionState<ForgotState, void>(async () => {
60
+ haptics.light();
61
+
62
+ const parsed = forgotPasswordSchema.safeParse({ email });
63
+ if (!parsed.success) {
64
+ haptics.error();
65
+ return { error: firstError(parsed)! };
66
+ }
67
+
68
+ try {
69
+ // OTP-based reset: server emails a 6-digit code, the next screen
70
+ // collects code + new password and calls emailOtp.resetPassword.
71
+ const response = await authClient.emailOtp.sendVerificationOtp({
72
+ email: parsed.data.email,
73
+ type: "forget-password",
74
+ });
75
+
76
+ if (response.error) {
77
+ haptics.error();
78
+ return { error: response.error.message ?? "Failed to send reset code" };
79
+ }
80
+ haptics.success();
81
+ announce("Reset code sent");
82
+ router.push({ pathname: "/reset-password", params: { email: parsed.data.email } });
83
+ return {};
84
+ } catch {
85
+ haptics.error();
86
+ return { error: "An unexpected error occurred. Please try again." };
87
+ }
88
+ }, initialState);
89
+
90
+ return (
91
+ <Host style={{ flex: 1, backgroundColor: colors.background }}>
92
+ <ScrollView
93
+ modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
94
+ >
95
+ <VStack
96
+ spacing={20}
97
+ alignment="leading"
98
+ modifiers={[padding({ horizontal: 24, top: 60, bottom: 40 })]}
99
+ >
100
+ <RNHostView matchContents>
101
+ <ExpoImage
102
+ source={brandIcon}
103
+ style={{ width: 56, height: 56 } as never}
104
+ accessibilityLabel="App icon"
105
+ contentFit="contain"
106
+ />
107
+ </RNHostView>
108
+
109
+ <VStack spacing={6} alignment="leading">
110
+ <Text modifiers={[dfont({ size: 28, weight: "bold" })]}>Reset your password</Text>
111
+ <Text
112
+ modifiers={[dfont({ size: 16 }), foregroundStyle(colors.mutedForeground as string)]}
113
+ >
114
+ Enter your email and we&apos;ll send you a 6-digit code.
115
+ </Text>
116
+ </VStack>
117
+
118
+ {state.error && <ErrorText>{state.error}</ErrorText>}
119
+
120
+ <VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
121
+ <Text modifiers={[dfont({ size: 17, weight: "semibold" })]}>Email</Text>
122
+ <TextField
123
+ placeholder="you@example.com"
124
+ onTextChange={setEmail}
125
+ modifiers={[
126
+ textFieldStyle("plain"),
127
+ padding({ horizontal: 16 }),
128
+ frame({ maxWidth: Infinity, height: ButtonTokens.height }),
129
+ background(colors.muted as string),
130
+ clipShape("capsule"),
131
+ dfont({ size: 16 }),
132
+ keyboardType("email-address"),
133
+ autocorrectionDisabled(),
134
+ textInputAutocapitalization("never"),
135
+ onSubmit(() => startTransition(() => submit())),
136
+ disabled(isPending),
137
+ submitLabel("send"),
138
+ accessibilityLabel("Email address"),
139
+ accessibilityHint("Enter the email associated with your account"),
140
+ ]}
141
+ />
142
+ </VStack>
143
+
144
+ <ProminentButton
145
+ label={isPending ? "Sending..." : "Send reset code"}
146
+ onPress={() => startTransition(() => submit())}
147
+ disabled={isPending}
148
+ />
149
+
150
+ <VStack alignment="center" modifiers={[frame({ maxWidth: Infinity })]}>
151
+ <Button
152
+ label="Back to sign in"
153
+ modifiers={[
154
+ buttonStyle("plain"),
155
+ foregroundStyle(colors.mutedForeground as string),
156
+ dfont({ size: 14, weight: "semibold" }),
157
+ ]}
158
+ onPress={() => {
159
+ haptics.light();
160
+ router.back();
161
+ }}
162
+ />
163
+ </VStack>
164
+ </VStack>
165
+ </ScrollView>
166
+ </Host>
167
+ );
168
+ }