@ramonclaudio/create-vexpo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +183 -0
  3. package/dist/templates/default/.eas/workflows/asc-events.yml +84 -0
  4. package/dist/templates/default/.eas/workflows/deploy-production.yml +129 -0
  5. package/dist/templates/default/.eas/workflows/development-builds.yml +19 -0
  6. package/dist/templates/default/.eas/workflows/e2e-tests.yml +42 -0
  7. package/dist/templates/default/.eas/workflows/pr-preview.yml +98 -0
  8. package/dist/templates/default/.eas/workflows/release.yml +44 -0
  9. package/dist/templates/default/.eas/workflows/rollback.yml +86 -0
  10. package/dist/templates/default/.eas/workflows/rollout.yml +84 -0
  11. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +42 -0
  12. package/dist/templates/default/.eas/workflows/testflight.yml +57 -0
  13. package/dist/templates/default/.github/workflows/check.yml +28 -0
  14. package/dist/templates/default/.maestro/launch.yaml +18 -0
  15. package/dist/templates/default/AGENTS.md +79 -0
  16. package/dist/templates/default/DESIGN.md +331 -0
  17. package/dist/templates/default/LICENSE +21 -0
  18. package/dist/templates/default/README.md +153 -0
  19. package/dist/templates/default/SETUP.md +618 -0
  20. package/dist/templates/default/__tests__/convex/constants.test.ts +49 -0
  21. package/dist/templates/default/__tests__/convex/validators.test.ts +23 -0
  22. package/dist/templates/default/__tests__/convex/webhook.test.ts +343 -0
  23. package/dist/templates/default/__tests__/lib/deep-link.test.ts +67 -0
  24. package/dist/templates/default/_easignore +22 -0
  25. package/dist/templates/default/_editorconfig +9 -0
  26. package/dist/templates/default/_env.example +34 -0
  27. package/dist/templates/default/_fingerprintignore +24 -0
  28. package/dist/templates/default/_gitattributes +7 -0
  29. package/dist/templates/default/_gitignore +69 -0
  30. package/dist/templates/default/_oxfmtrc.json +3 -0
  31. package/dist/templates/default/_oxlintrc.json +34 -0
  32. package/dist/templates/default/app/(app)/(tabs)/(home)/index.tsx +50 -0
  33. package/dist/templates/default/app/(app)/(tabs)/(home,search)/_layout.tsx +44 -0
  34. package/dist/templates/default/app/(app)/(tabs)/(search)/index.tsx +247 -0
  35. package/dist/templates/default/app/(app)/(tabs)/_layout.tsx +77 -0
  36. package/dist/templates/default/app/(app)/(tabs)/settings/_layout.tsx +37 -0
  37. package/dist/templates/default/app/(app)/(tabs)/settings/index.tsx +362 -0
  38. package/dist/templates/default/app/(app)/(tabs)/settings/preferences.tsx +184 -0
  39. package/dist/templates/default/app/(app)/_layout.tsx +73 -0
  40. package/dist/templates/default/app/(app)/debug.tsx +389 -0
  41. package/dist/templates/default/app/(app)/help.tsx +254 -0
  42. package/dist/templates/default/app/(app)/linked.tsx +116 -0
  43. package/dist/templates/default/app/(app)/privacy.tsx +159 -0
  44. package/dist/templates/default/app/(app)/profile.tsx +915 -0
  45. package/dist/templates/default/app/(app)/sessions.tsx +191 -0
  46. package/dist/templates/default/app/(app)/welcome.tsx +140 -0
  47. package/dist/templates/default/app/(auth)/_layout.tsx +31 -0
  48. package/dist/templates/default/app/(auth)/forgot-password.tsx +168 -0
  49. package/dist/templates/default/app/(auth)/reset-password.tsx +314 -0
  50. package/dist/templates/default/app/(auth)/sign-in.tsx +453 -0
  51. package/dist/templates/default/app/(auth)/sign-up.tsx +563 -0
  52. package/dist/templates/default/app/+native-intent.tsx +14 -0
  53. package/dist/templates/default/app/+not-found.tsx +51 -0
  54. package/dist/templates/default/app/_layout.tsx +102 -0
  55. package/dist/templates/default/app-store/screenshots/.gitkeep +0 -0
  56. package/dist/templates/default/app-store/screenshots/README.md +13 -0
  57. package/dist/templates/default/app.config.ts +201 -0
  58. package/dist/templates/default/app.json +11 -0
  59. package/dist/templates/default/assets/brand-icon-dark.png +0 -0
  60. package/dist/templates/default/assets/brand-icon-light.png +0 -0
  61. package/dist/templates/default/assets/fonts/Geist-Black.ttf +0 -0
  62. package/dist/templates/default/assets/fonts/Geist-BlackItalic.ttf +0 -0
  63. package/dist/templates/default/assets/fonts/Geist-Bold.ttf +0 -0
  64. package/dist/templates/default/assets/fonts/Geist-BoldItalic.ttf +0 -0
  65. package/dist/templates/default/assets/fonts/Geist-ExtraBold.ttf +0 -0
  66. package/dist/templates/default/assets/fonts/Geist-ExtraBoldItalic.ttf +0 -0
  67. package/dist/templates/default/assets/fonts/Geist-ExtraLight.ttf +0 -0
  68. package/dist/templates/default/assets/fonts/Geist-ExtraLightItalic.ttf +0 -0
  69. package/dist/templates/default/assets/fonts/Geist-Italic.ttf +0 -0
  70. package/dist/templates/default/assets/fonts/Geist-Light.ttf +0 -0
  71. package/dist/templates/default/assets/fonts/Geist-LightItalic.ttf +0 -0
  72. package/dist/templates/default/assets/fonts/Geist-Medium.ttf +0 -0
  73. package/dist/templates/default/assets/fonts/Geist-MediumItalic.ttf +0 -0
  74. package/dist/templates/default/assets/fonts/Geist-Regular.ttf +0 -0
  75. package/dist/templates/default/assets/fonts/Geist-SemiBold.ttf +0 -0
  76. package/dist/templates/default/assets/fonts/Geist-SemiBoldItalic.ttf +0 -0
  77. package/dist/templates/default/assets/fonts/Geist-Thin.ttf +0 -0
  78. package/dist/templates/default/assets/fonts/Geist-ThinItalic.ttf +0 -0
  79. package/dist/templates/default/assets/fonts/Geist-Variable-Italic.ttf +0 -0
  80. package/dist/templates/default/assets/fonts/Geist-Variable.ttf +0 -0
  81. package/dist/templates/default/assets/fonts/GeistMono-Bold.ttf +0 -0
  82. package/dist/templates/default/assets/fonts/GeistMono-BoldItalic.ttf +0 -0
  83. package/dist/templates/default/assets/fonts/GeistMono-Italic.ttf +0 -0
  84. package/dist/templates/default/assets/fonts/GeistMono-Medium.ttf +0 -0
  85. package/dist/templates/default/assets/fonts/GeistMono-MediumItalic.ttf +0 -0
  86. package/dist/templates/default/assets/fonts/GeistMono-Regular.ttf +0 -0
  87. package/dist/templates/default/assets/fonts/GeistPixel-Square.ttf +0 -0
  88. package/dist/templates/default/assets/icon.png +0 -0
  89. package/dist/templates/default/assets/sounds/notification.wav +0 -0
  90. package/dist/templates/default/assets/splash-image-dark.png +0 -0
  91. package/dist/templates/default/assets/splash-image-light.png +0 -0
  92. package/dist/templates/default/bun.lock +1860 -0
  93. package/dist/templates/default/components/auth/otp-verification.tsx +255 -0
  94. package/dist/templates/default/components/auth/password-field.tsx +121 -0
  95. package/dist/templates/default/components/auth/segmented-toggle.tsx +47 -0
  96. package/dist/templates/default/components/ui/convex-error.tsx +32 -0
  97. package/dist/templates/default/components/ui/error-boundary.tsx +57 -0
  98. package/dist/templates/default/components/ui/loading-screen.tsx +31 -0
  99. package/dist/templates/default/components/ui/material.tsx +94 -0
  100. package/dist/templates/default/components/ui/offline-banner.tsx +58 -0
  101. package/dist/templates/default/components/ui/prominent-button.tsx +71 -0
  102. package/dist/templates/default/components/ui/skeleton.tsx +107 -0
  103. package/dist/templates/default/components/ui/status-text.tsx +49 -0
  104. package/dist/templates/default/components/ui/update-banner.tsx +82 -0
  105. package/dist/templates/default/constants/layout.ts +102 -0
  106. package/dist/templates/default/constants/theme.ts +401 -0
  107. package/dist/templates/default/constants/ui.ts +77 -0
  108. package/dist/templates/default/convex/_generated/api.d.ts +77 -0
  109. package/dist/templates/default/convex/_generated/api.js +23 -0
  110. package/dist/templates/default/convex/_generated/dataModel.d.ts +60 -0
  111. package/dist/templates/default/convex/_generated/server.d.ts +143 -0
  112. package/dist/templates/default/convex/_generated/server.js +93 -0
  113. package/dist/templates/default/convex/admin.ts +102 -0
  114. package/dist/templates/default/convex/auth.config.ts +6 -0
  115. package/dist/templates/default/convex/auth.ts +335 -0
  116. package/dist/templates/default/convex/constants.ts +46 -0
  117. package/dist/templates/default/convex/convex.config.ts +11 -0
  118. package/dist/templates/default/convex/crons.ts +42 -0
  119. package/dist/templates/default/convex/email.ts +109 -0
  120. package/dist/templates/default/convex/env.ts +31 -0
  121. package/dist/templates/default/convex/errors.ts +33 -0
  122. package/dist/templates/default/convex/functions.ts +54 -0
  123. package/dist/templates/default/convex/http.ts +176 -0
  124. package/dist/templates/default/convex/log.ts +81 -0
  125. package/dist/templates/default/convex/pushTokens.ts +114 -0
  126. package/dist/templates/default/convex/rateLimit.ts +92 -0
  127. package/dist/templates/default/convex/schema.ts +28 -0
  128. package/dist/templates/default/convex/tsconfig.json +18 -0
  129. package/dist/templates/default/convex/users.ts +279 -0
  130. package/dist/templates/default/convex/validators.ts +74 -0
  131. package/dist/templates/default/convex/webhook.ts +193 -0
  132. package/dist/templates/default/convex.json +6 -0
  133. package/dist/templates/default/eas.json +56 -0
  134. package/dist/templates/default/fingerprint.config.js +9 -0
  135. package/dist/templates/default/hooks/use-debounce.ts +20 -0
  136. package/dist/templates/default/hooks/use-deep-link.ts +43 -0
  137. package/dist/templates/default/hooks/use-navigation-tracking.ts +15 -0
  138. package/dist/templates/default/hooks/use-network.ts +11 -0
  139. package/dist/templates/default/hooks/use-notifications.ts +107 -0
  140. package/dist/templates/default/hooks/use-onboarding.ts +15 -0
  141. package/dist/templates/default/hooks/use-reduced-motion.ts +11 -0
  142. package/dist/templates/default/hooks/use-theme.ts +53 -0
  143. package/dist/templates/default/hooks/use-updates.ts +86 -0
  144. package/dist/templates/default/lib/a11y.ts +5 -0
  145. package/dist/templates/default/lib/app.ts +14 -0
  146. package/dist/templates/default/lib/assets.ts +17 -0
  147. package/dist/templates/default/lib/auth-client.ts +21 -0
  148. package/dist/templates/default/lib/convex-auth.tsx +79 -0
  149. package/dist/templates/default/lib/deep-link.ts +71 -0
  150. package/dist/templates/default/lib/dev-menu.ts +119 -0
  151. package/dist/templates/default/lib/device.ts +40 -0
  152. package/dist/templates/default/lib/dynamic-font.ts +49 -0
  153. package/dist/templates/default/lib/env.ts +10 -0
  154. package/dist/templates/default/lib/haptics.ts +24 -0
  155. package/dist/templates/default/lib/notifications.ts +276 -0
  156. package/dist/templates/default/lib/preferences.ts +45 -0
  157. package/dist/templates/default/lib/schemas.ts +137 -0
  158. package/dist/templates/default/lib/storage.ts +47 -0
  159. package/dist/templates/default/lib/updates.ts +107 -0
  160. package/dist/templates/default/metro.config.js +14 -0
  161. package/dist/templates/default/package.json +129 -0
  162. package/dist/templates/default/patches/PR-368.patch +91 -0
  163. package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
  164. package/dist/templates/default/plugins/README.md +9 -0
  165. package/dist/templates/default/plugins/with-auto-signing.js +45 -0
  166. package/dist/templates/default/plugins/with-pod-deployment-target.js +35 -0
  167. package/dist/templates/default/scripts/README.md +36 -0
  168. package/dist/templates/default/scripts/_run.mjs +77 -0
  169. package/dist/templates/default/scripts/clean.ts +543 -0
  170. package/dist/templates/default/scripts/rotate-apple-jwt.mjs +80 -0
  171. package/dist/templates/default/store.config.json +58 -0
  172. package/dist/templates/default/tsconfig.json +13 -0
  173. package/dist/templates/default/vitest.config.ts +21 -0
  174. package/package.json +69 -0
