@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.
Files changed (174) hide show
  1. package/README.md +10 -10
  2. package/dist/index.js +8 -7
  3. package/dist/templates/default/.eas/workflows/asc-events.yml +9 -6
  4. package/dist/templates/default/.eas/workflows/deploy-production.yml +28 -15
  5. package/dist/templates/default/.eas/workflows/e2e-tests.yml +3 -2
  6. package/dist/templates/default/.eas/workflows/pr-preview.yml +12 -21
  7. package/dist/templates/default/.eas/workflows/release.yml +3 -7
  8. package/dist/templates/default/.eas/workflows/rollback.yml +54 -28
  9. package/dist/templates/default/.eas/workflows/rollout.yml +27 -33
  10. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +1 -5
  11. package/dist/templates/default/.eas/workflows/testflight.yml +3 -7
  12. package/dist/templates/default/.github/workflows/check.yml +20 -12
  13. package/dist/templates/default/.maestro/launch.yaml +19 -10
  14. package/dist/templates/default/AGENTS.md +25 -8
  15. package/dist/templates/default/DESIGN.md +14 -10
  16. package/dist/templates/default/README.md +83 -78
  17. package/dist/templates/default/SETUP.md +159 -152
  18. package/dist/templates/default/__tests__/convex/_auth-harness.test.ts +112 -0
  19. package/dist/templates/default/__tests__/convex/_harness.ts +132 -0
  20. package/dist/templates/default/__tests__/convex/appAttest.test.ts +172 -0
  21. package/dist/templates/default/__tests__/convex/appAttestStore.test.ts +48 -0
  22. package/dist/templates/default/__tests__/convex/pushTokens-remove.test.ts +106 -0
  23. package/dist/templates/default/__tests__/convex/pushTokens-upsert.test.ts +146 -0
  24. package/dist/templates/default/__tests__/convex/users-deleteAccount.test.ts +140 -0
  25. package/dist/templates/default/__tests__/convex/users-deleteAvatar.test.ts +104 -0
  26. package/dist/templates/default/__tests__/convex/users-getMe.test.ts +98 -0
  27. package/dist/templates/default/__tests__/convex/users-getUser.test.ts +120 -0
  28. package/dist/templates/default/__tests__/convex/users-hardDeleteExpired.test.ts +67 -0
  29. package/dist/templates/default/__tests__/convex/users-restoreAccount.test.ts +96 -0
  30. package/dist/templates/default/__tests__/convex/users-updateAvatar.test.ts +92 -0
  31. package/dist/templates/default/__tests__/convex/users-updateProfile.test.ts +126 -0
  32. package/dist/templates/default/__tests__/convex/webhook.test.ts +31 -0
  33. package/dist/templates/default/__tests__/lib/deep-link.test.ts +51 -6
  34. package/dist/templates/default/__tests__/lib/schemas.test.ts +205 -0
  35. package/dist/templates/default/__tests__/lib/text-style.test.ts +31 -0
  36. package/dist/templates/default/_env.example +7 -7
  37. package/dist/templates/default/_gitattributes +1 -1
  38. package/dist/templates/default/_gitignore +17 -2
  39. package/dist/templates/default/_npmrc +7 -0
  40. package/dist/templates/default/_oxlintrc.json +1 -1
  41. package/dist/templates/default/app-store/accessibility.config.json +20 -0
  42. package/dist/templates/default/app-store/privacy.config.json +27 -0
  43. package/dist/templates/default/app.config.ts +105 -33
  44. package/dist/templates/default/app.json +1 -9
  45. package/dist/templates/default/convex/_generated/api.d.ts +12 -0
  46. package/dist/templates/default/convex/admin.ts +0 -13
  47. package/dist/templates/default/convex/appAttest.ts +467 -0
  48. package/dist/templates/default/convex/appAttestStore.ts +141 -0
  49. package/dist/templates/default/convex/apple.ts +53 -0
  50. package/dist/templates/default/convex/auth.ts +6 -45
  51. package/dist/templates/default/convex/constants.ts +2 -7
  52. package/dist/templates/default/convex/crons.ts +12 -5
  53. package/dist/templates/default/convex/email.ts +4 -24
  54. package/dist/templates/default/convex/env.ts +0 -4
  55. package/dist/templates/default/convex/errors.ts +0 -7
  56. package/dist/templates/default/convex/functions.ts +0 -26
  57. package/dist/templates/default/convex/http.ts +3 -5
  58. package/dist/templates/default/convex/log.ts +2 -25
  59. package/dist/templates/default/convex/pushSender.ts +145 -0
  60. package/dist/templates/default/convex/pushTokens.ts +110 -13
  61. package/dist/templates/default/convex/rateLimit.ts +8 -39
  62. package/dist/templates/default/convex/schema.ts +48 -5
  63. package/dist/templates/default/convex/tsconfig.json +1 -0
  64. package/dist/templates/default/convex/users.ts +143 -61
  65. package/dist/templates/default/convex/validators.ts +1 -38
  66. package/dist/templates/default/convex/webhook.ts +1 -31
  67. package/dist/templates/default/convex.json +1 -2
  68. package/dist/templates/default/metro.config.js +9 -1
  69. package/dist/templates/default/package.json +67 -70
  70. package/dist/templates/default/plugins/README.md +5 -1
  71. package/dist/templates/default/scripts/README.md +9 -9
  72. package/dist/templates/default/scripts/_run.mjs +3 -20
  73. package/dist/templates/default/scripts/clean.ts +81 -69
  74. package/dist/templates/default/scripts/gen-update-cert.mjs +98 -0
  75. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home)/index.tsx +21 -6
  76. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home,search)/_layout.tsx +9 -8
  77. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(search)/index.tsx +26 -24
  78. package/dist/templates/default/{app → src/app}/(app)/(tabs)/_layout.tsx +3 -4
  79. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/_layout.tsx +10 -6
  80. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/index.tsx +81 -51
  81. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/preferences.tsx +72 -12
  82. package/dist/templates/default/src/app/(app)/_layout.tsx +147 -0
  83. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/_layout.tsx +4 -5
  84. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/forgot-password.tsx +15 -9
  85. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/reset-password.tsx +88 -14
  86. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-in.tsx +65 -35
  87. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-up.tsx +131 -196
  88. package/dist/templates/default/src/app/(app)/debug.tsx +479 -0
  89. package/dist/templates/default/{app → src/app}/(app)/help.tsx +76 -64
  90. package/dist/templates/default/{app → src/app}/(app)/linked.tsx +21 -27
  91. package/dist/templates/default/{app → src/app}/(app)/privacy.tsx +35 -8
  92. package/dist/templates/default/src/app/(app)/profile/change-password.tsx +264 -0
  93. package/dist/templates/default/{app/(app)/profile.tsx → src/app/(app)/profile/index.tsx} +179 -255
  94. package/dist/templates/default/src/app/(app)/restore-account.tsx +192 -0
  95. package/dist/templates/default/src/app/(app)/sessions.tsx +287 -0
  96. package/dist/templates/default/src/app/(app)/welcome.tsx +194 -0
  97. package/dist/templates/default/src/app/+native-intent.tsx +25 -0
  98. package/dist/templates/default/src/app/+not-found.tsx +43 -0
  99. package/dist/templates/default/{app → src/app}/_layout.tsx +28 -37
  100. package/dist/templates/default/src/components/auth/apple-button.tsx +51 -0
  101. package/dist/templates/default/{components → src/components}/auth/otp-verification.tsx +79 -58
  102. package/dist/templates/default/{components → src/components}/auth/password-field.tsx +74 -18
  103. package/dist/templates/default/src/components/auth/segmented-toggle.tsx +71 -0
  104. package/dist/templates/default/src/components/ui/content-unavailable.tsx +81 -0
  105. package/dist/templates/default/src/components/ui/convex-error.tsx +21 -0
  106. package/dist/templates/default/src/components/ui/error-boundary.tsx +89 -0
  107. package/dist/templates/default/{components → src/components}/ui/loading-screen.tsx +5 -4
  108. package/dist/templates/default/{components → src/components}/ui/material.tsx +50 -17
  109. package/dist/templates/default/src/components/ui/offline-banner.tsx +59 -0
  110. package/dist/templates/default/{components → src/components}/ui/prominent-button.tsx +8 -11
  111. package/dist/templates/default/{components → src/components}/ui/skeleton.tsx +31 -13
  112. package/dist/templates/default/src/components/ui/status-text.tsx +64 -0
  113. package/dist/templates/default/src/components/ui/update-banner.tsx +85 -0
  114. package/dist/templates/default/{constants → src/constants}/layout.ts +0 -6
  115. package/dist/templates/default/{constants → src/constants}/theme.ts +49 -64
  116. package/dist/templates/default/{constants → src/constants}/ui.ts +13 -4
  117. package/dist/templates/default/src/hooks/use-debounce.ts +12 -0
  118. package/dist/templates/default/src/hooks/use-deep-link.ts +51 -0
  119. package/dist/templates/default/src/hooks/use-delete-account.ts +35 -0
  120. package/dist/templates/default/src/hooks/use-motion-screen-options.ts +13 -0
  121. package/dist/templates/default/src/hooks/use-network.ts +34 -0
  122. package/dist/templates/default/{hooks → src/hooks}/use-notifications.ts +39 -30
  123. package/dist/templates/default/src/hooks/use-reduce-transparency.ts +30 -0
  124. package/dist/templates/default/{hooks → src/hooks}/use-theme.ts +0 -5
  125. package/dist/templates/default/src/lib/appAttest.ts +78 -0
  126. package/dist/templates/default/src/lib/assets.ts +9 -0
  127. package/dist/templates/default/src/lib/deep-link.ts +82 -0
  128. package/dist/templates/default/{lib → src/lib}/dev-menu.ts +0 -4
  129. package/dist/templates/default/{lib → src/lib}/device.ts +1 -13
  130. package/dist/templates/default/{lib → src/lib}/dynamic-font.ts +13 -10
  131. package/dist/templates/default/src/lib/dynamic-symbol-size.ts +33 -0
  132. package/dist/templates/default/src/lib/masks.ts +21 -0
  133. package/dist/templates/default/src/lib/native-state.ts +20 -0
  134. package/dist/templates/default/{lib → src/lib}/notifications.ts +7 -45
  135. package/dist/templates/default/{lib → src/lib}/preferences.ts +0 -2
  136. package/dist/templates/default/{lib → src/lib}/schemas.ts +19 -16
  137. package/dist/templates/default/src/lib/text-style.ts +20 -0
  138. package/dist/templates/default/{lib → src/lib}/updates.ts +0 -7
  139. package/dist/templates/default/store.config.json +1 -1
  140. package/dist/templates/default/tsconfig.json +3 -1
  141. package/dist/templates/default/vitest.config.ts +8 -1
  142. package/package.json +5 -5
  143. package/dist/templates/default/app/(app)/_layout.tsx +0 -73
  144. package/dist/templates/default/app/(app)/debug.tsx +0 -389
  145. package/dist/templates/default/app/(app)/sessions.tsx +0 -191
  146. package/dist/templates/default/app/(app)/welcome.tsx +0 -140
  147. package/dist/templates/default/app/+native-intent.tsx +0 -14
  148. package/dist/templates/default/app/+not-found.tsx +0 -51
  149. package/dist/templates/default/bun.lock +0 -1860
  150. package/dist/templates/default/components/auth/segmented-toggle.tsx +0 -47
  151. package/dist/templates/default/components/ui/convex-error.tsx +0 -32
  152. package/dist/templates/default/components/ui/error-boundary.tsx +0 -57
  153. package/dist/templates/default/components/ui/offline-banner.tsx +0 -58
  154. package/dist/templates/default/components/ui/status-text.tsx +0 -49
  155. package/dist/templates/default/components/ui/update-banner.tsx +0 -82
  156. package/dist/templates/default/fingerprint.config.js +0 -9
  157. package/dist/templates/default/hooks/use-debounce.ts +0 -20
  158. package/dist/templates/default/hooks/use-deep-link.ts +0 -43
  159. package/dist/templates/default/hooks/use-network.ts +0 -11
  160. package/dist/templates/default/lib/assets.ts +0 -17
  161. package/dist/templates/default/lib/deep-link.ts +0 -71
  162. package/dist/templates/default/patches/PR-368.patch +0 -91
  163. package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
  164. /package/dist/templates/default/{hooks → src/hooks}/use-navigation-tracking.ts +0 -0
  165. /package/dist/templates/default/{hooks → src/hooks}/use-onboarding.ts +0 -0
  166. /package/dist/templates/default/{hooks → src/hooks}/use-reduced-motion.ts +0 -0
  167. /package/dist/templates/default/{hooks → src/hooks}/use-updates.ts +0 -0
  168. /package/dist/templates/default/{lib → src/lib}/a11y.ts +0 -0
  169. /package/dist/templates/default/{lib → src/lib}/app.ts +0 -0
  170. /package/dist/templates/default/{lib → src/lib}/auth-client.ts +0 -0
  171. /package/dist/templates/default/{lib → src/lib}/convex-auth.tsx +0 -0
  172. /package/dist/templates/default/{lib → src/lib}/env.ts +0 -0
  173. /package/dist/templates/default/{lib → src/lib}/haptics.ts +0 -0
  174. /package/dist/templates/default/{lib → src/lib}/storage.ts +0 -0
