@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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { startTransition, useActionState, useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import * as AppleAuthentication from "expo-apple-authentication";
|
|
3
3
|
import { Image as ExpoImage } from "expo-image";
|
|
4
|
-
import * as ImagePicker from "expo-image-picker";
|
|
5
4
|
import { router, useNavigation } from "expo-router";
|
|
6
5
|
import { useQuery } from "convex/react";
|
|
7
6
|
import {
|
|
@@ -16,54 +15,64 @@ import {
|
|
|
16
15
|
Spacer,
|
|
17
16
|
RNHostView,
|
|
18
17
|
ConfirmationDialog,
|
|
18
|
+
useNativeState,
|
|
19
19
|
} from "@expo/ui/swift-ui";
|
|
20
20
|
import {
|
|
21
21
|
autocorrectionDisabled,
|
|
22
22
|
foregroundStyle,
|
|
23
|
+
defaultScrollAnchorForRole,
|
|
23
24
|
disabled,
|
|
24
25
|
keyboardType,
|
|
25
26
|
submitLabel,
|
|
27
|
+
textContentType,
|
|
26
28
|
textFieldStyle,
|
|
27
29
|
textInputAutocapitalization,
|
|
28
30
|
padding,
|
|
29
31
|
frame,
|
|
30
32
|
scrollDismissesKeyboard,
|
|
33
|
+
accessibilityHidden,
|
|
31
34
|
accessibilityLabel,
|
|
32
35
|
accessibilityHint,
|
|
33
|
-
onTapGesture,
|
|
34
36
|
tint,
|
|
35
37
|
background,
|
|
36
|
-
border,
|
|
37
38
|
clipShape,
|
|
39
|
+
id,
|
|
40
|
+
scrollPosition,
|
|
41
|
+
scrollTargetLayout,
|
|
38
42
|
} from "@expo/ui/swift-ui/modifiers";
|
|
39
43
|
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
44
|
+
import { useSymbolSize } from "@/lib/dynamic-symbol-size";
|
|
40
45
|
import { Button as ButtonTokens } from "@/constants/layout";
|
|
41
46
|
|
|
42
47
|
import { api } from "@/convex/_generated/api";
|
|
43
48
|
import { isReservedUsername, isValidUsernameFormat } from "@/convex/constants";
|
|
49
|
+
import { runOnJS } from "react-native-worklets";
|
|
50
|
+
|
|
44
51
|
import { authClient } from "@/lib/auth-client";
|
|
45
52
|
import { assets } from "@/lib/assets";
|
|
46
53
|
import { haptics } from "@/lib/haptics";
|
|
47
|
-
import {
|
|
54
|
+
import { maskUsername } from "@/lib/masks";
|
|
55
|
+
import { setNativeValue } from "@/lib/native-state";
|
|
56
|
+
import { OtpVerification } from "@/components/auth/otp-verification";
|
|
48
57
|
import { PasswordField } from "@/components/auth/password-field";
|
|
49
58
|
import { SegmentedToggle } from "@/components/auth/segmented-toggle";
|
|
50
59
|
import { ProminentButton } from "@/components/ui/prominent-button";
|
|
51
|
-
import { firstError, signUpSchema } from "@/lib/schemas";
|
|
60
|
+
import { firstError, firstErrorField, signUpSchema } from "@/lib/schemas";
|
|
52
61
|
import { ErrorText } from "@/components/ui/status-text";
|
|
53
62
|
import { announce } from "@/lib/a11y";
|
|
54
|
-
import {
|
|
63
|
+
import { useColors, useThemedAsset } from "@/hooks/use-theme";
|
|
64
|
+
import { AppleButton } from "@/components/auth/apple-button";
|
|
55
65
|
|
|
56
66
|
type SignUpState = { error?: string; verify?: boolean };
|
|
57
67
|
const initialState: SignUpState = {};
|
|
58
68
|
|
|
59
|
-
const AVATAR_SIZE = 56;
|
|
60
|
-
|
|
61
69
|
export default function SignUpScreen() {
|
|
62
70
|
const dfont = useDynamicFont();
|
|
63
|
-
const
|
|
71
|
+
const symbolSize = useSymbolSize();
|
|
64
72
|
const colors = useColors();
|
|
65
73
|
const brandIcon = useThemedAsset(assets.brandIconLight, assets.brandIconDark);
|
|
66
74
|
const [name, setName] = useState("");
|
|
75
|
+
const usernameState = useNativeState("");
|
|
67
76
|
const [username, setUsername] = useState("");
|
|
68
77
|
const [email, setEmail] = useState("");
|
|
69
78
|
const [password, setPassword] = useState("");
|
|
@@ -73,19 +82,14 @@ export default function SignUpScreen() {
|
|
|
73
82
|
const showApple = appleAvailable && providers?.apple === true;
|
|
74
83
|
// When `emailFeatures` is false (minimal-tier setup, no Resend), the
|
|
75
84
|
// server auto-verifies on sign-up and the user is signed in immediately
|
|
76
|
-
|
|
85
|
+
// no OTP step. When true (testflight tier+), the OTP verification
|
|
77
86
|
// screen renders after sign-up.
|
|
78
87
|
const emailFeatures = providers?.emailFeatures === true;
|
|
79
88
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
const [avatarPicker, setAvatarPicker] = useState(false);
|
|
84
|
-
const [avatarError, setAvatarError] = useState<string | null>(null);
|
|
89
|
+
// Bound to ScrollView via `scrollPosition`. Writing a field id scrolls the
|
|
90
|
+
// form so that field aligns with the top of the viewport.
|
|
91
|
+
const activeField = useNativeState<string | null>(null);
|
|
85
92
|
|
|
86
|
-
// Live username availability via the better-auth `username` plugin. Status
|
|
87
|
-
// is null while idle, true when the server says the handle is free, false
|
|
88
|
-
// when reserved or already taken. Format errors are left to the schema.
|
|
89
93
|
const [usernameAvailable, setUsernameAvailable] = useState<boolean | null>(null);
|
|
90
94
|
const [isCheckingUsername, setIsCheckingUsername] = useState(false);
|
|
91
95
|
const usernameCheckRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -104,13 +108,13 @@ export default function SignUpScreen() {
|
|
|
104
108
|
|
|
105
109
|
const handleUsernameChange = useCallback(
|
|
106
110
|
(value: string) => {
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
setUsername(
|
|
111
|
+
// `value` arrives already masked (lowercase, `[a-z0-9._]`) from the
|
|
112
|
+
// field's worklet, so this only mirrors it and drives the availability
|
|
113
|
+
// check off the JS thread.
|
|
114
|
+
setUsername(value);
|
|
111
115
|
setUsernameAvailable(null);
|
|
112
116
|
if (usernameCheckRef.current) clearTimeout(usernameCheckRef.current);
|
|
113
|
-
const trimmed =
|
|
117
|
+
const trimmed = value.trim();
|
|
114
118
|
if (!trimmed || !isValidUsernameFormat(trimmed)) return;
|
|
115
119
|
if (isReservedUsername(trimmed)) {
|
|
116
120
|
setUsernameAvailable(false);
|
|
@@ -130,45 +134,6 @@ export default function SignUpScreen() {
|
|
|
130
134
|
[],
|
|
131
135
|
);
|
|
132
136
|
|
|
133
|
-
const pickAvatar = useCallback(async (source: "library" | "camera") => {
|
|
134
|
-
setAvatarPicker(false);
|
|
135
|
-
// Wait for the action sheet to finish dismissing before opening the
|
|
136
|
-
// picker. iOS refuses to present a second view controller while one is
|
|
137
|
-
// still animating away.
|
|
138
|
-
await new Promise((r) => setTimeout(r, 350));
|
|
139
|
-
const perm =
|
|
140
|
-
source === "camera"
|
|
141
|
-
? await ImagePicker.requestCameraPermissionsAsync()
|
|
142
|
-
: await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
143
|
-
if (!perm.granted) {
|
|
144
|
-
setAvatarError(source === "camera" ? "Camera access denied" : "Photos access denied");
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
haptics.light();
|
|
148
|
-
const options: ImagePicker.ImagePickerOptions = {
|
|
149
|
-
mediaTypes: ["images"],
|
|
150
|
-
allowsEditing: true,
|
|
151
|
-
aspect: [1, 1],
|
|
152
|
-
quality: 0.8,
|
|
153
|
-
};
|
|
154
|
-
const result =
|
|
155
|
-
source === "camera"
|
|
156
|
-
? await ImagePicker.launchCameraAsync(options)
|
|
157
|
-
: await ImagePicker.launchImageLibraryAsync(options);
|
|
158
|
-
if (result.canceled) return;
|
|
159
|
-
const asset = result.assets[0];
|
|
160
|
-
if (!asset) return;
|
|
161
|
-
setAvatarError(null);
|
|
162
|
-
setPendingAvatar({ uri: asset.uri, mimeType: asset.mimeType ?? "image/jpeg" });
|
|
163
|
-
}, []);
|
|
164
|
-
|
|
165
|
-
const removeAvatar = useCallback(() => {
|
|
166
|
-
setAvatarPicker(false);
|
|
167
|
-
haptics.medium();
|
|
168
|
-
setPendingAvatar(null);
|
|
169
|
-
setAvatarError(null);
|
|
170
|
-
}, []);
|
|
171
|
-
|
|
172
137
|
useEffect(() => {
|
|
173
138
|
AppleAuthentication.isAvailableAsync().then(setAppleAvailable);
|
|
174
139
|
}, []);
|
|
@@ -176,8 +141,6 @@ export default function SignUpScreen() {
|
|
|
176
141
|
const navigation = useNavigation();
|
|
177
142
|
const hasInput =
|
|
178
143
|
name.length > 0 || username.length > 0 || email.length > 0 || password.length > 0;
|
|
179
|
-
// Hold the pending navigation action while we show the discard-changes
|
|
180
|
-
// ConfirmationDialog. Cleared on Discard (after dispatch) or Cancel.
|
|
181
144
|
const [pendingNavAction, setPendingNavAction] = useState<
|
|
182
145
|
Parameters<typeof navigation.dispatch>[0] | null
|
|
183
146
|
>(null);
|
|
@@ -195,6 +158,8 @@ export default function SignUpScreen() {
|
|
|
195
158
|
const parsed = signUpSchema.safeParse({ name, username, email, password });
|
|
196
159
|
if (!parsed.success) {
|
|
197
160
|
haptics.error();
|
|
161
|
+
const field = firstErrorField(parsed);
|
|
162
|
+
if (field) setNativeValue(activeField, `field-${field}`);
|
|
198
163
|
return { error: firstError(parsed)! };
|
|
199
164
|
}
|
|
200
165
|
|
|
@@ -268,29 +233,41 @@ export default function SignUpScreen() {
|
|
|
268
233
|
);
|
|
269
234
|
|
|
270
235
|
const isLoading = isPending || isApplePending;
|
|
271
|
-
const error = state.error ?? appleState.error
|
|
272
|
-
|
|
236
|
+
const error = state.error ?? appleState.error;
|
|
237
|
+
// HIG: pair color with a non-color signal. The status row carries text +
|
|
238
|
+
// color + an SF Symbol so a colorblind user gets the same answer.
|
|
239
|
+
const usernameStatus: {
|
|
240
|
+
text: string;
|
|
241
|
+
color: string;
|
|
242
|
+
icon: "ellipsis.circle" | "checkmark.circle.fill" | "exclamationmark.circle.fill";
|
|
243
|
+
} | null = (() => {
|
|
273
244
|
if (!username || !isValidUsernameFormat(username.trim().toLowerCase())) return null;
|
|
274
245
|
if (isCheckingUsername) {
|
|
275
|
-
return {
|
|
246
|
+
return {
|
|
247
|
+
text: "Checking availability...",
|
|
248
|
+
color: colors.mutedForeground as string,
|
|
249
|
+
icon: "ellipsis.circle",
|
|
250
|
+
};
|
|
276
251
|
}
|
|
277
252
|
if (usernameAvailable === true) {
|
|
278
|
-
return {
|
|
253
|
+
return {
|
|
254
|
+
text: "Username is available",
|
|
255
|
+
color: colors.success as string,
|
|
256
|
+
icon: "checkmark.circle.fill",
|
|
257
|
+
};
|
|
279
258
|
}
|
|
280
259
|
if (usernameAvailable === false) {
|
|
281
|
-
return {
|
|
260
|
+
return {
|
|
261
|
+
text: "This username is not available",
|
|
262
|
+
color: colors.destructive as string,
|
|
263
|
+
icon: "exclamationmark.circle.fill",
|
|
264
|
+
};
|
|
282
265
|
}
|
|
283
266
|
return null;
|
|
284
267
|
})();
|
|
285
268
|
|
|
286
269
|
if (showVerification) {
|
|
287
|
-
return (
|
|
288
|
-
<OtpVerification
|
|
289
|
-
email={email}
|
|
290
|
-
pendingAvatar={pendingAvatar}
|
|
291
|
-
onBack={() => setShowVerification(false)}
|
|
292
|
-
/>
|
|
293
|
-
);
|
|
270
|
+
return <OtpVerification email={email} onBack={() => setShowVerification(false)} />;
|
|
294
271
|
}
|
|
295
272
|
|
|
296
273
|
const labelModifiers = [dfont({ size: 17, weight: "semibold" })];
|
|
@@ -298,21 +275,29 @@ export default function SignUpScreen() {
|
|
|
298
275
|
const inputModifiers = [
|
|
299
276
|
textFieldStyle("plain"),
|
|
300
277
|
padding({ horizontal: 16 }),
|
|
301
|
-
frame({ maxWidth: Infinity,
|
|
278
|
+
frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
|
|
302
279
|
background(colors.muted as string),
|
|
303
280
|
clipShape("capsule"),
|
|
304
281
|
dfont({ size: 16 }),
|
|
305
282
|
];
|
|
306
283
|
|
|
307
284
|
return (
|
|
308
|
-
<Host style={{ flex: 1, backgroundColor: colors.background }}>
|
|
285
|
+
<Host testID="sign-up-screen" style={{ flex: 1, backgroundColor: colors.background }}>
|
|
309
286
|
<ScrollView
|
|
310
|
-
modifiers={[
|
|
287
|
+
modifiers={[
|
|
288
|
+
scrollDismissesKeyboard("interactively"),
|
|
289
|
+
tint(colors.primary as string),
|
|
290
|
+
scrollPosition(activeField, { anchor: "top" }),
|
|
291
|
+
// Anchor the visible center on size changes so a username-availability
|
|
292
|
+
// line appearing or a dynamic-type bump doesn't shift the field the
|
|
293
|
+
// user is reading. No-op below iOS 18.
|
|
294
|
+
defaultScrollAnchorForRole("center", "sizeChanges"),
|
|
295
|
+
]}
|
|
311
296
|
>
|
|
312
297
|
<VStack
|
|
313
298
|
spacing={20}
|
|
314
299
|
alignment="leading"
|
|
315
|
-
modifiers={[padding({ horizontal: 24, top: 60, bottom: 40 })]}
|
|
300
|
+
modifiers={[padding({ horizontal: 24, top: 60, bottom: 40 }), scrollTargetLayout()]}
|
|
316
301
|
>
|
|
317
302
|
<RNHostView matchContents>
|
|
318
303
|
<ExpoImage
|
|
@@ -324,7 +309,9 @@ export default function SignUpScreen() {
|
|
|
324
309
|
</RNHostView>
|
|
325
310
|
|
|
326
311
|
<VStack spacing={6} alignment="leading">
|
|
327
|
-
<Text modifiers={[dfont({ size: 28, weight: "bold" })]}>
|
|
312
|
+
<Text testID="sign-up-title" modifiers={[dfont({ size: 28, weight: "bold" })]}>
|
|
313
|
+
Create your account
|
|
314
|
+
</Text>
|
|
328
315
|
<Text
|
|
329
316
|
modifiers={[dfont({ size: 16 }), foregroundStyle(colors.mutedForeground as string)]}
|
|
330
317
|
>
|
|
@@ -333,106 +320,34 @@ export default function SignUpScreen() {
|
|
|
333
320
|
</VStack>
|
|
334
321
|
|
|
335
322
|
<SegmentedToggle
|
|
323
|
+
testID="sign-up-auth-mode"
|
|
324
|
+
accessibilityLabel="Sign in or sign up"
|
|
336
325
|
value="sign-up"
|
|
337
326
|
options={[
|
|
338
327
|
{ value: "sign-in", label: "Sign in" },
|
|
339
328
|
{ value: "sign-up", label: "Sign up" },
|
|
340
329
|
]}
|
|
341
330
|
onChange={(v) => {
|
|
342
|
-
if (v === "sign-in") router.replace("/sign-in");
|
|
331
|
+
if (v === "sign-in") router.replace("/auth/sign-in");
|
|
343
332
|
}}
|
|
344
333
|
/>
|
|
345
334
|
|
|
346
|
-
{error && <ErrorText>{error}</ErrorText>}
|
|
335
|
+
{error && <ErrorText testID="sign-up-error">{error}</ErrorText>}
|
|
347
336
|
|
|
348
|
-
<VStack
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
onIsPresentedChange={setAvatarPicker}
|
|
354
|
-
titleVisibility="visible"
|
|
355
|
-
>
|
|
356
|
-
<ConfirmationDialog.Trigger>
|
|
357
|
-
<HStack
|
|
358
|
-
spacing={16}
|
|
359
|
-
alignment="center"
|
|
360
|
-
modifiers={[
|
|
361
|
-
frame({ maxWidth: 10000 }),
|
|
362
|
-
onTapGesture(() => {
|
|
363
|
-
haptics.light();
|
|
364
|
-
setAvatarPicker(true);
|
|
365
|
-
}),
|
|
366
|
-
accessibilityLabel(
|
|
367
|
-
pendingAvatar ? "Change profile photo" : "Add profile photo",
|
|
368
|
-
),
|
|
369
|
-
]}
|
|
370
|
-
>
|
|
371
|
-
{pendingAvatar ? (
|
|
372
|
-
<RNHostView matchContents>
|
|
373
|
-
<ExpoImage
|
|
374
|
-
source={{ uri: pendingAvatar.uri }}
|
|
375
|
-
style={
|
|
376
|
-
{
|
|
377
|
-
width: AVATAR_SIZE,
|
|
378
|
-
height: AVATAR_SIZE,
|
|
379
|
-
borderRadius: AVATAR_SIZE / 2,
|
|
380
|
-
} as never
|
|
381
|
-
}
|
|
382
|
-
contentFit="cover"
|
|
383
|
-
accessibilityLabel="Selected profile photo"
|
|
384
|
-
/>
|
|
385
|
-
</RNHostView>
|
|
386
|
-
) : (
|
|
387
|
-
<VStack
|
|
388
|
-
alignment="center"
|
|
389
|
-
modifiers={[
|
|
390
|
-
frame({ width: AVATAR_SIZE, height: AVATAR_SIZE }),
|
|
391
|
-
background(colors.muted as string),
|
|
392
|
-
border({ color: colors.border as string, width: 2 }),
|
|
393
|
-
clipShape("circle"),
|
|
394
|
-
]}
|
|
395
|
-
>
|
|
396
|
-
<Image
|
|
397
|
-
systemName="camera"
|
|
398
|
-
size={20}
|
|
399
|
-
color={colors.mutedForeground as string}
|
|
400
|
-
/>
|
|
401
|
-
</VStack>
|
|
402
|
-
)}
|
|
403
|
-
<Text modifiers={helperModifiers}>
|
|
404
|
-
{pendingAvatar ? "Photo selected" : "Click to upload"}
|
|
405
|
-
</Text>
|
|
406
|
-
<Spacer />
|
|
407
|
-
</HStack>
|
|
408
|
-
</ConfirmationDialog.Trigger>
|
|
409
|
-
<ConfirmationDialog.Actions>
|
|
410
|
-
<Button
|
|
411
|
-
label="Choose Photo"
|
|
412
|
-
systemImage="photo.on.rectangle"
|
|
413
|
-
onPress={() => pickAvatar("library")}
|
|
414
|
-
/>
|
|
415
|
-
<Button
|
|
416
|
-
label="Take Photo"
|
|
417
|
-
systemImage="camera"
|
|
418
|
-
onPress={() => pickAvatar("camera")}
|
|
419
|
-
/>
|
|
420
|
-
{pendingAvatar ? (
|
|
421
|
-
<Button label="Remove Photo" role="destructive" onPress={removeAvatar} />
|
|
422
|
-
) : null}
|
|
423
|
-
<Button label="Cancel" role="cancel" />
|
|
424
|
-
</ConfirmationDialog.Actions>
|
|
425
|
-
</ConfirmationDialog>
|
|
426
|
-
</VStack>
|
|
427
|
-
|
|
428
|
-
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
337
|
+
<VStack
|
|
338
|
+
spacing={6}
|
|
339
|
+
alignment="leading"
|
|
340
|
+
modifiers={[frame({ maxWidth: Infinity }), id("field-name")]}
|
|
341
|
+
>
|
|
429
342
|
<Text modifiers={labelModifiers}>Name</Text>
|
|
430
343
|
<TextField
|
|
344
|
+
testID="sign-up-name"
|
|
431
345
|
placeholder="Your name"
|
|
432
346
|
onTextChange={setName}
|
|
433
347
|
modifiers={[
|
|
434
348
|
...inputModifiers,
|
|
435
349
|
textInputAutocapitalization("words"),
|
|
350
|
+
textContentType("name"),
|
|
436
351
|
disabled(isLoading),
|
|
437
352
|
submitLabel("next"),
|
|
438
353
|
accessibilityLabel("Full name"),
|
|
@@ -441,16 +356,28 @@ export default function SignUpScreen() {
|
|
|
441
356
|
/>
|
|
442
357
|
</VStack>
|
|
443
358
|
|
|
444
|
-
<VStack
|
|
359
|
+
<VStack
|
|
360
|
+
spacing={6}
|
|
361
|
+
alignment="leading"
|
|
362
|
+
modifiers={[frame({ maxWidth: Infinity }), id("field-username")]}
|
|
363
|
+
>
|
|
445
364
|
<Text modifiers={labelModifiers}>Username (optional)</Text>
|
|
446
365
|
<TextField
|
|
366
|
+
testID="sign-up-username"
|
|
367
|
+
text={usernameState}
|
|
447
368
|
placeholder="johndoe"
|
|
448
|
-
onTextChange={
|
|
369
|
+
onTextChange={(text) => {
|
|
370
|
+
"worklet";
|
|
371
|
+
const next = maskUsername(text);
|
|
372
|
+
usernameState.value = next;
|
|
373
|
+
runOnJS(handleUsernameChange)(next);
|
|
374
|
+
}}
|
|
449
375
|
modifiers={[
|
|
450
376
|
...inputModifiers,
|
|
451
377
|
keyboardType("ascii-capable"),
|
|
452
378
|
autocorrectionDisabled(),
|
|
453
379
|
textInputAutocapitalization("never"),
|
|
380
|
+
textContentType("username"),
|
|
454
381
|
disabled(isLoading),
|
|
455
382
|
submitLabel("next"),
|
|
456
383
|
accessibilityLabel("Username"),
|
|
@@ -458,19 +385,33 @@ export default function SignUpScreen() {
|
|
|
458
385
|
]}
|
|
459
386
|
/>
|
|
460
387
|
{usernameStatus ? (
|
|
461
|
-
<
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
388
|
+
<HStack spacing={6} alignment="center">
|
|
389
|
+
<Image
|
|
390
|
+
systemName={usernameStatus.icon}
|
|
391
|
+
size={symbolSize(13)}
|
|
392
|
+
color={usernameStatus.color}
|
|
393
|
+
modifiers={[accessibilityHidden(true)]}
|
|
394
|
+
/>
|
|
395
|
+
<Text
|
|
396
|
+
testID="sign-up-username-status"
|
|
397
|
+
modifiers={[dfont({ size: 13 }), foregroundStyle(usernameStatus.color as string)]}
|
|
398
|
+
>
|
|
399
|
+
{usernameStatus.text}
|
|
400
|
+
</Text>
|
|
401
|
+
</HStack>
|
|
466
402
|
) : (
|
|
467
403
|
<Text modifiers={helperModifiers}>A unique handle others can use to find you.</Text>
|
|
468
404
|
)}
|
|
469
405
|
</VStack>
|
|
470
406
|
|
|
471
|
-
<VStack
|
|
407
|
+
<VStack
|
|
408
|
+
spacing={6}
|
|
409
|
+
alignment="leading"
|
|
410
|
+
modifiers={[frame({ maxWidth: Infinity }), id("field-email")]}
|
|
411
|
+
>
|
|
472
412
|
<Text modifiers={labelModifiers}>Email</Text>
|
|
473
413
|
<TextField
|
|
414
|
+
testID="sign-up-email"
|
|
474
415
|
placeholder="you@example.com"
|
|
475
416
|
onTextChange={setEmail}
|
|
476
417
|
modifiers={[
|
|
@@ -478,6 +419,7 @@ export default function SignUpScreen() {
|
|
|
478
419
|
keyboardType("email-address"),
|
|
479
420
|
autocorrectionDisabled(),
|
|
480
421
|
textInputAutocapitalization("never"),
|
|
422
|
+
textContentType("emailAddress"),
|
|
481
423
|
disabled(isLoading),
|
|
482
424
|
submitLabel("next"),
|
|
483
425
|
accessibilityLabel("Email address"),
|
|
@@ -486,11 +428,17 @@ export default function SignUpScreen() {
|
|
|
486
428
|
/>
|
|
487
429
|
</VStack>
|
|
488
430
|
|
|
489
|
-
<VStack
|
|
431
|
+
<VStack
|
|
432
|
+
spacing={6}
|
|
433
|
+
alignment="leading"
|
|
434
|
+
modifiers={[frame({ maxWidth: Infinity }), id("field-password")]}
|
|
435
|
+
>
|
|
490
436
|
<Text modifiers={labelModifiers}>Password</Text>
|
|
491
437
|
<PasswordField
|
|
438
|
+
testID="sign-up-password"
|
|
492
439
|
onTextChange={setPassword}
|
|
493
440
|
onSubmit={() => startTransition(() => signUp())}
|
|
441
|
+
contentType="newPassword"
|
|
494
442
|
disabled={isLoading}
|
|
495
443
|
accessibilityLabel="Password"
|
|
496
444
|
accessibilityHint="Enter a password with at least 10 characters"
|
|
@@ -499,34 +447,19 @@ export default function SignUpScreen() {
|
|
|
499
447
|
</VStack>
|
|
500
448
|
|
|
501
449
|
<ProminentButton
|
|
450
|
+
testID="sign-up-submit"
|
|
502
451
|
label={isPending ? "Creating account..." : "Create account"}
|
|
503
452
|
onPress={() => startTransition(() => signUp())}
|
|
504
453
|
disabled={isLoading}
|
|
505
454
|
/>
|
|
506
455
|
|
|
507
456
|
{showApple && (
|
|
508
|
-
<
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_UP}
|
|
515
|
-
buttonStyle={
|
|
516
|
-
colorScheme === "dark"
|
|
517
|
-
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
|
|
518
|
-
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK
|
|
519
|
-
}
|
|
520
|
-
cornerRadius={ButtonTokens.cornerRadius}
|
|
521
|
-
style={{
|
|
522
|
-
width: "100%",
|
|
523
|
-
height: "100%",
|
|
524
|
-
opacity: isLoading ? 0.5 : 1,
|
|
525
|
-
}}
|
|
526
|
-
onPress={() => startTransition(() => signUpWithApple())}
|
|
527
|
-
/>
|
|
528
|
-
</RNHostView>
|
|
529
|
-
</VStack>
|
|
457
|
+
<AppleButton
|
|
458
|
+
testID="sign-up-apple"
|
|
459
|
+
type={AppleAuthentication.AppleAuthenticationButtonType.SIGN_UP}
|
|
460
|
+
onPress={() => startTransition(() => signUpWithApple())}
|
|
461
|
+
disabled={isLoading}
|
|
462
|
+
/>
|
|
530
463
|
)}
|
|
531
464
|
</VStack>
|
|
532
465
|
</ScrollView>
|
|
@@ -544,15 +477,17 @@ export default function SignUpScreen() {
|
|
|
544
477
|
</ConfirmationDialog.Trigger>
|
|
545
478
|
<ConfirmationDialog.Actions>
|
|
546
479
|
<Button
|
|
480
|
+
testID="sign-up-discard"
|
|
547
481
|
label="Discard"
|
|
548
482
|
role="destructive"
|
|
549
483
|
onPress={() => {
|
|
484
|
+
haptics.warning();
|
|
550
485
|
const action = pendingNavAction;
|
|
551
486
|
setPendingNavAction(null);
|
|
552
487
|
if (action) navigation.dispatch(action);
|
|
553
488
|
}}
|
|
554
489
|
/>
|
|
555
|
-
<Button label="Keep Editing" role="cancel" />
|
|
490
|
+
<Button testID="sign-up-keep-editing" label="Keep Editing" role="cancel" />
|
|
556
491
|
</ConfirmationDialog.Actions>
|
|
557
492
|
<ConfirmationDialog.Message>
|
|
558
493
|
<Text modifiers={[dfont({ size: 16 })]}>You have unsaved input that will be lost.</Text>
|