@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,453 @@
1
+ import { startTransition, useActionState, useEffect, useState } from "react";
2
+ import * as AppleAuthentication from "expo-apple-authentication";
3
+ import { Image as ExpoImage } from "expo-image";
4
+ import { router } from "expo-router";
5
+ import { useQuery } from "convex/react";
6
+ import { Host, ScrollView, VStack, TextField, Button, Text, RNHostView } from "@expo/ui/swift-ui";
7
+ import {
8
+ autocorrectionDisabled,
9
+ foregroundStyle,
10
+ buttonStyle,
11
+ background,
12
+ clipShape,
13
+ disabled,
14
+ keyboardType,
15
+ onSubmit as onSubmitModifier,
16
+ submitLabel,
17
+ textFieldStyle,
18
+ textInputAutocapitalization,
19
+ padding,
20
+ frame,
21
+ scrollDismissesKeyboard,
22
+ accessibilityLabel,
23
+ accessibilityHint,
24
+ tint,
25
+ } from "@expo/ui/swift-ui/modifiers";
26
+ import { useDynamicFont } from "@/lib/dynamic-font";
27
+ import { Button as ButtonTokens } from "@/constants/layout";
28
+
29
+ import { api } from "@/convex/_generated/api";
30
+ import { authClient } from "@/lib/auth-client";
31
+ import { assets } from "@/lib/assets";
32
+ import { haptics } from "@/lib/haptics";
33
+ import {
34
+ firstError,
35
+ forgotPasswordSchema,
36
+ signInEmailSchema,
37
+ signInUsernameSchema,
38
+ } from "@/lib/schemas";
39
+ import { OtpVerification } from "@/components/auth/otp-verification";
40
+ import { PasswordField } from "@/components/auth/password-field";
41
+ import { SegmentedToggle } from "@/components/auth/segmented-toggle";
42
+ import { ProminentButton } from "@/components/ui/prominent-button";
43
+ import { ErrorText } from "@/components/ui/status-text";
44
+ import { announce } from "@/lib/a11y";
45
+ import { useColorScheme, useColors, useThemedAsset } from "@/hooks/use-theme";
46
+
47
+ type SignInState = { error?: string; ok?: boolean };
48
+ const initialState: SignInState = {};
49
+
50
+ type SignInMethod = "email" | "username" | "otp";
51
+
52
+ export default function SignInScreen() {
53
+ const dfont = useDynamicFont();
54
+ const colorScheme = useColorScheme();
55
+ const colors = useColors();
56
+ const brandIcon = useThemedAsset(assets.brandIconLight, assets.brandIconDark);
57
+
58
+ const [signInMethod, setSignInMethod] = useState<SignInMethod>("email");
59
+ const [emailValue, setEmailValue] = useState("");
60
+ const [usernameValue, setUsernameValue] = useState("");
61
+ const [password, setPassword] = useState("");
62
+ const [otpEmail, setOtpEmail] = useState("");
63
+ const [showOtpVerification, setShowOtpVerification] = useState(false);
64
+ const [appleAvailable, setAppleAvailable] = useState(false);
65
+ const providers = useQuery(api.auth.getEnabledProviders);
66
+ const showApple = appleAvailable && providers?.apple === true;
67
+ // Email features (OTP sign-in, password reset) require the Convex env to
68
+ // have `REQUIRE_EMAIL_VERIFICATION=true` (set by `bunx vexpo full`).
69
+ // Until then, hide them so users don't hit a code-was-logged-to-console
70
+ // dead end. Email + password sign-up/sign-in remains available.
71
+ const emailFeatures = providers?.emailFeatures === true;
72
+ const isOtp = signInMethod === "otp";
73
+
74
+ useEffect(() => {
75
+ AppleAuthentication.isAvailableAsync().then(setAppleAvailable);
76
+ }, []);
77
+
78
+ const [emailState, signInWithEmail, isEmailPending] = useActionState<SignInState, void>(
79
+ async () => {
80
+ haptics.light();
81
+ const parsed = signInEmailSchema.safeParse({ email: emailValue, password });
82
+ if (!parsed.success) {
83
+ haptics.error();
84
+ return { error: firstError(parsed)! };
85
+ }
86
+ try {
87
+ const response = await authClient.signIn.email({
88
+ email: parsed.data.email,
89
+ password: parsed.data.password,
90
+ });
91
+ if (response.error) {
92
+ haptics.error();
93
+ return { error: response.error.message ?? "Invalid email or password" };
94
+ }
95
+ haptics.success();
96
+ announce("Signed in");
97
+ return { ok: true };
98
+ } catch (e) {
99
+ haptics.error();
100
+ return {
101
+ error: e instanceof Error ? e.message : "An unexpected error occurred. Please try again.",
102
+ };
103
+ }
104
+ },
105
+ initialState,
106
+ );
107
+
108
+ const [usernameState, signInWithUsername, isUsernamePending] = useActionState<SignInState, void>(
109
+ async () => {
110
+ haptics.light();
111
+ const parsed = signInUsernameSchema.safeParse({ username: usernameValue, password });
112
+ if (!parsed.success) {
113
+ haptics.error();
114
+ return { error: firstError(parsed)! };
115
+ }
116
+ try {
117
+ const response = await authClient.signIn.username({
118
+ username: parsed.data.username,
119
+ password: parsed.data.password,
120
+ });
121
+ if (response.error) {
122
+ haptics.error();
123
+ return { error: response.error.message ?? "Invalid username or password" };
124
+ }
125
+ haptics.success();
126
+ announce("Signed in");
127
+ return { ok: true };
128
+ } catch (e) {
129
+ haptics.error();
130
+ return {
131
+ error: e instanceof Error ? e.message : "An unexpected error occurred. Please try again.",
132
+ };
133
+ }
134
+ },
135
+ initialState,
136
+ );
137
+
138
+ const [otpRequestState, sendSignInOtp, isSendingOtp] = useActionState<SignInState, void>(
139
+ async () => {
140
+ haptics.light();
141
+ const parsed = forgotPasswordSchema.safeParse({ email: otpEmail });
142
+ if (!parsed.success) {
143
+ haptics.error();
144
+ return { error: firstError(parsed)! };
145
+ }
146
+ try {
147
+ const response = await authClient.emailOtp.sendVerificationOtp({
148
+ email: parsed.data.email,
149
+ type: "sign-in",
150
+ });
151
+ if (response.error) {
152
+ haptics.error();
153
+ return { error: response.error.message ?? "Failed to send sign-in code" };
154
+ }
155
+ haptics.success();
156
+ announce("Sign-in code sent");
157
+ setShowOtpVerification(true);
158
+ return { ok: true };
159
+ } catch (e) {
160
+ haptics.error();
161
+ return {
162
+ error: e instanceof Error ? e.message : "An unexpected error occurred. Please try again.",
163
+ };
164
+ }
165
+ },
166
+ initialState,
167
+ );
168
+
169
+ const [appleState, signInWithApple, isApplePending] = useActionState<SignInState, void>(
170
+ async () => {
171
+ haptics.light();
172
+ try {
173
+ const credential = await AppleAuthentication.signInAsync({
174
+ requestedScopes: [
175
+ AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
176
+ AppleAuthentication.AppleAuthenticationScope.EMAIL,
177
+ ],
178
+ });
179
+
180
+ if (!credential.identityToken) {
181
+ haptics.error();
182
+ return { error: "Apple did not return an identity token" };
183
+ }
184
+
185
+ const response = await authClient.signIn.social({
186
+ provider: "apple",
187
+ idToken: { token: credential.identityToken },
188
+ });
189
+
190
+ if (response.error) {
191
+ haptics.error();
192
+ return { error: response.error.message ?? "Apple sign-in failed" };
193
+ }
194
+ haptics.success();
195
+ announce("Signed in with Apple");
196
+ return { ok: true };
197
+ } catch (e) {
198
+ if (e instanceof Error && "code" in e && e.code === "ERR_REQUEST_CANCELED") return {};
199
+ haptics.error();
200
+ return { error: e instanceof Error ? e.message : "Apple sign-in failed" };
201
+ }
202
+ },
203
+ initialState,
204
+ );
205
+
206
+ const error =
207
+ emailState.error ?? usernameState.error ?? otpRequestState.error ?? appleState.error;
208
+ const isLoading = isEmailPending || isUsernamePending || isSendingOtp || isApplePending;
209
+
210
+ if (showOtpVerification) {
211
+ return (
212
+ <OtpVerification
213
+ email={otpEmail}
214
+ flow="sign-in"
215
+ onBack={() => setShowOtpVerification(false)}
216
+ />
217
+ );
218
+ }
219
+
220
+ const onSubmit = () => {
221
+ if (signInMethod === "email") return signInWithEmail();
222
+ if (signInMethod === "username") return signInWithUsername();
223
+ return sendSignInOtp();
224
+ };
225
+
226
+ const primaryLabel = (() => {
227
+ if (signInMethod === "otp") return isSendingOtp ? "Sending..." : "Send code";
228
+ if (signInMethod === "email") return isEmailPending ? "Signing in..." : "Sign in";
229
+ return isUsernamePending ? "Signing in..." : "Sign in";
230
+ })();
231
+
232
+ const labelModifiers = [dfont({ size: 17, weight: "semibold" })];
233
+ const helperModifiers = [dfont({ size: 13 }), foregroundStyle(colors.mutedForeground as string)];
234
+ const inputModifiers = [
235
+ textFieldStyle("plain"),
236
+ padding({ horizontal: 16 }),
237
+ frame({ maxWidth: Infinity, height: ButtonTokens.height }),
238
+ background(colors.muted as string),
239
+ clipShape("capsule"),
240
+ dfont({ size: 16 }),
241
+ ];
242
+
243
+ return (
244
+ <Host style={{ flex: 1, backgroundColor: colors.background }}>
245
+ <ScrollView
246
+ modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
247
+ >
248
+ <VStack
249
+ spacing={20}
250
+ alignment="leading"
251
+ modifiers={[padding({ horizontal: 24, top: 60, bottom: 40 })]}
252
+ >
253
+ <RNHostView matchContents>
254
+ <ExpoImage
255
+ source={brandIcon}
256
+ style={{ width: 56, height: 56 } as never}
257
+ accessibilityLabel="App icon"
258
+ contentFit="contain"
259
+ />
260
+ </RNHostView>
261
+
262
+ <VStack spacing={6} alignment="leading">
263
+ <Text modifiers={[dfont({ size: 28, weight: "bold" })]}>Sign in</Text>
264
+ <Text
265
+ modifiers={[dfont({ size: 16 }), foregroundStyle(colors.mutedForeground as string)]}
266
+ >
267
+ {isOtp
268
+ ? "We'll email you a 6-digit code. No password needed."
269
+ : "Enter your credentials to access your account."}
270
+ </Text>
271
+ </VStack>
272
+
273
+ <SegmentedToggle
274
+ value="sign-in"
275
+ options={[
276
+ { value: "sign-in", label: "Sign in" },
277
+ { value: "sign-up", label: "Sign up" },
278
+ ]}
279
+ onChange={(v) => {
280
+ if (v === "sign-up") router.replace("/sign-up");
281
+ }}
282
+ />
283
+
284
+ <SegmentedToggle
285
+ value={signInMethod}
286
+ options={
287
+ emailFeatures
288
+ ? [
289
+ { value: "email", label: "Email" },
290
+ { value: "username", label: "Username" },
291
+ { value: "otp", label: "Email OTP" },
292
+ ]
293
+ : [
294
+ { value: "email", label: "Email" },
295
+ { value: "username", label: "Username" },
296
+ ]
297
+ }
298
+ onChange={(value) => setSignInMethod(value as SignInMethod)}
299
+ />
300
+
301
+ {error && <ErrorText>{error}</ErrorText>}
302
+
303
+ {signInMethod === "email" && (
304
+ <>
305
+ <VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
306
+ <Text modifiers={labelModifiers}>Email</Text>
307
+ <TextField
308
+ placeholder="you@example.com"
309
+ onTextChange={setEmailValue}
310
+ modifiers={[
311
+ ...inputModifiers,
312
+ keyboardType("email-address"),
313
+ autocorrectionDisabled(),
314
+ textInputAutocapitalization("never"),
315
+ disabled(isLoading),
316
+ submitLabel("next"),
317
+ accessibilityLabel("Email address"),
318
+ accessibilityHint("Enter the email for your account"),
319
+ ]}
320
+ />
321
+ </VStack>
322
+ <VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
323
+ <Text modifiers={labelModifiers}>Password</Text>
324
+ <PasswordField
325
+ onTextChange={setPassword}
326
+ onSubmit={() => startTransition(() => signInWithEmail())}
327
+ disabled={isLoading}
328
+ accessibilityLabel="Password"
329
+ accessibilityHint="Enter your account password"
330
+ />
331
+ </VStack>
332
+ {emailFeatures && (
333
+ <Button
334
+ label="Forgot password?"
335
+ modifiers={[
336
+ buttonStyle("plain"),
337
+ foregroundStyle(colors.mutedForeground as string),
338
+ dfont({ size: 13 }),
339
+ ]}
340
+ onPress={() => {
341
+ haptics.light();
342
+ router.push("/forgot-password");
343
+ }}
344
+ />
345
+ )}
346
+ </>
347
+ )}
348
+
349
+ {signInMethod === "username" && (
350
+ <>
351
+ <VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
352
+ <Text modifiers={labelModifiers}>Username</Text>
353
+ <TextField
354
+ placeholder="johndoe"
355
+ onTextChange={setUsernameValue}
356
+ modifiers={[
357
+ ...inputModifiers,
358
+ keyboardType("ascii-capable"),
359
+ autocorrectionDisabled(),
360
+ textInputAutocapitalization("never"),
361
+ disabled(isLoading),
362
+ submitLabel("next"),
363
+ accessibilityLabel("Username"),
364
+ accessibilityHint("Enter the username for your account"),
365
+ ]}
366
+ />
367
+ </VStack>
368
+ <VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
369
+ <Text modifiers={labelModifiers}>Password</Text>
370
+ <PasswordField
371
+ onTextChange={setPassword}
372
+ onSubmit={() => startTransition(() => signInWithUsername())}
373
+ disabled={isLoading}
374
+ accessibilityLabel="Password"
375
+ accessibilityHint="Enter your account password"
376
+ />
377
+ </VStack>
378
+ {emailFeatures && (
379
+ <Button
380
+ label="Forgot password?"
381
+ modifiers={[
382
+ buttonStyle("plain"),
383
+ foregroundStyle(colors.mutedForeground as string),
384
+ dfont({ size: 13 }),
385
+ ]}
386
+ onPress={() => {
387
+ haptics.light();
388
+ router.push("/forgot-password");
389
+ }}
390
+ />
391
+ )}
392
+ </>
393
+ )}
394
+
395
+ {signInMethod === "otp" && (
396
+ <VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
397
+ <Text modifiers={labelModifiers}>Email</Text>
398
+ <TextField
399
+ placeholder="you@example.com"
400
+ onTextChange={setOtpEmail}
401
+ modifiers={[
402
+ ...inputModifiers,
403
+ keyboardType("email-address"),
404
+ autocorrectionDisabled(),
405
+ textInputAutocapitalization("never"),
406
+ onSubmitModifier(() => startTransition(() => sendSignInOtp())),
407
+ disabled(isLoading),
408
+ submitLabel("send"),
409
+ accessibilityLabel("Email address"),
410
+ accessibilityHint("Enter the email address for your account"),
411
+ ]}
412
+ />
413
+ <Text modifiers={helperModifiers}>
414
+ We&apos;ll email you a 6-digit code. No password needed.
415
+ </Text>
416
+ </VStack>
417
+ )}
418
+
419
+ <ProminentButton
420
+ label={primaryLabel}
421
+ onPress={() => startTransition(onSubmit)}
422
+ disabled={isLoading}
423
+ />
424
+
425
+ {!isOtp && showApple && (
426
+ <VStack
427
+ alignment="center"
428
+ modifiers={[frame({ maxWidth: Infinity, height: ButtonTokens.height })]}
429
+ >
430
+ <RNHostView>
431
+ <AppleAuthentication.AppleAuthenticationButton
432
+ buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
433
+ buttonStyle={
434
+ colorScheme === "dark"
435
+ ? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
436
+ : AppleAuthentication.AppleAuthenticationButtonStyle.BLACK
437
+ }
438
+ cornerRadius={ButtonTokens.cornerRadius}
439
+ style={{
440
+ width: "100%",
441
+ height: "100%",
442
+ opacity: isLoading ? 0.5 : 1,
443
+ }}
444
+ onPress={() => startTransition(() => signInWithApple())}
445
+ />
446
+ </RNHostView>
447
+ </VStack>
448
+ )}
449
+ </VStack>
450
+ </ScrollView>
451
+ </Host>
452
+ );
453
+ }