@@ -1,73 +0,0 @@
1
- import { Stack } from "expo-router";
2
-
3
- import { useColors } from "@/hooks/use-theme";
4
- import { useReducedMotion } from "@/hooks/use-reduced-motion";
5
- import { FontFamily } from "@/constants/layout";
6
- import { LoadingScreen } from "@/components/ui/loading-screen";
7
-
8
- export { AppErrorBoundary as ErrorBoundary } from "@/components/ui/error-boundary";
9
-
10
- export function SuspenseFallback() {
11
- return <LoadingScreen />;
12
- }
13
-
14
- export default function AppLayout() {
15
- const colors = useColors();
16
- const reduceMotion = useReducedMotion();
17
- const headerTint = colors.foreground as string;
18
- const titleStyle = { color: headerTint, fontFamily: FontFamily.semiBold };
19
-
20
- return (
21
- <Stack
22
- screenOptions={{
23
- headerShown: false,
24
- contentStyle: { backgroundColor: colors.background as string },
25
- headerBackTitle: "Back",
26
- headerTintColor: headerTint,
27
- headerShadowVisible: false,
28
- animation: reduceMotion ? "fade" : "slide_from_right",
29
- animationDuration: reduceMotion ? 150 : 300,
30
- }}
31
- >
32
- <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
33
- <Stack.Screen
34
- name="welcome"
35
- options={{ headerShown: false, animation: reduceMotion ? "none" : "fade" }}
36
- />
37
-
38
- <Stack.Screen name="debug">
39
- <Stack.Header transparent />
40
- <Stack.Screen.Title style={titleStyle}>Debug</Stack.Screen.Title>
41
- <Stack.Screen.BackButton withMenu>Settings</Stack.Screen.BackButton>
42
- </Stack.Screen>
43
-
44
- <Stack.Screen name="help">
45
- <Stack.Header transparent />
46
- <Stack.Screen.Title style={titleStyle}>Help</Stack.Screen.Title>
47
- <Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
48
- </Stack.Screen>
49
-
50
- <Stack.Screen name="privacy">
51
- <Stack.Header transparent />
52
- <Stack.Screen.Title style={titleStyle}>Privacy</Stack.Screen.Title>
53
- <Stack.Screen.BackButton displayMode="minimal" withMenu>
54
- Settings
55
- </Stack.Screen.BackButton>
56
- </Stack.Screen>
57
-
58
- <Stack.Screen name="linked" options={{ headerShown: true, title: "Linked" }} />
59
-
60
- <Stack.Screen name="profile" options={{ headerShown: true }}>
61
- <Stack.Header transparent />
62
- <Stack.Screen.Title style={titleStyle}>Profile</Stack.Screen.Title>
63
- <Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
64
- </Stack.Screen>
65
-
66
- <Stack.Screen name="sessions" options={{ headerShown: true }}>
67
- <Stack.Header transparent />
68
- <Stack.Screen.Title style={titleStyle}>Sessions</Stack.Screen.Title>
69
- <Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
70
- </Stack.Screen>
71
- </Stack>
72
- );
73
- }
@@ -1,389 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import * as Sharing from "expo-sharing";
3
- import { Stack } from "expo-router";
4
- import Constants from "expo-constants";
5
- import * as Application from "expo-application";
6
- import { ApplicationReleaseType } from "expo-application";
7
- import * as Device from "expo-device";
8
- import {
9
- Host,
10
- ScrollView,
11
- Button,
12
- Text,
13
- VStack,
14
- HStack,
15
- Spacer,
16
- ProgressView,
17
- } from "@expo/ui/swift-ui";
18
- import {
19
- background,
20
- buttonStyle,
21
- clipShape,
22
- cornerRadius,
23
- foregroundStyle,
24
- frame,
25
- padding,
26
- progressViewStyle,
27
- scrollDismissesKeyboard,
28
- textSelection,
29
- tint,
30
- } from "@expo/ui/swift-ui/modifiers";
31
-
32
- import { executionEnvironment, expoRuntimeVersion, sessionId, debugMode } from "@/lib/device";
33
- import { isEnabled as updatesEnabled, readLogEntries, type UpdatesLogEntry } from "@/lib/updates";
34
- import { useAppUpdates } from "@/hooks/use-updates";
35
- import { haptics } from "@/lib/haptics";
36
- import { useColors } from "@/hooks/use-theme";
37
- import { useDynamicFont } from "@/lib/dynamic-font";
38
- import { Button as ButtonTokens } from "@/constants/layout";
39
-
40
- const RELEASE_TYPE_LABELS: Record<number, string> = {
41
- [ApplicationReleaseType.UNKNOWN]: "Unknown",
42
- [ApplicationReleaseType.SIMULATOR]: "Simulator",
43
- [ApplicationReleaseType.ENTERPRISE]: "Enterprise",
44
- [ApplicationReleaseType.DEVELOPMENT]: "Development",
45
- [ApplicationReleaseType.AD_HOC]: "Ad Hoc",
46
- [ApplicationReleaseType.APP_STORE]: "App Store",
47
- };
48
-
49
- // Surface the last 5 expo-updates log entries so bsdiff patch success,
50
- // signature failures, and runtime-version mismatches are visible at
51
- // runtime. Refetches on every update lifecycle transition.
52
- function useUpdateLogEntries(isUpdatePending: boolean, restartCount: number) {
53
- const [entries, setEntries] = useState<UpdatesLogEntry[]>([]);
54
- useEffect(() => {
55
- if (!updatesEnabled) return;
56
- let cancelled = false;
57
- (async () => {
58
- try {
59
- const all = await readLogEntries();
60
- if (!cancelled) setEntries(all.slice(-5).toReversed());
61
- } catch {
62
- // expo-updates internal log is best-effort; quietly skip failures.
63
- }
64
- })();
65
- return () => {
66
- cancelled = true;
67
- };
68
- }, [isUpdatePending, restartCount]);
69
- return entries;
70
- }
71
-
72
- function useApplicationInfo() {
73
- const [installTime, setInstallTime] = useState<string | null>(null);
74
- const [iosVendorId, setIosVendorId] = useState<string | null>(null);
75
- const [iosReleaseType, setIosReleaseType] = useState<string | null>(null);
76
- const [iosPushEnv, setIosPushEnv] = useState<string | null>(null);
77
- useEffect(() => {
78
- Application.getInstallationTimeAsync()
79
- .then((date) => {
80
- if (date) setInstallTime(date.toLocaleDateString());
81
- })
82
- .catch(() => {});
83
-
84
- Application.getIosIdForVendorAsync()
85
- .then(setIosVendorId)
86
- .catch(() => {});
87
- Application.getIosApplicationReleaseTypeAsync()
88
- .then((type) => setIosReleaseType(RELEASE_TYPE_LABELS[type] ?? "Unknown"))
89
- .catch(() => {});
90
- Application.getIosPushNotificationServiceEnvironmentAsync()
91
- .then((env) => setIosPushEnv(env ?? "N/A"))
92
- .catch(() => {});
93
- }, []);
94
-
95
- return { installTime, iosVendorId, iosReleaseType, iosPushEnv };
96
- }
97
-
98
- export default function DebugScreen() {
99
- const dfont = useDynamicFont();
100
- const colors = useColors();
101
- const appInfo = useApplicationInfo();
102
- const updates = useAppUpdates();
103
- const updateLog = useUpdateLogEntries(updates.isUpdatePending, updates.restartCount);
104
-
105
- const appVersion =
106
- Application.nativeApplicationVersion ?? Constants.expoConfig?.version ?? "1.0.0";
107
- const buildNumber = Application.nativeBuildVersion ?? "1";
108
- const deviceInfo = Device.modelName
109
- ? `${Device.manufacturer ?? ""} ${Device.modelName}`.trim()
110
- : "iOS";
111
- const osVersion = Device.osVersion ? `iOS ${Device.osVersion}` : "iOS";
112
-
113
- const handleShare = async () => {
114
- haptics.light();
115
- try {
116
- const available = await Sharing.isAvailableAsync();
117
- if (!available) return;
118
- await Sharing.shareAsync(`App v${appVersion} (${buildNumber})`, {
119
- dialogTitle: "Share build info",
120
- });
121
- } catch {
122
- // share canceled
123
- }
124
- };
125
-
126
- const sectionLabelModifiers = [
127
- dfont({ size: 13, weight: "semibold" }),
128
- foregroundStyle(colors.mutedForeground as string),
129
- padding({ horizontal: 8, top: 4 }),
130
- ];
131
-
132
- const InfoRow = ({
133
- label,
134
- value,
135
- valueModifiers,
136
- valueColor,
137
- }: {
138
- label: string;
139
- value: string;
140
- valueModifiers?: Parameters<typeof Text>[0]["modifiers"];
141
- valueColor?: string;
142
- }) => (
143
- <HStack
144
- spacing={12}
145
- alignment="center"
146
- modifiers={[frame({ maxWidth: 10000 }), padding({ horizontal: 16, vertical: 12 })]}
147
- >
148
- <Text modifiers={[dfont({ size: 15 }), foregroundStyle(colors.mutedForeground as string)]}>
149
- {label}
150
- </Text>
151
- <Spacer />
152
- <Text
153
- modifiers={[
154
- dfont({ size: 15, weight: "medium" }),
155
- foregroundStyle((valueColor ?? colors.foreground) as string),
156
- textSelection(true),
157
- ...(valueModifiers ?? []),
158
- ]}
159
- >
160
- {value}
161
- </Text>
162
- </HStack>
163
- );
164
-
165
- const InfoCard = ({ children }: { children: React.ReactNode }) => (
166
- <VStack
167
- spacing={0}
168
- alignment="leading"
169
- modifiers={[frame({ maxWidth: 10000 }), background(colors.muted as string), cornerRadius(20)]}
170
- >
171
- {children}
172
- </VStack>
173
- );
174
-
175
- return (
176
- <>
177
- <Stack.Toolbar placement="right">
178
- <Stack.Toolbar.Button
179
- icon="square.and.arrow.up"
180
- onPress={handleShare}
181
- tintColor={colors.primary}
182
- accessibilityLabel="Share build info"
183
- />
184
- </Stack.Toolbar>
185
- <Host style={{ flex: 1, backgroundColor: colors.background }}>
186
- <ScrollView
187
- modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
188
- >
189
- <VStack
190
- spacing={20}
191
- alignment="leading"
192
- modifiers={[padding({ horizontal: 24, top: 24, bottom: 40 })]}
193
- >
194
- <VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
195
- <Text modifiers={sectionLabelModifiers}>BUILD</Text>
196
- <InfoCard>
197
- <InfoRow label="Version" value={`${appVersion} (${buildNumber})`} />
198
- <InfoRow label="Expo SDK" value={Constants.expoConfig?.sdkVersion ?? "Unknown"} />
199
- <InfoRow label="App name" value={Application.applicationName ?? "N/A"} />
200
- <InfoRow label="Bundle id" value={Application.applicationId ?? "N/A"} />
201
- <InfoRow label="Environment" value={executionEnvironment} />
202
- {appInfo.installTime ? (
203
- <InfoRow label="Installed" value={appInfo.installTime} />
204
- ) : null}
205
- </InfoCard>
206
- </VStack>
207
-
208
- {updatesEnabled && !__DEV__ ? (
209
- <VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
210
- <Text modifiers={sectionLabelModifiers}>OTA UPDATES</Text>
211
- <InfoCard>
212
- <InfoRow label="Status" value={updates.statusText} />
213
- <InfoRow label="Channel" value={updates.currentlyRunning.channel ?? "N/A"} />
214
- <InfoRow
215
- label="Runtime"
216
- value={updates.currentlyRunning.runtimeVersion ?? expoRuntimeVersion ?? "N/A"}
217
- />
218
- <InfoRow
219
- label="Update id"
220
- value={updates.currentlyRunning.updateId?.slice(0, 8) ?? "Embedded"}
221
- valueModifiers={[dfont({ size: 13, design: "monospaced" })]}
222
- />
223
- <InfoRow
224
- label="Created"
225
- value={updates.currentlyRunning.createdAt?.toLocaleDateString() ?? "N/A"}
226
- />
227
- <InfoRow
228
- label="Source"
229
- value={updates.currentlyRunning.isEmbeddedLaunch ? "Embedded" : "OTA Update"}
230
- />
231
- {updates.currentlyRunning.launchDuration != null ? (
232
- <InfoRow
233
- label="Launch time"
234
- value={`${updates.currentlyRunning.launchDuration}ms`}
235
- />
236
- ) : null}
237
- {updates.currentlyRunning.isEmergencyLaunch ? (
238
- <InfoRow
239
- label="Emergency launch"
240
- value={updates.currentlyRunning.emergencyLaunchReason ?? "Unknown error"}
241
- valueColor="orange"
242
- />
243
- ) : null}
244
- {updates.isDownloading ? (
245
- <HStack
246
- modifiers={[
247
- frame({ maxWidth: 10000 }),
248
- padding({ horizontal: 16, vertical: 12 }),
249
- ]}
250
- >
251
- <ProgressView
252
- value={updates.downloadProgress ?? undefined}
253
- modifiers={[progressViewStyle("linear"), frame({ maxWidth: 10000 })]}
254
- />
255
- </HStack>
256
- ) : null}
257
- {(updates.checkError ?? updates.downloadError) ? (
258
- <InfoRow
259
- label="Error"
260
- value={(updates.checkError ?? updates.downloadError)?.message ?? "Unknown"}
261
- valueColor={colors.destructive as string}
262
- />
263
- ) : null}
264
- {updates.lastCheckForUpdateTimeSinceRestart ? (
265
- <InfoRow
266
- label="Last checked"
267
- value={updates.lastCheckForUpdateTimeSinceRestart.toLocaleTimeString()}
268
- />
269
- ) : null}
270
- </InfoCard>
271
- {updates.isUpdateAvailable && !updates.isDownloading ? (
272
- <UpdateActionButton
273
- label="Download & install"
274
- onPress={updates.downloadAndApply}
275
- colors={colors}
276
- dfont={dfont}
277
- />
278
- ) : !updates.isChecking && !updates.isDownloading ? (
279
- <UpdateActionButton
280
- label="Check for updates"
281
- onPress={updates.checkForUpdate}
282
- colors={colors}
283
- dfont={dfont}
284
- />
285
- ) : null}
286
- {updateLog.length > 0 ? (
287
- <InfoCard>
288
- {updateLog.map((entry) => (
289
- <InfoRow
290
- key={`${entry.timestamp}-${entry.code}`}
291
- label={entry.level.toUpperCase()}
292
- value={`${entry.code}: ${entry.message}`}
293
- />
294
- ))}
295
- </InfoCard>
296
- ) : null}
297
- </VStack>
298
- ) : null}
299
-
300
- {appInfo.iosReleaseType || appInfo.iosPushEnv || appInfo.iosVendorId ? (
301
- <VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
302
- <Text modifiers={sectionLabelModifiers}>iOS</Text>
303
- <InfoCard>
304
- {appInfo.iosReleaseType ? (
305
- <InfoRow label="Release type" value={appInfo.iosReleaseType} />
306
- ) : null}
307
- {appInfo.iosPushEnv ? (
308
- <InfoRow label="Push env" value={appInfo.iosPushEnv} />
309
- ) : null}
310
- {appInfo.iosVendorId ? (
311
- <InfoRow
312
- label="Vendor id"
313
- value={appInfo.iosVendorId}
314
- valueModifiers={[dfont({ size: 13, design: "monospaced" })]}
315
- />
316
- ) : null}
317
- </InfoCard>
318
- </VStack>
319
- ) : null}
320
-
321
- <VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
322
- <Text modifiers={sectionLabelModifiers}>RUNTIME</Text>
323
- <InfoCard>
324
- <InfoRow
325
- label="Session id"
326
- value={sessionId.slice(0, 8)}
327
- valueModifiers={[dfont({ size: 13, design: "monospaced" })]}
328
- />
329
- <InfoRow label="Build mode" value={debugMode ? "Debug" : "Release"} />
330
- </InfoCard>
331
- </VStack>
332
-
333
- <VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
334
- <Text modifiers={sectionLabelModifiers}>DEVICE</Text>
335
- <InfoCard>
336
- <InfoRow label="Model" value={deviceInfo} />
337
- <InfoRow label="OS" value={osVersion} />
338
- </InfoCard>
339
- </VStack>
340
-
341
- <HStack modifiers={[frame({ maxWidth: 10000 }), padding({ top: 8 })]}>
342
- <Spacer />
343
- <Text
344
- modifiers={[dfont({ size: 12 }), foregroundStyle(colors.tertiaryLabel as string)]}
345
- >
346
- v{appVersion} ({buildNumber})
347
- </Text>
348
- <Spacer />
349
- </HStack>
350
- </VStack>
351
- </ScrollView>
352
- </Host>
353
- </>
354
- );
355
- }
356
-
357
- function UpdateActionButton({
358
- label,
359
- onPress,
360
- colors,
361
- dfont,
362
- }: {
363
- label: string;
364
- onPress: () => void;
365
- colors: ReturnType<typeof useColors>;
366
- dfont: ReturnType<typeof useDynamicFont>;
367
- }) {
368
- return (
369
- <Button
370
- modifiers={[
371
- buttonStyle("plain"),
372
- frame({ maxWidth: 10000 }),
373
- background(colors.muted as string),
374
- clipShape("capsule"),
375
- ]}
376
- onPress={onPress}
377
- >
378
- <Text
379
- modifiers={[
380
- frame({ maxWidth: 10000, height: ButtonTokens.height }),
381
- dfont({ size: 16, weight: "medium" }),
382
- foregroundStyle(colors.foreground as string),
383
- ]}
384
- >
385
- {label}
386
- </Text>
387
- </Button>
388
- );
389
- }
@@ -1,191 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import {
3
- Host,
4
- ScrollView,
5
- Button,
6
- Text,
7
- VStack,
8
- HStack,
9
- Spacer,
10
- ContentUnavailableView,
11
- } from "@expo/ui/swift-ui";
12
- import {
13
- background,
14
- buttonStyle,
15
- cornerRadius,
16
- foregroundStyle,
17
- frame,
18
- multilineTextAlignment,
19
- padding,
20
- textSelection,
21
- tint,
22
- } from "@expo/ui/swift-ui/modifiers";
23
-
24
- import { SkeletonSessions } from "@/components/ui/skeleton";
25
- import { useDynamicFont } from "@/lib/dynamic-font";
26
-
27
- import { authClient } from "@/lib/auth-client";
28
- import { haptics } from "@/lib/haptics";
29
- import { announce } from "@/lib/a11y";
30
- import { useColors } from "@/hooks/use-theme";
31
-
32
- type SessionRow = {
33
- id: string;
34
- token: string;
35
- ipAddress?: string | null;
36
- userAgent?: string | null;
37
- createdAt: Date;
38
- expiresAt: Date;
39
- };
40
-
41
- function formatRelative(date: Date): string {
42
- const now = Date.now();
43
- const delta = Math.max(0, now - date.getTime());
44
- const seconds = Math.floor(delta / 1000);
45
- if (seconds < 60) return `${seconds}s ago`;
46
- const minutes = Math.floor(seconds / 60);
47
- if (minutes < 60) return `${minutes}m ago`;
48
- const hours = Math.floor(minutes / 60);
49
- if (hours < 24) return `${hours}h ago`;
50
- const days = Math.floor(hours / 24);
51
- if (days < 30) return `${days}d ago`;
52
- const months = Math.floor(days / 30);
53
- if (months < 12) return `${months}mo ago`;
54
- return `${Math.floor(months / 12)}y ago`;
55
- }
56
-
57
- function deviceLabel(userAgent?: string | null): string {
58
- if (!userAgent) return "Unknown device";
59
- if (/iPhone/i.test(userAgent)) return "iPhone";
60
- if (/iPad/i.test(userAgent)) return "iPad";
61
- if (/Mac/i.test(userAgent)) return "Mac";
62
- if (/Android/i.test(userAgent)) return "Android";
63
- if (/Windows/i.test(userAgent)) return "Windows";
64
- if (/Linux/i.test(userAgent)) return "Linux";
65
- return userAgent.slice(0, 40);
66
- }
67
-
68
- export default function SessionsScreen() {
69
- const dfont = useDynamicFont();
70
- const colors = useColors();
71
- const [sessions, setSessions] = useState<SessionRow[] | null>(null);
72
- const [revoking, setRevoking] = useState<string | null>(null);
73
-
74
- const load = async () => {
75
- const res = await authClient.listSessions();
76
- if (res.error) {
77
- setSessions([]);
78
- return;
79
- }
80
- const rows = (res.data ?? []).map((s) => ({
81
- id: s.id,
82
- token: s.token,
83
- ipAddress: s.ipAddress ?? null,
84
- userAgent: s.userAgent ?? null,
85
- createdAt: new Date(s.createdAt),
86
- expiresAt: new Date(s.expiresAt),
87
- }));
88
- setSessions(rows);
89
- };
90
-
91
- useEffect(() => {
92
- load();
93
- }, []);
94
-
95
- const revoke = async (token: string) => {
96
- haptics.medium();
97
- setRevoking(token);
98
- try {
99
- await authClient.revokeSession({ token });
100
- haptics.success();
101
- announce("Session revoked");
102
- await load();
103
- } catch {
104
- haptics.error();
105
- } finally {
106
- setRevoking(null);
107
- }
108
- };
109
-
110
- return (
111
- <Host style={{ flex: 1, backgroundColor: colors.background }}>
112
- {sessions === null ? (
113
- <SkeletonSessions />
114
- ) : sessions.length === 0 ? (
115
- <ContentUnavailableView
116
- title="No active sessions"
117
- systemImage="list.bullet.rectangle.portrait"
118
- description="You have no other active sessions."
119
- />
120
- ) : (
121
- <ScrollView modifiers={[tint(colors.primary as string)]}>
122
- <VStack
123
- spacing={12}
124
- alignment="leading"
125
- modifiers={[padding({ horizontal: 24, top: 24, bottom: 40 })]}
126
- >
127
- <Text
128
- modifiers={[
129
- dfont({ size: 13, weight: "semibold" }),
130
- foregroundStyle(colors.mutedForeground as string),
131
- ]}
132
- >
133
- ACTIVE SESSIONS
134
- </Text>
135
- {sessions.map((s) => (
136
- <HStack
137
- key={s.id}
138
- spacing={12}
139
- alignment="center"
140
- modifiers={[
141
- frame({ maxWidth: 10000 }),
142
- background(colors.muted as string),
143
- cornerRadius(20),
144
- padding({ horizontal: 20, vertical: 14 }),
145
- ]}
146
- >
147
- <VStack alignment="leading" spacing={2}>
148
- <Text modifiers={[dfont({ size: 16, weight: "semibold" }), textSelection(true)]}>
149
- {deviceLabel(s.userAgent)}
150
- </Text>
151
- <Text
152
- modifiers={[
153
- dfont({ size: 13 }),
154
- foregroundStyle(colors.mutedForeground as string),
155
- textSelection(true),
156
- ]}
157
- >
158
- {s.ipAddress ?? "Unknown IP"} · {formatRelative(s.createdAt)}
159
- </Text>
160
- </VStack>
161
- <Spacer />
162
- <Button modifiers={[buttonStyle("plain")]} onPress={() => revoke(s.token)}>
163
- <Text
164
- modifiers={[
165
- dfont({ size: 14, weight: "medium" }),
166
- foregroundStyle(colors.destructive as string),
167
- ]}
168
- >
169
- Revoke
170
- </Text>
171
- </Button>
172
- </HStack>
173
- ))}
174
- {revoking ? (
175
- <Text
176
- modifiers={[
177
- dfont({ size: 13 }),
178
- foregroundStyle(colors.mutedForeground as string),
179
- multilineTextAlignment("center"),
180
- frame({ maxWidth: 10000 }),
181
- ]}
182
- >
183
- Revoking session...
184
- </Text>
185
- ) : null}
186
- </VStack>
187
- </ScrollView>
188
- )}
189
- </Host>
190
- );
191
- }