@@ -0,0 +1,45 @@
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ import { createStorage } from "@/lib/storage";
4
+
5
+ export type ReduceMotionPref = "system" | "always" | "never";
6
+
7
+ const hapticsStore = createStorage<boolean>("pref.hapticsEnabled", true);
8
+ const reduceMotionStore = createStorage<ReduceMotionPref>("pref.reduceMotion", "system");
9
+ // Default on in dev, off in production. Reveals the Debug screen with version,
10
+ // device, OTA update, and push diagnostics.
11
+ const debugEnabledStore = createStorage<boolean>("pref.debugEnabled", __DEV__);
12
+
13
+ export const preferences = {
14
+ hapticsEnabled: () => hapticsStore.get(),
15
+ setHapticsEnabled: (v: boolean) => hapticsStore.set(v),
16
+
17
+ reduceMotion: () => reduceMotionStore.get(),
18
+ setReduceMotion: (v: ReduceMotionPref) => reduceMotionStore.set(v),
19
+
20
+ debugEnabled: () => debugEnabledStore.get(),
21
+ setDebugEnabled: (v: boolean) => debugEnabledStore.set(v),
22
+ };
23
+
24
+ export function useHapticsEnabled(): [boolean, (v: boolean) => void] {
25
+ const v = useSyncExternalStore(hapticsStore.subscribe, hapticsStore.get, hapticsStore.get);
26
+ return [v, hapticsStore.set];
27
+ }
28
+
29
+ export function useReduceMotionPref(): [ReduceMotionPref, (v: ReduceMotionPref) => void] {
30
+ const v = useSyncExternalStore(
31
+ reduceMotionStore.subscribe,
32
+ reduceMotionStore.get,
33
+ reduceMotionStore.get,
34
+ );
35
+ return [v, reduceMotionStore.set];
36
+ }
37
+
38
+ export function useDebugEnabled(): [boolean, (v: boolean) => void] {
39
+ const v = useSyncExternalStore(
40
+ debugEnabledStore.subscribe,
41
+ debugEnabledStore.get,
42
+ debugEnabledStore.get,
43
+ );
44
+ return [v, debugEnabledStore.set];
45
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Form validation schemas.
3
+ *
4
+ * Each form parses raw input via `schema.safeParse(values)` inside
5
+ * `useActionState`. Errors flatten to inline `Section.footer` text under each
6
+ * field. Constants and reserved-name helpers come from `@/convex/constants`
7
+ * to keep client/server in sync.
8
+ */
9
+
10
+ import { z } from "zod";
11
+
12
+ import {
13
+ USERNAME_FORMAT_REGEX,
14
+ USERNAME_MAX_LENGTH,
15
+ USERNAME_MIN_LENGTH,
16
+ isReservedUsername,
17
+ } from "@/convex/constants";
18
+
19
+ export const PASSWORD_MIN_LENGTH = 10;
20
+ export const PASSWORD_MAX_LENGTH = 128;
21
+
22
+ const usernameSchema = z
23
+ .string()
24
+ .trim()
25
+ .toLowerCase()
26
+ .min(USERNAME_MIN_LENGTH, {
27
+ error: `Username must be at least ${USERNAME_MIN_LENGTH} characters`,
28
+ })
29
+ .max(USERNAME_MAX_LENGTH, {
30
+ error: `Username must be ${USERNAME_MAX_LENGTH} characters or fewer`,
31
+ })
32
+ .regex(USERNAME_FORMAT_REGEX, { error: "Letters, numbers, dots, and underscores only" })
33
+ .refine((value) => !isReservedUsername(value), { error: "That username is reserved" });
34
+
35
+ // Optional variant used at sign-up: empty string is valid (the user can pick
36
+ // a handle later from the profile screen). Format and reserved checks only
37
+ // apply when the user actually typed something.
38
+ const optionalUsernameSchema = z
39
+ .string()
40
+ .trim()
41
+ .toLowerCase()
42
+ .refine((value) => value === "" || value.length >= USERNAME_MIN_LENGTH, {
43
+ error: `Username must be at least ${USERNAME_MIN_LENGTH} characters`,
44
+ })
45
+ .refine((value) => value === "" || value.length <= USERNAME_MAX_LENGTH, {
46
+ error: `Username must be ${USERNAME_MAX_LENGTH} characters or fewer`,
47
+ })
48
+ .refine((value) => value === "" || USERNAME_FORMAT_REGEX.test(value), {
49
+ error: "Letters, numbers, dots, and underscores only",
50
+ })
51
+ .refine((value) => value === "" || !isReservedUsername(value), {
52
+ error: "That username is reserved",
53
+ });
54
+
55
+ const passwordSchema = z
56
+ .string()
57
+ .min(PASSWORD_MIN_LENGTH, {
58
+ error: `Password must be at least ${PASSWORD_MIN_LENGTH} characters`,
59
+ })
60
+ .max(PASSWORD_MAX_LENGTH, {
61
+ error: `Password must be ${PASSWORD_MAX_LENGTH} characters or fewer`,
62
+ });
63
+
64
+ const emailSchema = z.string().trim().toLowerCase().email({ error: "Enter a valid email address" });
65
+
66
+ const nameSchema = z.string().trim().min(1, { error: "Name is required" });
67
+
68
+ const otpSchema = z.string().regex(/^\d{6}$/, { error: "Enter the 6-digit code" });
69
+
70
+ export const signInSchema = z.object({
71
+ identifier: z.string().trim().min(1, { error: "Username or email is required" }),
72
+ password: z.string().min(1, { error: "Password is required" }),
73
+ });
74
+
75
+ export const signInEmailSchema = z.object({
76
+ email: emailSchema,
77
+ password: z.string().min(1, { error: "Password is required" }),
78
+ });
79
+
80
+ export const signInUsernameSchema = z.object({
81
+ username: z
82
+ .string()
83
+ .trim()
84
+ .toLowerCase()
85
+ .min(USERNAME_MIN_LENGTH, {
86
+ error: `Username must be at least ${USERNAME_MIN_LENGTH} characters`,
87
+ }),
88
+ password: z.string().min(1, { error: "Password is required" }),
89
+ });
90
+
91
+ export const signUpSchema = z.object({
92
+ name: nameSchema,
93
+ username: optionalUsernameSchema,
94
+ email: emailSchema,
95
+ password: passwordSchema,
96
+ });
97
+
98
+ export const forgotPasswordSchema = z.object({
99
+ email: emailSchema,
100
+ });
101
+
102
+ export const resetPasswordSchema = z
103
+ .object({
104
+ email: emailSchema,
105
+ otp: otpSchema,
106
+ password: passwordSchema,
107
+ confirmPassword: z.string(),
108
+ })
109
+ .refine((data) => data.password === data.confirmPassword, {
110
+ error: "Passwords do not match",
111
+ path: ["confirmPassword"],
112
+ });
113
+
114
+ export const profileUpdateSchema = z.object({
115
+ name: nameSchema,
116
+ username: usernameSchema,
117
+ email: emailSchema,
118
+ });
119
+
120
+ export type SignInValues = z.infer<typeof signInSchema>;
121
+ export type SignInEmailValues = z.infer<typeof signInEmailSchema>;
122
+ export type SignInUsernameValues = z.infer<typeof signInUsernameSchema>;
123
+ export type SignUpValues = z.infer<typeof signUpSchema>;
124
+ export type ForgotPasswordValues = z.infer<typeof forgotPasswordSchema>;
125
+ export type ResetPasswordValues = z.infer<typeof resetPasswordSchema>;
126
+ export type ProfileUpdateValues = z.infer<typeof profileUpdateSchema>;
127
+
128
+ /**
129
+ * Extract the first error message from a zod safeParse result, formatted for
130
+ * inline display. Returns `null` if validation succeeded.
131
+ */
132
+ export function firstError(
133
+ result: { success: false; error: z.ZodError } | { success: true; data: unknown },
134
+ ): string | null {
135
+ if (result.success) return null;
136
+ return result.error.issues[0]?.message ?? "Invalid input";
137
+ }
@@ -0,0 +1,47 @@
1
+ import "expo-sqlite/localStorage/install";
2
+
3
+ type Listener = () => void;
4
+
5
+ const listeners = new Map<string, Set<Listener>>();
6
+
7
+ function notify(key: string) {
8
+ listeners.get(key)?.forEach((fn) => fn());
9
+ }
10
+
11
+ function read<T>(key: string, defaultValue: T): T {
12
+ const raw = localStorage.getItem(key);
13
+ if (raw === null) return defaultValue;
14
+ try {
15
+ return JSON.parse(raw) as T;
16
+ } catch {
17
+ return defaultValue;
18
+ }
19
+ }
20
+
21
+ export type Storage<T> = {
22
+ get: () => T;
23
+ set: (value: T) => void;
24
+ subscribe: (listener: Listener) => () => void;
25
+ };
26
+
27
+ export function createStorage<T>(key: string, defaultValue: T): Storage<T> {
28
+ return {
29
+ get: () => read(key, defaultValue),
30
+ set: (value: T) => {
31
+ localStorage.setItem(key, JSON.stringify(value));
32
+ notify(key);
33
+ },
34
+ subscribe: (listener: Listener) => {
35
+ let set = listeners.get(key);
36
+ if (!set) {
37
+ set = new Set();
38
+ listeners.set(key, set);
39
+ }
40
+ set.add(listener);
41
+ return () => {
42
+ set!.delete(listener);
43
+ if (set!.size === 0) listeners.delete(key);
44
+ };
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,107 @@
1
+ import {
2
+ // Constants
3
+ isEnabled,
4
+ updateId,
5
+ channel,
6
+ runtimeVersion,
7
+ checkAutomatically,
8
+ isEmergencyLaunch,
9
+ emergencyLaunchReason,
10
+ isEmbeddedLaunch,
11
+ manifest,
12
+ createdAt,
13
+ launchDuration,
14
+
15
+ // Methods
16
+ checkForUpdateAsync,
17
+ fetchUpdateAsync,
18
+ reloadAsync,
19
+ readLogEntriesAsync,
20
+ clearLogEntriesAsync,
21
+ getExtraParamsAsync,
22
+ setExtraParamAsync,
23
+ setUpdateRequestHeadersOverride,
24
+ setUpdateURLAndRequestHeadersOverride,
25
+ showReloadScreen,
26
+ hideReloadScreen,
27
+
28
+ // Enums (runtime values)
29
+ UpdateCheckResultNotAvailableReason,
30
+ UpdatesLogEntryCode,
31
+ UpdatesLogEntryLevel,
32
+ UpdatesCheckAutomaticallyValue,
33
+ UpdateInfoType,
34
+
35
+ // Hook
36
+ useUpdates,
37
+ } from "expo-updates";
38
+
39
+ import type { ReloadScreenOptions } from "expo-updates";
40
+
41
+ export type {
42
+ ReloadScreenOptions,
43
+ ReloadScreenImageSource,
44
+ Manifest,
45
+ UpdateCheckResult,
46
+ UpdateCheckResultAvailable,
47
+ UpdateCheckResultNotAvailable,
48
+ UpdateCheckResultRollBack,
49
+ UpdateFetchResult,
50
+ UpdateFetchResultSuccess,
51
+ UpdateFetchResultFailure,
52
+ UpdateFetchResultRollBackToEmbedded,
53
+ UpdatesLogEntry,
54
+ CurrentlyRunningInfo,
55
+ UpdateInfo,
56
+ UpdateInfoNew,
57
+ UpdateInfoRollback,
58
+ UseUpdatesReturnType,
59
+ } from "expo-updates";
60
+
61
+ export {
62
+ UpdateCheckResultNotAvailableReason,
63
+ UpdatesLogEntryCode,
64
+ UpdatesLogEntryLevel,
65
+ UpdatesCheckAutomaticallyValue,
66
+ UpdateInfoType,
67
+ };
68
+
69
+ export { useUpdates };
70
+
71
+ export {
72
+ isEnabled,
73
+ updateId,
74
+ channel,
75
+ runtimeVersion,
76
+ checkAutomatically,
77
+ isEmergencyLaunch,
78
+ emergencyLaunchReason,
79
+ isEmbeddedLaunch,
80
+ manifest,
81
+ createdAt,
82
+ launchDuration,
83
+ };
84
+
85
+ export function buildReloadScreenConfig(
86
+ scheme: "light" | "dark",
87
+ reduceMotion = false,
88
+ ): ReloadScreenOptions {
89
+ const dark = scheme === "dark";
90
+ return {
91
+ backgroundColor: dark ? "#0E0E0E" : "#FFFFFF",
92
+ fade: !reduceMotion,
93
+ spinner: { color: dark ? "#FFFFFF" : "#0E0E0E", enabled: true, size: "medium" },
94
+ };
95
+ }
96
+
97
+ export const checkForUpdate = isEnabled ? checkForUpdateAsync : async () => {};
98
+ export const fetchUpdate = isEnabled ? fetchUpdateAsync : async () => {};
99
+ export const reload = reloadAsync;
100
+ export const readLogEntries = readLogEntriesAsync;
101
+ export const clearLogEntries = clearLogEntriesAsync;
102
+ export const getExtraParams = getExtraParamsAsync;
103
+ export const setExtraParam = setExtraParamAsync;
104
+ export const setRequestHeadersOverride = setUpdateRequestHeadersOverride;
105
+ export const setURLAndHeadersOverride = setUpdateURLAndRequestHeadersOverride;
106
+
107
+ export { showReloadScreen, hideReloadScreen };
@@ -0,0 +1,14 @@
1
+ const { getDefaultConfig } = require("expo/metro-config");
2
+
3
+ /** @type {import('expo/metro-config').MetroConfig} */
4
+ const config = getDefaultConfig(__dirname);
5
+
6
+ config.resolver.blockList = [/\.env\.convex\.local$/];
7
+
8
+ config.transformer.minifierConfig = {
9
+ compress: {
10
+ drop_console: ["log", "info"],
11
+ },
12
+ };
13
+
14
+ module.exports = config;
@@ -0,0 +1,129 @@
1
+ {
2
+ "name": "vexpo",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "main": "expo-router/entry",
6
+ "scripts": {
7
+ "dev": "expo start --dev-client",
8
+ "start": "expo start --dev-client --clear",
9
+ "convex:dev": "convex dev",
10
+ "convex:deploy": "convex deploy",
11
+ "convex:logs": "convex logs",
12
+ "convex:logs:prod": "convex logs --prod",
13
+ "convex:env": "convex env list",
14
+ "convex:env:prod": "convex env list --prod",
15
+ "convex:insights": "convex insights",
16
+ "convex:insights:prod": "convex insights --prod",
17
+ "convex:dashboard": "convex dashboard",
18
+ "convex:codegen": "convex codegen",
19
+ "ios": "node scripts/_run.mjs scripts/clean.ts --metro && expo prebuild --clean --platform ios && expo run:ios --no-build-cache",
20
+ "ios:dev": "expo run:ios",
21
+ "ios:device": "node scripts/_run.mjs scripts/clean.ts --metro && expo prebuild --clean --platform ios && expo run:ios --device --no-build-cache",
22
+ "prebuild": "expo prebuild --clean --platform ios",
23
+ "eas:dev": "bunx eas build -p ios --profile development:simulator",
24
+ "eas:dev:device": "bunx eas build -p ios --profile development:device",
25
+ "eas:tf": "bunx eas build -p ios --profile production --auto-submit-with-profile testflight",
26
+ "eas:prod": "bunx eas build -p ios --profile production",
27
+ "metadata:lint": "bunx eas metadata:lint",
28
+ "metadata:push": "bunx eas metadata:lint && bunx eas metadata:push",
29
+ "metadata:pull": "bunx eas metadata:pull",
30
+ "env:pull": "bunx eas env:pull --environment development",
31
+ "env:pull:prod": "bunx eas env:pull --environment production",
32
+ "clean": "node scripts/_run.mjs scripts/clean.ts",
33
+ "clean:metro": "node scripts/_run.mjs scripts/clean.ts --metro",
34
+ "clean:state": "node scripts/_run.mjs scripts/clean.ts --state",
35
+ "typecheck": "tsc --noEmit",
36
+ "fp": "bunx @expo/fingerprint fingerprint:generate",
37
+ "fp:diff": "bunx @expo/fingerprint fingerprint:diff",
38
+ "lint": "oxlint",
39
+ "format": "oxfmt",
40
+ "format:check": "oxfmt --check",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "upgrade": "bunx expo install expo@canary && bunx expo install --fix",
44
+ "upgrade:stable": "bunx expo install expo@latest && bunx expo install --fix"
45
+ },
46
+ "dependencies": {
47
+ "@better-auth/expo": "1.6.9",
48
+ "@convex-dev/better-auth": "file:./patches/convex-dev-better-auth-0.12.2.tgz",
49
+ "@convex-dev/rate-limiter": "^0.3.2",
50
+ "@convex-dev/resend": "^0.2.3",
51
+ "@expo/ui": "56.0.0-canary-20260506-03817f5",
52
+ "@react-navigation/native": "7.2.3",
53
+ "better-auth": "1.6.9",
54
+ "convex": "^1.37.0",
55
+ "convex-helpers": "^0.1.116",
56
+ "expo": "56.0.0-canary-20260506-03817f5",
57
+ "expo-apple-authentication": "56.0.0-canary-20260506-03817f5",
58
+ "expo-application": "56.0.0-canary-20260506-03817f5",
59
+ "expo-asset": "56.0.0-canary-20260506-03817f5",
60
+ "expo-blur": "56.0.0-canary-20260506-03817f5",
61
+ "expo-build-properties": "56.0.0-canary-20260506-03817f5",
62
+ "expo-clipboard": "56.0.0-canary-20260506-03817f5",
63
+ "expo-constants": "56.0.0-canary-20260506-03817f5",
64
+ "expo-dev-client": "56.0.0-canary-20260506-03817f5",
65
+ "expo-device": "56.0.0-canary-20260506-03817f5",
66
+ "expo-font": "56.0.0-canary-20260506-03817f5",
67
+ "expo-glass-effect": "56.0.0-canary-20260506-03817f5",
68
+ "expo-haptics": "56.0.0-canary-20260506-03817f5",
69
+ "expo-image": "56.0.0-canary-20260506-03817f5",
70
+ "expo-image-picker": "56.0.0-canary-20260506-03817f5",
71
+ "expo-insights": "56.0.0-canary-20260506-03817f5",
72
+ "expo-linking": "56.0.0-canary-20260506-03817f5",
73
+ "expo-local-authentication": "56.0.0-canary-20260506-03817f5",
74
+ "expo-network": "56.0.0-canary-20260506-03817f5",
75
+ "expo-notifications": "56.0.0-canary-20260506-03817f5",
76
+ "expo-router": "56.0.0-canary-20260506-03817f5",
77
+ "expo-secure-store": "56.0.0-canary-20260506-03817f5",
78
+ "expo-sharing": "56.0.0-canary-20260506-03817f5",
79
+ "expo-splash-screen": "56.0.0-canary-20260506-03817f5",
80
+ "expo-sqlite": "56.0.0-canary-20260506-03817f5",
81
+ "expo-status-bar": "56.0.0-canary-20260506-03817f5",
82
+ "expo-symbols": "56.0.0-canary-20260506-03817f5",
83
+ "expo-system-ui": "56.0.0-canary-20260506-03817f5",
84
+ "expo-task-manager": "56.0.0-canary-20260506-03817f5",
85
+ "expo-updates": "56.0.0-canary-20260506-03817f5",
86
+ "expo-web-browser": "56.0.0-canary-20260506-03817f5",
87
+ "react": "19.2.3",
88
+ "react-native": "0.85.3",
89
+ "react-native-gesture-handler": "~2.31.2",
90
+ "react-native-keyboard-controller": "1.21.6",
91
+ "react-native-reanimated": "4.3.0",
92
+ "react-native-safe-area-context": "~5.7.0",
93
+ "react-native-screens": "4.25.0-beta.1",
94
+ "react-native-worklets": "0.8.3",
95
+ "zod": "^4.4.3"
96
+ },
97
+ "devDependencies": {
98
+ "@ramonclaudio/vexpo": "^0.1.0",
99
+ "@types/node": "^25.6.0",
100
+ "@types/react": "~19.2.14",
101
+ "@vitest/ui": "^4.1.5",
102
+ "convex-test": "^0.0.51",
103
+ "oxfmt": "^0.48.0",
104
+ "oxlint": "^1.63.0",
105
+ "tsx": "^4.21.0",
106
+ "typescript": "^6.0.3",
107
+ "vitest": "^4.1.5"
108
+ },
109
+ "overrides": {
110
+ "@better-auth/passkey": "1.6.9",
111
+ "@expo/ui": "56.0.0-canary-20260506-03817f5"
112
+ },
113
+ "expo": {
114
+ "install": {
115
+ "exclude": [
116
+ "react-native-reanimated"
117
+ ]
118
+ },
119
+ "doctor": {
120
+ "reactNativeDirectoryCheck": {
121
+ "enabled": true,
122
+ "listUnknownPackages": true,
123
+ "exclude": [
124
+ "expo-modules-jsi"
125
+ ]
126
+ }
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,91 @@
1
+ From 53c46f0ac639231af3e8ac975d36851658f4e0b9 Mon Sep 17 00:00:00 2001
2
+ From: Ray <hello@ramonclaudio.com>
3
+ Date: Thu, 7 May 2026 19:07:36 -0400
4
+ Subject: [PATCH] fix(react): wrap fetchAccessToken in new Promise to fix
5
+ useConvexAuth on Hermes V1
6
+
7
+ The /convex/token response triggers a session rotation (via Better
8
+ Auth's Set-Cookie processing) plus a setCachedToken call inside the
9
+ bridge's .then. The next render rebuilds fetchAccessToken's
10
+ useCallback (keyed on [sessionId]) and fires
11
+ ConvexAuthStateFirstEffect's client.setAuth a second time.
12
+
13
+ On Hermes V1 native async (Expo SDK 56 canary 2026-05-05+ since
14
+ expo/expo#45345 dropped @babel/plugin-transform-async-to-generator),
15
+ that second setAuth lands inside the first setConfig's await window
16
+ in authentication_manager.ts. fetchTokenAndGuardAgainstRace bumps
17
+ configVersion on entry and the original await sees the stale value,
18
+ returning isFromOutdatedConfig: true. setConfig bails without
19
+ resumeSocket() and the chain repeats.
20
+
21
+ Drop the async keyword and wrap the body in new Promise(executor)
22
+ directly. The constructor's resolve(thenable) schedules a
23
+ NewPromiseResolveThenableJob microtask, the same hop regenerator's
24
+ _asyncToGenerator provides. With the hop in place the second setAuth
25
+ lands after the first setConfig finishes rather than during its
26
+ await window.
27
+ ---
28
+ src/react/index.tsx | 48 ++++++++++++++++++++++++---------------------
29
+ 1 file changed, 26 insertions(+), 22 deletions(-)
30
+
31
+ diff --git a/src/react/index.tsx b/src/react/index.tsx
32
+ index dc10909a..2f906878 100644
33
+ --- a/src/react/index.tsx
34
+ +++ b/src/react/index.tsx
35
+ @@ -127,30 +127,34 @@ function useUseAuthFromBetterAuth(
36
+ }
37
+ }, [session, isSessionPending]);
38
+ const fetchAccessToken = useCallback(
39
+ - async ({
40
+ + ({
41
+ forceRefreshToken = false,
42
+ }: { forceRefreshToken?: boolean } = {}) => {
43
+ - if (cachedToken && !forceRefreshToken) {
44
+ - return cachedToken;
45
+ - }
46
+ - if (!forceRefreshToken && pendingTokenRef.current) {
47
+ - return pendingTokenRef.current;
48
+ - }
49
+ - pendingTokenRef.current = authClient.convex
50
+ - .token({ fetchOptions: { throw: false } })
51
+ - .then(({ data }) => {
52
+ - const token = data?.token || null;
53
+ - setCachedToken(token);
54
+ - return token;
55
+ - })
56
+ - .catch(() => {
57
+ - setCachedToken(null);
58
+ - return null;
59
+ - })
60
+ - .finally(() => {
61
+ - pendingTokenRef.current = null;
62
+ - });
63
+ - return pendingTokenRef.current;
64
+ + return new Promise<string | null>((resolve, reject) => {
65
+ + if (cachedToken && !forceRefreshToken) {
66
+ + resolve(cachedToken);
67
+ + return;
68
+ + }
69
+ + if (!forceRefreshToken && pendingTokenRef.current) {
70
+ + pendingTokenRef.current.then(resolve, reject);
71
+ + return;
72
+ + }
73
+ + pendingTokenRef.current = authClient.convex
74
+ + .token({ fetchOptions: { throw: false } })
75
+ + .then(({ data }) => {
76
+ + const token = data?.token || null;
77
+ + setCachedToken(token);
78
+ + return token;
79
+ + })
80
+ + .catch(() => {
81
+ + setCachedToken(null);
82
+ + return null;
83
+ + })
84
+ + .finally(() => {
85
+ + pendingTokenRef.current = null;
86
+ + });
87
+ + pendingTokenRef.current.then(resolve, reject);
88
+ + });
89
+ },
90
+ // Build a new fetchAccessToken to trigger setAuth() whenever the
91
+ // session changes.
@@ -0,0 +1,9 @@
1
+ # plugins
2
+
3
+ ## `with-auto-signing.js`
4
+
5
+ Forces automatic code signing for the Xcode project during local `prebuild`. Sets `CODE_SIGN_STYLE = Automatic` on every build configuration with a `PRODUCT_BUNDLE_IDENTIFIER`, drops any leftover `PROVISIONING_PROFILE*` keys, and sets `DEVELOPMENT_TEAM` from `ios.appleTeamId`.
6
+
7
+ No-ops when `EAS_BUILD` is set, so EAS continues to use the provisioning profile from the build credentials.
8
+
9
+ Use: local `bun run ios` on a physical device without juggling provisioning profiles in Xcode.
@@ -0,0 +1,45 @@
1
+ const { withXcodeProject } = require("@expo/config-plugins");
2
+
3
+ const withAutoSigning = (config) => {
4
+ if (process.env.EAS_BUILD) return config;
5
+
6
+ return withXcodeProject(config, async (cfg) => {
7
+ const xcodeProject = cfg.modResults;
8
+
9
+ const targetName = xcodeProject.getFirstTarget()?.firstTarget?.name;
10
+ if (!targetName) {
11
+ console.warn("[with-auto-signing] Could not find main target");
12
+ return cfg;
13
+ }
14
+
15
+ const buildConfigurations = xcodeProject.pbxXCBuildConfigurationSection();
16
+ for (const key in buildConfigurations) {
17
+ const buildConfig = buildConfigurations[key];
18
+ if (buildConfig.buildSettings && buildConfig.buildSettings.PRODUCT_BUNDLE_IDENTIFIER) {
19
+ buildConfig.buildSettings.CODE_SIGN_STYLE = "Automatic";
20
+ if (cfg.ios?.appleTeamId) {
21
+ buildConfig.buildSettings.DEVELOPMENT_TEAM = cfg.ios.appleTeamId;
22
+ }
23
+ delete buildConfig.buildSettings.PROVISIONING_PROFILE;
24
+ delete buildConfig.buildSettings.PROVISIONING_PROFILE_SPECIFIER;
25
+ }
26
+ }
27
+
28
+ const projectSection = xcodeProject.pbxProjectSection();
29
+ for (const key in projectSection) {
30
+ const project = projectSection[key];
31
+ if (project.attributes && project.attributes.TargetAttributes) {
32
+ for (const targetId in project.attributes.TargetAttributes) {
33
+ project.attributes.TargetAttributes[targetId].ProvisioningStyle = "Automatic";
34
+ if (cfg.ios?.appleTeamId) {
35
+ project.attributes.TargetAttributes[targetId].DevelopmentTeam = cfg.ios.appleTeamId;
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ return cfg;
42
+ });
43
+ };
44
+
45
+ module.exports = withAutoSigning;
@@ -0,0 +1,35 @@
1
+ const { withDangerousMod } = require("@expo/config-plugins");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ const MARKER = "# with-pod-deployment-target";
6
+
7
+ const buildInjection = (target) => `
8
+ ${MARKER}
9
+ installer.pods_project.targets.each do |t|
10
+ t.build_configurations.each do |c|
11
+ c.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '${target}'
12
+ end
13
+ end
14
+ `;
15
+
16
+ const withPodDeploymentTarget = (config, { target = "16.4" } = {}) =>
17
+ withDangerousMod(config, [
18
+ "ios",
19
+ (cfg) => {
20
+ const podfilePath = path.join(cfg.modRequest.platformProjectRoot, "Podfile");
21
+ const podfile = fs.readFileSync(podfilePath, "utf8");
22
+ if (podfile.includes(MARKER)) return cfg;
23
+
24
+ const injection = buildInjection(target);
25
+ const re = /(react_native_post_install\([\s\S]*?\n\s*\)\n)/;
26
+ if (!re.test(podfile)) {
27
+ throw new Error("with-pod-deployment-target: react_native_post_install call not found");
28
+ }
29
+ const next = podfile.replace(re, `$1${injection}`);
30
+ fs.writeFileSync(podfilePath, next);
31
+ return cfg;
32
+ },
33
+ ]);
34
+
35
+ module.exports = withPodDeploymentTarget;