@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,8 +1,8 @@
|
|
|
1
1
|
import { startTransition, useActionState, useEffect, useState } from "react";
|
|
2
2
|
import { Image as ExpoImage, useImage } from "expo-image";
|
|
3
3
|
import * as ImagePicker from "expo-image-picker";
|
|
4
|
-
import
|
|
5
|
-
import { Stack } from "expo-router";
|
|
4
|
+
import { useDeleteAccount } from "@/hooks/use-delete-account";
|
|
5
|
+
import { router, Stack } from "expo-router";
|
|
6
6
|
import { useMutation, useQuery } from "convex/react";
|
|
7
7
|
import {
|
|
8
8
|
Host,
|
|
@@ -15,9 +15,8 @@ import {
|
|
|
15
15
|
Spacer,
|
|
16
16
|
Image,
|
|
17
17
|
RNHostView,
|
|
18
|
+
Alert,
|
|
18
19
|
ConfirmationDialog,
|
|
19
|
-
BottomSheet,
|
|
20
|
-
Group,
|
|
21
20
|
ProgressView,
|
|
22
21
|
useNativeState,
|
|
23
22
|
} from "@expo/ui/swift-ui";
|
|
@@ -27,12 +26,15 @@ import {
|
|
|
27
26
|
buttonStyle,
|
|
28
27
|
clipShape,
|
|
29
28
|
cornerRadius,
|
|
29
|
+
defaultScrollAnchorForRole,
|
|
30
|
+
dynamicTypeSize,
|
|
30
31
|
foregroundStyle,
|
|
31
32
|
disabled,
|
|
32
33
|
keyboardType,
|
|
33
34
|
lineLimit,
|
|
34
35
|
onSubmit,
|
|
35
36
|
submitLabel,
|
|
37
|
+
textContentType,
|
|
36
38
|
textFieldStyle,
|
|
37
39
|
textInputAutocapitalization,
|
|
38
40
|
monospacedDigit,
|
|
@@ -40,25 +42,34 @@ import {
|
|
|
40
42
|
multilineTextAlignment,
|
|
41
43
|
padding,
|
|
42
44
|
frame,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
contentShape,
|
|
46
|
+
shapes,
|
|
45
47
|
progressViewStyle,
|
|
46
48
|
scrollDismissesKeyboard,
|
|
47
|
-
|
|
49
|
+
accessibilityHidden,
|
|
48
50
|
accessibilityLabel,
|
|
49
51
|
accessibilityHint,
|
|
50
52
|
tint,
|
|
51
53
|
} from "@expo/ui/swift-ui/modifiers";
|
|
52
54
|
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
53
|
-
import {
|
|
55
|
+
import { useSymbolSize } from "@/lib/dynamic-symbol-size";
|
|
56
|
+
import { Button as ButtonTokens, TouchTarget } from "@/constants/layout";
|
|
57
|
+
import { DynamicType } from "@/constants/ui";
|
|
58
|
+
|
|
59
|
+
import { runOnJS } from "react-native-worklets";
|
|
54
60
|
|
|
55
61
|
import { api } from "@/convex/_generated/api";
|
|
56
62
|
import { authClient } from "@/lib/auth-client";
|
|
57
63
|
import { haptics } from "@/lib/haptics";
|
|
58
|
-
import {
|
|
64
|
+
import { maskOtp, maskUsername } from "@/lib/masks";
|
|
65
|
+
import { setNativeValue } from "@/lib/native-state";
|
|
66
|
+
import {
|
|
67
|
+
firstError,
|
|
68
|
+
profileUpdateOptionalUsernameSchema,
|
|
69
|
+
profileUpdateSchema,
|
|
70
|
+
} from "@/lib/schemas";
|
|
59
71
|
import { validateBio } from "@/convex/validators";
|
|
60
72
|
import { useColors } from "@/hooks/use-theme";
|
|
61
|
-
import { PasswordField } from "@/components/auth/password-field";
|
|
62
73
|
import { ProminentButton } from "@/components/ui/prominent-button";
|
|
63
74
|
import { ErrorText, SuccessText } from "@/components/ui/status-text";
|
|
64
75
|
import { formatError } from "@/components/ui/convex-error";
|
|
@@ -69,16 +80,16 @@ const AVATAR_SIZE = 96;
|
|
|
69
80
|
|
|
70
81
|
type SaveState = { error?: string; success?: string; pendingEmail?: string };
|
|
71
82
|
type OtpState = { error?: string; success?: string };
|
|
72
|
-
type PasswordState = { error?: string; success?: string };
|
|
73
83
|
|
|
74
84
|
export default function ProfileScreen() {
|
|
75
85
|
const dfont = useDynamicFont();
|
|
86
|
+
const symbolSize = useSymbolSize();
|
|
76
87
|
const colors = useColors();
|
|
77
88
|
const me = useQuery(api.users.getMe);
|
|
78
89
|
const hasPasswordResult = useQuery(api.auth.hasPassword);
|
|
79
90
|
// Email change requires the email-OTP flow which requires Resend. In lite
|
|
80
91
|
// mode (`REQUIRE_EMAIL_VERIFICATION` unset) the email field is read-only
|
|
81
|
-
|
|
92
|
+
// no way to send a verification code to the new address.
|
|
82
93
|
const providers = useQuery(api.auth.getEnabledProviders);
|
|
83
94
|
const emailFeatures = providers?.emailFeatures === true;
|
|
84
95
|
const updateProfile = useMutation(api.users.updateProfile);
|
|
@@ -86,10 +97,13 @@ export default function ProfileScreen() {
|
|
|
86
97
|
const updateAvatar = useMutation(api.users.updateAvatar);
|
|
87
98
|
const deleteAvatar = useMutation(api.users.deleteAvatar);
|
|
88
99
|
const removeAllTokens = useMutation(api.pushTokens.removeAll);
|
|
89
|
-
const
|
|
100
|
+
const { deleteAccount, deleteError } = useDeleteAccount();
|
|
90
101
|
|
|
91
102
|
// SwiftUI source of truth via useNativeState; mirrored to React state via
|
|
92
|
-
// onTextChange so derived values like `hasChanges` stay reactive.
|
|
103
|
+
// onTextChange so derived values like `hasChanges` stay reactive. Username
|
|
104
|
+
// and the email-OTP field below add a "worklet" onTextChange so the mask
|
|
105
|
+
// (lowercase / digits-only) rewrites the field synchronously on the UI
|
|
106
|
+
// thread; name, email, and bio need no masking so they keep a plain mirror.
|
|
93
107
|
const nameState = useNativeState(me?.name ?? "");
|
|
94
108
|
const usernameState = useNativeState(me?.username ?? "");
|
|
95
109
|
const emailState = useNativeState(me?.email ?? "");
|
|
@@ -107,10 +121,10 @@ export default function ProfileScreen() {
|
|
|
107
121
|
const currentKey = me ? `${me._id}:${me.updatedAt}` : null;
|
|
108
122
|
useEffect(() => {
|
|
109
123
|
if (!me) return;
|
|
110
|
-
nameState
|
|
111
|
-
usernameState
|
|
112
|
-
emailState
|
|
113
|
-
bioState
|
|
124
|
+
setNativeValue(nameState, me.name);
|
|
125
|
+
setNativeValue(usernameState, me.username ?? "");
|
|
126
|
+
setNativeValue(emailState, me.email);
|
|
127
|
+
setNativeValue(bioState, me.bio ?? "");
|
|
114
128
|
setName(me.name);
|
|
115
129
|
setUsername(me.username ?? "");
|
|
116
130
|
setEmail(me.email);
|
|
@@ -124,14 +138,6 @@ export default function ProfileScreen() {
|
|
|
124
138
|
const [avatarPicker, setAvatarPicker] = useState(false);
|
|
125
139
|
const [signOutConfirm, setSignOutConfirm] = useState(false);
|
|
126
140
|
const [deleteAccountConfirm, setDeleteAccountConfirm] = useState(false);
|
|
127
|
-
const [passwordSheet, setPasswordSheet] = useState(false);
|
|
128
|
-
const currentPasswordState = useNativeState("");
|
|
129
|
-
const newPasswordState = useNativeState("");
|
|
130
|
-
const confirmPasswordState = useNativeState("");
|
|
131
|
-
const [currentPassword, setCurrentPassword] = useState("");
|
|
132
|
-
const [newPassword, setNewPassword] = useState("");
|
|
133
|
-
const [confirmPassword, setConfirmPassword] = useState("");
|
|
134
|
-
|
|
135
141
|
const hasChanges =
|
|
136
142
|
!!me &&
|
|
137
143
|
(name.trim() !== me.name ||
|
|
@@ -143,7 +149,10 @@ export default function ProfileScreen() {
|
|
|
143
149
|
if (!me) return { error: "Not loaded" };
|
|
144
150
|
haptics.light();
|
|
145
151
|
|
|
146
|
-
|
|
152
|
+
// Accounts without a username must still save name/email/bio; the strict
|
|
153
|
+
// schema would reject the empty username field they never set.
|
|
154
|
+
const schema = me.username ? profileUpdateSchema : profileUpdateOptionalUsernameSchema;
|
|
155
|
+
const parsed = schema.safeParse({ name, username, email });
|
|
147
156
|
if (!parsed.success) {
|
|
148
157
|
haptics.error();
|
|
149
158
|
return { error: firstError(parsed)! };
|
|
@@ -226,6 +235,7 @@ export default function ProfileScreen() {
|
|
|
226
235
|
const [avatarError, setAvatarError] = useState<string | null>(null);
|
|
227
236
|
|
|
228
237
|
const pickAvatar = async (source: "library" | "camera") => {
|
|
238
|
+
haptics.light();
|
|
229
239
|
setAvatarPicker(false);
|
|
230
240
|
await new Promise((r) => setTimeout(r, 350));
|
|
231
241
|
const perm =
|
|
@@ -233,10 +243,10 @@ export default function ProfileScreen() {
|
|
|
233
243
|
? await ImagePicker.requestCameraPermissionsAsync()
|
|
234
244
|
: await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
235
245
|
if (!perm.granted) {
|
|
246
|
+
haptics.error();
|
|
236
247
|
setAvatarError(source === "camera" ? "Camera access denied" : "Photos access denied");
|
|
237
248
|
return;
|
|
238
249
|
}
|
|
239
|
-
haptics.light();
|
|
240
250
|
const options: ImagePicker.ImagePickerOptions = {
|
|
241
251
|
mediaTypes: ["images"],
|
|
242
252
|
allowsEditing: true,
|
|
@@ -304,66 +314,13 @@ export default function ProfileScreen() {
|
|
|
304
314
|
await authClient.signOut();
|
|
305
315
|
};
|
|
306
316
|
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
const result = await LocalAuthentication.authenticateAsync({
|
|
310
|
-
promptMessage: "Confirm with Face ID",
|
|
311
|
-
});
|
|
312
|
-
if (!result.success) return;
|
|
313
|
-
await deleteAccountMutation();
|
|
314
|
-
await authClient.signOut();
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const [passwordState, changePassword, isChangingPassword] = useActionState<PasswordState, void>(
|
|
318
|
-
async () => {
|
|
319
|
-
haptics.light();
|
|
320
|
-
if (!currentPassword || !newPassword || !confirmPassword) {
|
|
321
|
-
haptics.error();
|
|
322
|
-
return { error: "Fill in every field" };
|
|
323
|
-
}
|
|
324
|
-
if (newPassword.length < 10 || newPassword.length > 128) {
|
|
325
|
-
haptics.error();
|
|
326
|
-
return { error: "Password must be 10-128 characters" };
|
|
327
|
-
}
|
|
328
|
-
if (newPassword !== confirmPassword) {
|
|
329
|
-
haptics.error();
|
|
330
|
-
return { error: "Passwords do not match" };
|
|
331
|
-
}
|
|
332
|
-
try {
|
|
333
|
-
const res = await authClient.changePassword({
|
|
334
|
-
currentPassword,
|
|
335
|
-
newPassword,
|
|
336
|
-
revokeOtherSessions: true,
|
|
337
|
-
});
|
|
338
|
-
if (res.error) {
|
|
339
|
-
haptics.error();
|
|
340
|
-
return { error: res.error.message ?? "Failed to change password" };
|
|
341
|
-
}
|
|
342
|
-
haptics.success();
|
|
343
|
-
announce("Password changed. Other sessions have been signed out.");
|
|
344
|
-
currentPasswordState.value = "";
|
|
345
|
-
newPasswordState.value = "";
|
|
346
|
-
confirmPasswordState.value = "";
|
|
347
|
-
setCurrentPassword("");
|
|
348
|
-
setNewPassword("");
|
|
349
|
-
setConfirmPassword("");
|
|
350
|
-
setPasswordSheet(false);
|
|
351
|
-
return { success: "Password updated. Other sessions have been signed out." };
|
|
352
|
-
} catch {
|
|
353
|
-
haptics.error();
|
|
354
|
-
return { error: "An unexpected error occurred" };
|
|
355
|
-
}
|
|
356
|
-
},
|
|
357
|
-
{} as PasswordState,
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
const error = saveState.error ?? otpState.error ?? passwordState.error ?? avatarError;
|
|
361
|
-
const success = saveState.success ?? otpState.success ?? passwordState.success;
|
|
317
|
+
const error = saveState.error ?? otpState.error ?? avatarError ?? deleteError;
|
|
318
|
+
const success = saveState.success ?? otpState.success;
|
|
362
319
|
|
|
363
320
|
if (!me) {
|
|
364
321
|
return (
|
|
365
|
-
<Host style={{ flex: 1, backgroundColor: colors.background }}>
|
|
366
|
-
<SkeletonProfile />
|
|
322
|
+
<Host testID="profile-loading" style={{ flex: 1, backgroundColor: colors.background }}>
|
|
323
|
+
<SkeletonProfile testID="profile-skeleton" />
|
|
367
324
|
</Host>
|
|
368
325
|
);
|
|
369
326
|
}
|
|
@@ -373,7 +330,7 @@ export default function ProfileScreen() {
|
|
|
373
330
|
const inputModifiers = [
|
|
374
331
|
textFieldStyle("plain"),
|
|
375
332
|
padding({ horizontal: 16 }),
|
|
376
|
-
frame({ maxWidth: Infinity,
|
|
333
|
+
frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
|
|
377
334
|
background(colors.muted as string),
|
|
378
335
|
clipShape("capsule"),
|
|
379
336
|
dfont({ size: 16 }),
|
|
@@ -391,9 +348,16 @@ export default function ProfileScreen() {
|
|
|
391
348
|
/>
|
|
392
349
|
</Stack.Toolbar>
|
|
393
350
|
|
|
394
|
-
<Host style={{ flex: 1, backgroundColor: colors.background }}>
|
|
351
|
+
<Host testID="profile-screen" style={{ flex: 1, backgroundColor: colors.background }}>
|
|
395
352
|
<ScrollView
|
|
396
|
-
modifiers={[
|
|
353
|
+
modifiers={[
|
|
354
|
+
scrollDismissesKeyboard("interactively"),
|
|
355
|
+
tint(colors.primary as string),
|
|
356
|
+
// Keep the visible center pinned when an inline error or the avatar
|
|
357
|
+
// sheet expands the form so the user doesn't jump to a new section.
|
|
358
|
+
// No-op below iOS 18.
|
|
359
|
+
defaultScrollAnchorForRole("center", "sizeChanges"),
|
|
360
|
+
]}
|
|
397
361
|
>
|
|
398
362
|
<VStack
|
|
399
363
|
spacing={20}
|
|
@@ -407,92 +371,122 @@ export default function ProfileScreen() {
|
|
|
407
371
|
titleVisibility="visible"
|
|
408
372
|
>
|
|
409
373
|
<ConfirmationDialog.Trigger>
|
|
410
|
-
<
|
|
411
|
-
|
|
412
|
-
alignment="center"
|
|
374
|
+
<Button
|
|
375
|
+
testID="profile-avatar"
|
|
413
376
|
modifiers={[
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
setAvatarPicker(true);
|
|
418
|
-
}),
|
|
377
|
+
buttonStyle("plain"),
|
|
378
|
+
frame({ maxWidth: Infinity, minHeight: TouchTarget.min }),
|
|
379
|
+
contentShape(shapes.rectangle()),
|
|
419
380
|
accessibilityLabel("Change profile photo"),
|
|
420
381
|
]}
|
|
382
|
+
onPress={() => {
|
|
383
|
+
haptics.light();
|
|
384
|
+
setAvatarPicker(true);
|
|
385
|
+
}}
|
|
421
386
|
>
|
|
422
|
-
<
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
387
|
+
<HStack
|
|
388
|
+
spacing={16}
|
|
389
|
+
alignment="center"
|
|
390
|
+
modifiers={[frame({ maxWidth: Infinity })]}
|
|
391
|
+
>
|
|
392
|
+
<AvatarView avatarUrl={me.avatarUrl} loading={avatarUpdating} />
|
|
393
|
+
<VStack alignment="leading" spacing={4}>
|
|
394
|
+
<Text
|
|
395
|
+
testID="profile-name-value"
|
|
396
|
+
modifiers={[dfont({ size: 17, weight: "semibold" })]}
|
|
397
|
+
>
|
|
398
|
+
{me.name}
|
|
399
|
+
</Text>
|
|
400
|
+
<Text
|
|
401
|
+
testID="profile-email-value"
|
|
402
|
+
modifiers={[
|
|
403
|
+
dfont({ size: 14 }),
|
|
404
|
+
foregroundStyle(colors.mutedForeground as string),
|
|
405
|
+
]}
|
|
406
|
+
>
|
|
407
|
+
{me.email}
|
|
408
|
+
</Text>
|
|
409
|
+
</VStack>
|
|
410
|
+
<Spacer />
|
|
411
|
+
<Image
|
|
412
|
+
systemName="camera.circle.fill"
|
|
413
|
+
size={symbolSize(28)}
|
|
414
|
+
color={colors.primary as string}
|
|
415
|
+
modifiers={[accessibilityHidden(true)]}
|
|
416
|
+
/>
|
|
417
|
+
</HStack>
|
|
418
|
+
</Button>
|
|
441
419
|
</ConfirmationDialog.Trigger>
|
|
442
420
|
<ConfirmationDialog.Actions>
|
|
443
421
|
<Button
|
|
422
|
+
testID="profile-avatar-choose"
|
|
444
423
|
label="Choose Photo"
|
|
445
424
|
systemImage="photo.on.rectangle"
|
|
446
425
|
onPress={() => pickAvatar("library")}
|
|
447
426
|
/>
|
|
448
427
|
<Button
|
|
428
|
+
testID="profile-avatar-take"
|
|
449
429
|
label="Take Photo"
|
|
450
430
|
systemImage="camera"
|
|
451
431
|
onPress={() => pickAvatar("camera")}
|
|
452
432
|
/>
|
|
453
433
|
{me.hasUploadedAvatar && (
|
|
454
|
-
<Button
|
|
434
|
+
<Button
|
|
435
|
+
testID="profile-avatar-remove"
|
|
436
|
+
label="Remove Photo"
|
|
437
|
+
role="destructive"
|
|
438
|
+
onPress={removeAvatar}
|
|
439
|
+
/>
|
|
455
440
|
)}
|
|
456
|
-
<Button label="Cancel" role="cancel" />
|
|
441
|
+
<Button testID="profile-avatar-cancel" label="Cancel" role="cancel" />
|
|
457
442
|
</ConfirmationDialog.Actions>
|
|
458
443
|
</ConfirmationDialog>
|
|
459
444
|
|
|
460
|
-
{error ? <ErrorText>{error}</ErrorText> : null}
|
|
461
|
-
{success && !pendingEmail ?
|
|
445
|
+
{error ? <ErrorText testID="profile-error">{error}</ErrorText> : null}
|
|
446
|
+
{success && !pendingEmail ? (
|
|
447
|
+
<SuccessText testID="profile-success">{success}</SuccessText>
|
|
448
|
+
) : null}
|
|
462
449
|
|
|
463
450
|
{pendingEmail ? (
|
|
464
451
|
<>
|
|
465
452
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
466
453
|
<Text modifiers={labelModifiers}>Verify new email</Text>
|
|
467
454
|
<TextField
|
|
455
|
+
testID="profile-email-otp"
|
|
468
456
|
text={otpCodeState}
|
|
469
457
|
placeholder="000000"
|
|
470
458
|
onTextChange={(text) => {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
459
|
+
"worklet";
|
|
460
|
+
const digits = maskOtp(text);
|
|
461
|
+
otpCodeState.value = digits;
|
|
462
|
+
runOnJS(setOtp)(digits);
|
|
474
463
|
}}
|
|
475
464
|
autoFocus
|
|
476
465
|
modifiers={[
|
|
477
466
|
...inputModifiers,
|
|
478
467
|
keyboardType("numeric"),
|
|
468
|
+
textContentType("oneTimeCode"),
|
|
479
469
|
onSubmit(() => startTransition(() => verifyOtp())),
|
|
480
470
|
dfont({ size: 24, design: "monospaced" }),
|
|
481
471
|
monospacedDigit(),
|
|
482
472
|
kerning(8),
|
|
483
473
|
multilineTextAlignment("center"),
|
|
474
|
+
// upstream expo/expo#46540: six monospaced glyphs in a
|
|
475
|
+
// capsule that can't wrap, cap Dynamic Type so they fit.
|
|
476
|
+
dynamicTypeSize({ max: DynamicType.otp }),
|
|
484
477
|
submitLabel("done"),
|
|
485
478
|
disabled(isVerifying),
|
|
486
479
|
accessibilityLabel("Verification code"),
|
|
487
480
|
accessibilityHint("Enter the 6 digit code sent to your new email"),
|
|
488
481
|
]}
|
|
489
482
|
/>
|
|
490
|
-
<Text modifiers={helperModifiers}>
|
|
483
|
+
<Text testID="profile-email-otp-sent" modifiers={helperModifiers}>
|
|
491
484
|
A 6-digit code was sent to {pendingEmail}.
|
|
492
485
|
</Text>
|
|
493
486
|
</VStack>
|
|
494
487
|
|
|
495
488
|
<ProminentButton
|
|
489
|
+
testID="profile-email-verify"
|
|
496
490
|
label={isVerifying ? "Verifying..." : "Verify"}
|
|
497
491
|
onPress={() => startTransition(() => verifyOtp())}
|
|
498
492
|
disabled={isVerifying || otp.length !== 6}
|
|
@@ -500,6 +494,7 @@ export default function ProfileScreen() {
|
|
|
500
494
|
|
|
501
495
|
<VStack alignment="center" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
502
496
|
<Button
|
|
497
|
+
testID="profile-email-verify-cancel"
|
|
503
498
|
label="Cancel"
|
|
504
499
|
modifiers={[
|
|
505
500
|
buttonStyle("plain"),
|
|
@@ -508,6 +503,7 @@ export default function ProfileScreen() {
|
|
|
508
503
|
disabled(isVerifying),
|
|
509
504
|
]}
|
|
510
505
|
onPress={() => {
|
|
506
|
+
haptics.light();
|
|
511
507
|
setPendingEmail(null);
|
|
512
508
|
setOtp("");
|
|
513
509
|
}}
|
|
@@ -519,12 +515,14 @@ export default function ProfileScreen() {
|
|
|
519
515
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
520
516
|
<Text modifiers={labelModifiers}>Name</Text>
|
|
521
517
|
<TextField
|
|
518
|
+
testID="profile-name"
|
|
522
519
|
text={nameState}
|
|
523
520
|
placeholder="Name"
|
|
524
521
|
onTextChange={setName}
|
|
525
522
|
modifiers={[
|
|
526
523
|
...inputModifiers,
|
|
527
524
|
textInputAutocapitalization("words"),
|
|
525
|
+
textContentType("name"),
|
|
528
526
|
disabled(isSaving),
|
|
529
527
|
submitLabel("next"),
|
|
530
528
|
accessibilityLabel("Name"),
|
|
@@ -536,14 +534,21 @@ export default function ProfileScreen() {
|
|
|
536
534
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
537
535
|
<Text modifiers={labelModifiers}>Username</Text>
|
|
538
536
|
<TextField
|
|
537
|
+
testID="profile-username"
|
|
539
538
|
text={usernameState}
|
|
540
539
|
placeholder="johndoe"
|
|
541
|
-
onTextChange={(
|
|
540
|
+
onTextChange={(text) => {
|
|
541
|
+
"worklet";
|
|
542
|
+
const next = maskUsername(text);
|
|
543
|
+
usernameState.value = next;
|
|
544
|
+
runOnJS(setUsername)(next);
|
|
545
|
+
}}
|
|
542
546
|
modifiers={[
|
|
543
547
|
...inputModifiers,
|
|
544
548
|
keyboardType("ascii-capable"),
|
|
545
549
|
autocorrectionDisabled(),
|
|
546
550
|
textInputAutocapitalization("never"),
|
|
551
|
+
textContentType("username"),
|
|
547
552
|
disabled(isSaving),
|
|
548
553
|
submitLabel("next"),
|
|
549
554
|
accessibilityLabel("Username"),
|
|
@@ -558,6 +563,7 @@ export default function ProfileScreen() {
|
|
|
558
563
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
559
564
|
<Text modifiers={labelModifiers}>Email</Text>
|
|
560
565
|
<TextField
|
|
566
|
+
testID="profile-email"
|
|
561
567
|
text={emailState}
|
|
562
568
|
placeholder="you@example.com"
|
|
563
569
|
onTextChange={setEmail}
|
|
@@ -566,6 +572,7 @@ export default function ProfileScreen() {
|
|
|
566
572
|
keyboardType("email-address"),
|
|
567
573
|
autocorrectionDisabled(),
|
|
568
574
|
textInputAutocapitalization("never"),
|
|
575
|
+
textContentType("emailAddress"),
|
|
569
576
|
disabled(isSaving || !emailFeatures),
|
|
570
577
|
submitLabel("next"),
|
|
571
578
|
accessibilityLabel("Email address"),
|
|
@@ -579,13 +586,14 @@ export default function ProfileScreen() {
|
|
|
579
586
|
<Text modifiers={helperModifiers}>
|
|
580
587
|
{emailFeatures
|
|
581
588
|
? "Changing your email requires verifying the new address with a 6-digit code."
|
|
582
|
-
: "Email change requires Resend setup. Run `
|
|
589
|
+
: "Email change requires Resend setup. Run `npx vexpo full` to enable."}
|
|
583
590
|
</Text>
|
|
584
591
|
</VStack>
|
|
585
592
|
|
|
586
593
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
587
594
|
<Text modifiers={labelModifiers}>Bio</Text>
|
|
588
595
|
<TextField
|
|
596
|
+
testID="profile-bio"
|
|
589
597
|
text={bioState}
|
|
590
598
|
placeholder="Tell others about yourself"
|
|
591
599
|
onTextChange={setBio}
|
|
@@ -612,6 +620,7 @@ export default function ProfileScreen() {
|
|
|
612
620
|
<VStack spacing={6} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
613
621
|
<Text modifiers={labelModifiers}>Member since</Text>
|
|
614
622
|
<Text
|
|
623
|
+
testID="profile-member-since-value"
|
|
615
624
|
modifiers={[
|
|
616
625
|
dfont({ size: 16 }),
|
|
617
626
|
foregroundStyle(colors.mutedForeground as string),
|
|
@@ -623,6 +632,7 @@ export default function ProfileScreen() {
|
|
|
623
632
|
|
|
624
633
|
{hasChanges ? (
|
|
625
634
|
<ProminentButton
|
|
635
|
+
testID="profile-save"
|
|
626
636
|
label={isSaving ? "Saving..." : "Save changes"}
|
|
627
637
|
onPress={() => startTransition(() => save())}
|
|
628
638
|
disabled={isSaving}
|
|
@@ -631,20 +641,21 @@ export default function ProfileScreen() {
|
|
|
631
641
|
|
|
632
642
|
{hasPasswordResult ? (
|
|
633
643
|
<Button
|
|
644
|
+
testID="profile-change-password"
|
|
634
645
|
modifiers={[
|
|
635
646
|
buttonStyle("plain"),
|
|
636
|
-
frame({ maxWidth:
|
|
647
|
+
frame({ maxWidth: Infinity }),
|
|
637
648
|
background(colors.muted as string),
|
|
638
649
|
clipShape("capsule"),
|
|
639
650
|
]}
|
|
640
651
|
onPress={() => {
|
|
641
652
|
haptics.light();
|
|
642
|
-
|
|
653
|
+
router.push("/profile/change-password");
|
|
643
654
|
}}
|
|
644
655
|
>
|
|
645
656
|
<Text
|
|
646
657
|
modifiers={[
|
|
647
|
-
frame({ maxWidth:
|
|
658
|
+
frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
|
|
648
659
|
multilineTextAlignment("center"),
|
|
649
660
|
dfont({
|
|
650
661
|
size: ButtonTokens.fontSize,
|
|
@@ -666,17 +677,21 @@ export default function ProfileScreen() {
|
|
|
666
677
|
>
|
|
667
678
|
<ConfirmationDialog.Trigger>
|
|
668
679
|
<Button
|
|
680
|
+
testID="profile-sign-out"
|
|
669
681
|
modifiers={[
|
|
670
682
|
buttonStyle("plain"),
|
|
671
|
-
frame({ maxWidth:
|
|
683
|
+
frame({ maxWidth: Infinity }),
|
|
672
684
|
background(colors.muted as string),
|
|
673
685
|
clipShape("capsule"),
|
|
674
686
|
]}
|
|
675
|
-
onPress={() =>
|
|
687
|
+
onPress={() => {
|
|
688
|
+
haptics.medium();
|
|
689
|
+
setSignOutConfirm(true);
|
|
690
|
+
}}
|
|
676
691
|
>
|
|
677
692
|
<Text
|
|
678
693
|
modifiers={[
|
|
679
|
-
frame({ maxWidth:
|
|
694
|
+
frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
|
|
680
695
|
multilineTextAlignment("center"),
|
|
681
696
|
dfont({
|
|
682
697
|
size: ButtonTokens.fontSize,
|
|
@@ -690,8 +705,13 @@ export default function ProfileScreen() {
|
|
|
690
705
|
</Button>
|
|
691
706
|
</ConfirmationDialog.Trigger>
|
|
692
707
|
<ConfirmationDialog.Actions>
|
|
693
|
-
<Button
|
|
694
|
-
|
|
708
|
+
<Button
|
|
709
|
+
testID="profile-sign-out-confirm"
|
|
710
|
+
label="Sign Out"
|
|
711
|
+
role="destructive"
|
|
712
|
+
onPress={handleSignOut}
|
|
713
|
+
/>
|
|
714
|
+
<Button testID="profile-sign-out-cancel" label="Cancel" role="cancel" />
|
|
695
715
|
</ConfirmationDialog.Actions>
|
|
696
716
|
<ConfirmationDialog.Message>
|
|
697
717
|
<Text modifiers={[dfont({ size: 16 })]}>
|
|
@@ -700,24 +720,27 @@ export default function ProfileScreen() {
|
|
|
700
720
|
</ConfirmationDialog.Message>
|
|
701
721
|
</ConfirmationDialog>
|
|
702
722
|
|
|
703
|
-
<
|
|
723
|
+
<Alert
|
|
704
724
|
title="Delete account?"
|
|
705
725
|
isPresented={deleteAccountConfirm}
|
|
706
726
|
onIsPresentedChange={setDeleteAccountConfirm}
|
|
707
|
-
titleVisibility="visible"
|
|
708
727
|
>
|
|
709
|
-
<
|
|
728
|
+
<Alert.Trigger>
|
|
710
729
|
<Button
|
|
730
|
+
testID="profile-delete-account"
|
|
711
731
|
modifiers={[
|
|
712
732
|
buttonStyle("plain"),
|
|
713
|
-
frame({ maxWidth:
|
|
733
|
+
frame({ maxWidth: Infinity }),
|
|
714
734
|
clipShape("capsule"),
|
|
715
735
|
]}
|
|
716
|
-
onPress={() =>
|
|
736
|
+
onPress={() => {
|
|
737
|
+
haptics.warning();
|
|
738
|
+
setDeleteAccountConfirm(true);
|
|
739
|
+
}}
|
|
717
740
|
>
|
|
718
741
|
<Text
|
|
719
742
|
modifiers={[
|
|
720
|
-
frame({ maxWidth:
|
|
743
|
+
frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
|
|
721
744
|
multilineTextAlignment("center"),
|
|
722
745
|
dfont({
|
|
723
746
|
size: ButtonTokens.fontSize,
|
|
@@ -729,128 +752,27 @@ export default function ProfileScreen() {
|
|
|
729
752
|
Delete account
|
|
730
753
|
</Text>
|
|
731
754
|
</Button>
|
|
732
|
-
</
|
|
733
|
-
<
|
|
755
|
+
</Alert.Trigger>
|
|
756
|
+
<Alert.Actions>
|
|
734
757
|
<Button
|
|
735
|
-
|
|
758
|
+
testID="profile-delete-account-confirm"
|
|
759
|
+
label="Delete Account"
|
|
736
760
|
role="destructive"
|
|
737
|
-
onPress={
|
|
761
|
+
onPress={deleteAccount}
|
|
738
762
|
/>
|
|
739
|
-
<Button label="Cancel" role="cancel" />
|
|
740
|
-
</
|
|
741
|
-
<
|
|
763
|
+
<Button testID="profile-delete-account-cancel" label="Cancel" role="cancel" />
|
|
764
|
+
</Alert.Actions>
|
|
765
|
+
<Alert.Message>
|
|
742
766
|
<Text modifiers={[dfont({ size: 16 })]}>
|
|
743
|
-
|
|
767
|
+
Your account is scheduled for permanent deletion in 30 days. Sign in within
|
|
768
|
+
that window to restore it.
|
|
744
769
|
</Text>
|
|
745
|
-
</
|
|
746
|
-
</
|
|
770
|
+
</Alert.Message>
|
|
771
|
+
</Alert>
|
|
747
772
|
</>
|
|
748
773
|
)}
|
|
749
774
|
</VStack>
|
|
750
775
|
</ScrollView>
|
|
751
|
-
|
|
752
|
-
<BottomSheet isPresented={passwordSheet} onIsPresentedChange={setPasswordSheet}>
|
|
753
|
-
<Group
|
|
754
|
-
modifiers={[presentationDetents(["medium"]), presentationDragIndicator("visible")]}
|
|
755
|
-
>
|
|
756
|
-
<Host style={{ flex: 1, backgroundColor: colors.background }}>
|
|
757
|
-
<ScrollView
|
|
758
|
-
modifiers={[
|
|
759
|
-
scrollDismissesKeyboard("interactively"),
|
|
760
|
-
tint(colors.primary as string),
|
|
761
|
-
]}
|
|
762
|
-
>
|
|
763
|
-
<VStack
|
|
764
|
-
spacing={20}
|
|
765
|
-
alignment="leading"
|
|
766
|
-
modifiers={[padding({ horizontal: 24, top: 24, bottom: 40 })]}
|
|
767
|
-
>
|
|
768
|
-
<VStack spacing={6} alignment="leading">
|
|
769
|
-
<Text modifiers={[dfont({ size: 22, weight: "bold" })]}>Change password</Text>
|
|
770
|
-
<Text
|
|
771
|
-
modifiers={[
|
|
772
|
-
dfont({ size: 14 }),
|
|
773
|
-
foregroundStyle(colors.mutedForeground as string),
|
|
774
|
-
]}
|
|
775
|
-
>
|
|
776
|
-
Other devices will be signed out.
|
|
777
|
-
</Text>
|
|
778
|
-
</VStack>
|
|
779
|
-
|
|
780
|
-
<VStack
|
|
781
|
-
spacing={6}
|
|
782
|
-
alignment="leading"
|
|
783
|
-
modifiers={[frame({ maxWidth: Infinity })]}
|
|
784
|
-
>
|
|
785
|
-
<Text modifiers={labelModifiers}>Current password</Text>
|
|
786
|
-
<PasswordField
|
|
787
|
-
text={currentPasswordState}
|
|
788
|
-
onTextChange={setCurrentPassword}
|
|
789
|
-
disabled={isChangingPassword}
|
|
790
|
-
submitLabelType="next"
|
|
791
|
-
accessibilityLabel="Current password"
|
|
792
|
-
accessibilityHint="Enter your existing password"
|
|
793
|
-
/>
|
|
794
|
-
</VStack>
|
|
795
|
-
|
|
796
|
-
<VStack
|
|
797
|
-
spacing={6}
|
|
798
|
-
alignment="leading"
|
|
799
|
-
modifiers={[frame({ maxWidth: Infinity })]}
|
|
800
|
-
>
|
|
801
|
-
<Text modifiers={labelModifiers}>New password</Text>
|
|
802
|
-
<PasswordField
|
|
803
|
-
text={newPasswordState}
|
|
804
|
-
onTextChange={setNewPassword}
|
|
805
|
-
disabled={isChangingPassword}
|
|
806
|
-
submitLabelType="next"
|
|
807
|
-
accessibilityLabel="New password"
|
|
808
|
-
accessibilityHint="Choose a new password with at least 10 characters"
|
|
809
|
-
/>
|
|
810
|
-
<Text modifiers={helperModifiers}>At least 10 characters.</Text>
|
|
811
|
-
</VStack>
|
|
812
|
-
|
|
813
|
-
<VStack
|
|
814
|
-
spacing={6}
|
|
815
|
-
alignment="leading"
|
|
816
|
-
modifiers={[frame({ maxWidth: Infinity })]}
|
|
817
|
-
>
|
|
818
|
-
<Text modifiers={labelModifiers}>Confirm new password</Text>
|
|
819
|
-
<PasswordField
|
|
820
|
-
text={confirmPasswordState}
|
|
821
|
-
onTextChange={setConfirmPassword}
|
|
822
|
-
onSubmit={() => startTransition(() => changePassword())}
|
|
823
|
-
disabled={isChangingPassword}
|
|
824
|
-
accessibilityLabel="Confirm new password"
|
|
825
|
-
accessibilityHint="Re-enter the new password to confirm"
|
|
826
|
-
/>
|
|
827
|
-
</VStack>
|
|
828
|
-
|
|
829
|
-
{passwordState.error ? <ErrorText>{passwordState.error}</ErrorText> : null}
|
|
830
|
-
|
|
831
|
-
<ProminentButton
|
|
832
|
-
label={isChangingPassword ? "Updating..." : "Update password"}
|
|
833
|
-
onPress={() => startTransition(() => changePassword())}
|
|
834
|
-
disabled={isChangingPassword}
|
|
835
|
-
/>
|
|
836
|
-
|
|
837
|
-
<VStack alignment="center" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
838
|
-
<Button
|
|
839
|
-
label="Cancel"
|
|
840
|
-
modifiers={[
|
|
841
|
-
buttonStyle("plain"),
|
|
842
|
-
foregroundStyle(colors.mutedForeground as string),
|
|
843
|
-
dfont({ size: 14, weight: "semibold" }),
|
|
844
|
-
disabled(isChangingPassword),
|
|
845
|
-
]}
|
|
846
|
-
onPress={() => setPasswordSheet(false)}
|
|
847
|
-
/>
|
|
848
|
-
</VStack>
|
|
849
|
-
</VStack>
|
|
850
|
-
</ScrollView>
|
|
851
|
-
</Host>
|
|
852
|
-
</Group>
|
|
853
|
-
</BottomSheet>
|
|
854
776
|
</Host>
|
|
855
777
|
</>
|
|
856
778
|
);
|
|
@@ -864,7 +786,9 @@ function AvatarView({ avatarUrl, loading }: { avatarUrl: string | null; loading:
|
|
|
864
786
|
alignment="center"
|
|
865
787
|
modifiers={[frame({ width: AVATAR_SIZE, height: AVATAR_SIZE }), clipShape("circle")]}
|
|
866
788
|
>
|
|
867
|
-
<ProgressView
|
|
789
|
+
<ProgressView
|
|
790
|
+
modifiers={[progressViewStyle("circular"), accessibilityLabel("Updating profile photo")]}
|
|
791
|
+
/>
|
|
868
792
|
</VStack>
|
|
869
793
|
);
|
|
870
794
|
}
|
|
@@ -876,7 +800,7 @@ function AvatarView({ avatarUrl, loading }: { avatarUrl: string | null; loading:
|
|
|
876
800
|
systemName="person.crop.circle.fill"
|
|
877
801
|
size={AVATAR_SIZE}
|
|
878
802
|
color={colors.mutedForeground as string}
|
|
879
|
-
modifiers={[frame({ width: AVATAR_SIZE, height: AVATAR_SIZE })]}
|
|
803
|
+
modifiers={[frame({ width: AVATAR_SIZE, height: AVATAR_SIZE }), accessibilityHidden(true)]}
|
|
880
804
|
/>
|
|
881
805
|
);
|
|
882
806
|
}
|
|
@@ -890,7 +814,7 @@ function RemoteAvatar({ url, size }: { url: string; size: number }) {
|
|
|
890
814
|
systemName="person.crop.circle.fill"
|
|
891
815
|
size={size}
|
|
892
816
|
color={colors.mutedForeground as string}
|
|
893
|
-
modifiers={[frame({ width: size, height: size })]}
|
|
817
|
+
modifiers={[frame({ width: size, height: size }), accessibilityHidden(true)]}
|
|
894
818
|
/>
|
|
895
819
|
);
|
|
896
820
|
}
|