@ramonclaudio/create-vexpo 0.1.0 → 0.1.2

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,47 +0,0 @@
1
- import { Picker, Text } from "@expo/ui/swift-ui";
2
- import { controlSize, frame, pickerStyle, tag } from "@expo/ui/swift-ui/modifiers";
3
-
4
- import { useDynamicFont } from "@/lib/dynamic-font";
5
- import { Button as ButtonTokens } from "@/constants/layout";
6
- import { haptics } from "@/lib/haptics";
7
-
8
- // Native iOS segmented control via @expo/ui's SwiftUI Picker. Mirrors the
9
- // affordance of tanvex's web SegmentedToggle (Sign in/Sign up, Email/Username
10
- // /Email OTP) but renders as the platform-native control on iOS.
11
-
12
- export type SegmentedOption<T extends string> = {
13
- value: T;
14
- label: string;
15
- };
16
-
17
- type Props<T extends string> = {
18
- value: T;
19
- options: SegmentedOption<T>[];
20
- onChange: (value: T) => void;
21
- };
22
-
23
- export function SegmentedToggle<T extends string>({ value, options, onChange }: Props<T>) {
24
- const dfont = useDynamicFont();
25
- return (
26
- <Picker
27
- modifiers={[
28
- pickerStyle("segmented"),
29
- controlSize("large"),
30
- frame({ maxWidth: 10000, height: ButtonTokens.height }),
31
- ]}
32
- selection={value}
33
- onSelectionChange={(selection) => {
34
- const next = selection as T;
35
- if (next === value) return;
36
- haptics.light();
37
- onChange(next);
38
- }}
39
- >
40
- {options.map((opt) => (
41
- <Text key={opt.value} modifiers={[tag(opt.value), dfont({ size: 14, weight: "medium" })]}>
42
- {opt.label}
43
- </Text>
44
- ))}
45
- </Picker>
46
- );
47
- }
@@ -1,32 +0,0 @@
1
- import { ConvexError } from "convex/values";
2
-
3
- import { ErrorText } from "./status-text";
4
-
5
- /**
6
- * Pull a human-readable message out of an unknown thrown value. Knows about
7
- * `ConvexError`'s structured `data` payload (`{ code, message, field? }` from
8
- * `convex/errors.ts`) so server-side validation errors and auth failures
9
- * surface their original message instead of a stringified object.
10
- */
11
- export function formatError(err: unknown): string {
12
- if (err instanceof ConvexError) {
13
- const data = err.data as unknown;
14
- if (typeof data === "object" && data !== null && "message" in data) {
15
- const msg = (data as { message?: unknown }).message;
16
- if (typeof msg === "string" && msg.length > 0) return msg;
17
- }
18
- return err.message;
19
- }
20
- if (err instanceof Error) return err.message;
21
- return "An unexpected error occurred";
22
- }
23
-
24
- /**
25
- * Render an unknown thrown value through `ErrorText`. Returns null when there
26
- * is no error so call sites can do `<ConvexErrorView error={state.error} />`
27
- * unconditionally.
28
- */
29
- export function ConvexErrorView({ error }: { error: unknown }) {
30
- if (error === undefined || error === null) return null;
31
- return <ErrorText>{formatError(error)}</ErrorText>;
32
- }
@@ -1,57 +0,0 @@
1
- import { router, type ErrorBoundaryProps } from "expo-router";
2
- import { Host, VStack, Text, Button, Image, Spacer } from "@expo/ui/swift-ui";
3
- import {
4
- foregroundStyle,
5
- buttonStyle,
6
- frame,
7
- padding,
8
- multilineTextAlignment,
9
- tint,
10
- } from "@expo/ui/swift-ui/modifiers";
11
- import { useDynamicFont } from "@/lib/dynamic-font";
12
- import { ProminentButton } from "@/components/ui/prominent-button";
13
- import { useColors } from "@/hooks/use-theme";
14
-
15
- export function AppErrorBoundary({ error, retry }: ErrorBoundaryProps) {
16
- const dfont = useDynamicFont();
17
- const colors = useColors();
18
- console.error("[ErrorBoundary]", error);
19
-
20
- return (
21
- <Host style={{ flex: 1 }}>
22
- <VStack
23
- spacing={20}
24
- alignment="center"
25
- modifiers={[padding({ horizontal: 24, vertical: 32 }), tint(colors.primary as string)]}
26
- >
27
- <Spacer />
28
- <Image
29
- systemName="exclamationmark.triangle"
30
- size={72}
31
- color={colors.destructive as string}
32
- />
33
- <Text modifiers={[dfont({ size: 28, weight: "bold" }), multilineTextAlignment("center")]}>
34
- Something went wrong
35
- </Text>
36
- <Text
37
- modifiers={[
38
- dfont({ size: 16 }),
39
- foregroundStyle(colors.mutedForeground as string),
40
- multilineTextAlignment("center"),
41
- ]}
42
- >
43
- Don&apos;t worry. Let&apos;s get you back on track.
44
- </Text>
45
- <VStack spacing={12} modifiers={[frame({ maxWidth: Infinity })]}>
46
- <ProminentButton label="Try Again" onPress={retry} />
47
- <Button
48
- label="Go Home"
49
- modifiers={[buttonStyle("plain"), foregroundStyle(colors.mutedForeground as string)]}
50
- onPress={() => router.replace("/")}
51
- />
52
- </VStack>
53
- <Spacer />
54
- </VStack>
55
- </Host>
56
- );
57
- }
@@ -1,58 +0,0 @@
1
- import { Text, View } from "react-native";
2
- import { useSafeAreaInsets } from "react-native-safe-area-context";
3
-
4
- import { Material } from "@/components/ui/material";
5
- import { useNetwork } from "@/hooks/use-network";
6
- import { Spacing, FontSize, FontFamily } from "@/constants/layout";
7
- import { Radius } from "@/constants/theme";
8
- import { ZIndex } from "@/constants/ui";
9
- import { useColors } from "@/hooks/use-theme";
10
-
11
- // HIG: notification banners overlay the navigation layer with a translucent
12
- // material so context behind the alert remains visible.
13
- export function OfflineBanner() {
14
- const { isOffline } = useNetwork();
15
- const insets = useSafeAreaInsets();
16
- const colors = useColors();
17
-
18
- if (!isOffline) return null;
19
-
20
- return (
21
- <View
22
- accessibilityLiveRegion="assertive"
23
- accessibilityRole="alert"
24
- style={{
25
- position: "absolute",
26
- top: 0,
27
- left: 0,
28
- right: 0,
29
- zIndex: ZIndex.offlineBanner,
30
- paddingTop: insets.top,
31
- }}
32
- >
33
- <Material
34
- variant="chrome"
35
- tintColor={colors.destructive as string}
36
- style={{
37
- marginHorizontal: Spacing.md,
38
- marginTop: Spacing.xs,
39
- borderRadius: Radius.full,
40
- overflow: "hidden",
41
- paddingVertical: Spacing.sm,
42
- paddingHorizontal: Spacing.lg,
43
- alignItems: "center",
44
- }}
45
- >
46
- <Text
47
- style={{
48
- fontSize: FontSize.md,
49
- fontFamily: FontFamily.semiBold,
50
- color: colors.destructiveForeground as string,
51
- }}
52
- >
53
- You&apos;re offline
54
- </Text>
55
- </Material>
56
- </View>
57
- );
58
- }
@@ -1,49 +0,0 @@
1
- import { useEffect } from "react";
2
- import { AccessibilityInfo } from "react-native";
3
- import { HStack, Image, Text } from "@expo/ui/swift-ui";
4
- import { foregroundStyle } from "@expo/ui/swift-ui/modifiers";
5
-
6
- import { useDynamicFont } from "@/lib/dynamic-font";
7
- import { Colors } from "@/constants/theme";
8
-
9
- type Props = { children: string; size?: number };
10
-
11
- function announce(prefix: string, message: string) {
12
- AccessibilityInfo.announceForAccessibility(`${prefix}: ${message}`);
13
- }
14
-
15
- export function ErrorText({ children, size = 14 }: Props) {
16
- const dfont = useDynamicFont();
17
- useEffect(() => {
18
- announce("Error", children);
19
- }, [children]);
20
-
21
- return (
22
- <HStack spacing={6} alignment="center">
23
- <Image
24
- systemName="exclamationmark.triangle.fill"
25
- size={size}
26
- color={Colors.destructive as string}
27
- />
28
- <Text modifiers={[dfont({ size }), foregroundStyle(Colors.destructive as string)]}>
29
- {children}
30
- </Text>
31
- </HStack>
32
- );
33
- }
34
-
35
- export function SuccessText({ children, size = 14 }: Props) {
36
- const dfont = useDynamicFont();
37
- useEffect(() => {
38
- announce("Success", children);
39
- }, [children]);
40
-
41
- return (
42
- <HStack spacing={6} alignment="center">
43
- <Image systemName="checkmark.circle.fill" size={size} color={Colors.success as string} />
44
- <Text modifiers={[dfont({ size }), foregroundStyle(Colors.success as string)]}>
45
- {children}
46
- </Text>
47
- </HStack>
48
- );
49
- }
@@ -1,82 +0,0 @@
1
- import { Pressable, Text, View } from "react-native";
2
- import { useSafeAreaInsets } from "react-native-safe-area-context";
3
-
4
- import { Material } from "@/components/ui/material";
5
- import { useAppUpdates } from "@/hooks/use-updates";
6
- import { Spacing, FontSize, FontFamily } from "@/constants/layout";
7
- import { Radius } from "@/constants/theme";
8
- import { ZIndex } from "@/constants/ui";
9
- import { useColors } from "@/hooks/use-theme";
10
-
11
- // In-app surface for the EAS Update lifecycle. Mirrors the OfflineBanner
12
- // pattern (translucent material overlaying the nav layer per HIG) and is
13
- // only visible while the update state machine is doing something the user
14
- // would want to see:
15
- //
16
- // - downloading progress %, no tap target. auto-applies on finish
17
- // - download failed tap to retry
18
- // - check failed silent unless the user previously asked for an update
19
- //
20
- // `isUpdatePending` (downloaded, awaiting reload) is handled by the
21
- // `useAppUpdates` hook (auto-reload with reload screen), so we don't
22
- // surface it here. the splash-screen overlay does the visual work.
23
- export function UpdateBanner() {
24
- const updates = useAppUpdates();
25
- const insets = useSafeAreaInsets();
26
- const colors = useColors();
27
-
28
- const showProgress = updates.isDownloading;
29
- const showError = !!updates.downloadError;
30
- if (!showProgress && !showError) return null;
31
-
32
- const tint = showError ? (colors.destructive as string) : (colors.primary as string);
33
- const fg = showError
34
- ? (colors.destructiveForeground as string)
35
- : (colors.primaryForeground as string);
36
- const pct =
37
- showProgress && updates.downloadProgress != null
38
- ? ` ${Math.round(updates.downloadProgress * 100)}%`
39
- : "";
40
- const label = showError ? "Update failed. Tap to retry." : `Updating${pct}`;
41
-
42
- return (
43
- <View
44
- accessibilityLiveRegion="polite"
45
- accessibilityRole="alert"
46
- style={{
47
- position: "absolute",
48
- bottom: 0,
49
- left: 0,
50
- right: 0,
51
- zIndex: ZIndex.updateBanner,
52
- paddingBottom: insets.bottom,
53
- }}
54
- >
55
- <Pressable
56
- accessibilityRole="button"
57
- accessibilityLabel={label}
58
- accessibilityHint={showError ? "Re-attempts the update download" : undefined}
59
- disabled={!showError}
60
- onPress={showError ? () => updates.downloadAndApply() : undefined}
61
- >
62
- <Material
63
- variant="chrome"
64
- tintColor={tint}
65
- style={{
66
- marginHorizontal: Spacing.md,
67
- marginBottom: Spacing.xs,
68
- borderRadius: Radius.full,
69
- overflow: "hidden",
70
- paddingVertical: Spacing.sm,
71
- paddingHorizontal: Spacing.lg,
72
- alignItems: "center",
73
- }}
74
- >
75
- <Text style={{ fontSize: FontSize.md, fontFamily: FontFamily.semiBold, color: fg }}>
76
- {label}
77
- </Text>
78
- </Material>
79
- </Pressable>
80
- </View>
81
- );
82
- }
@@ -1,9 +0,0 @@
1
- /** @type {import('expo/fingerprint').Config} */
2
- const config = {
3
- sourceSkips: [
4
- "ExpoConfigVersions",
5
- "ExpoConfigRuntimeVersionIfString",
6
- "PackageJsonAndroidAndIosScriptsIfNotContainRun",
7
- ],
8
- };
9
- module.exports = config;
@@ -1,20 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- /**
4
- * Returns a value that lags the input by `delay` milliseconds. Useful for
5
- * coalescing rapid changes (typing, scroll, etc.) before kicking off a more
6
- * expensive operation downstream (filtering, fetching, rendering a big list).
7
- *
8
- * Each input change resets the timer, so the returned value only updates
9
- * after `delay` ms have passed without further changes.
10
- */
11
- export function useDebounce<T>(value: T, delay: number): T {
12
- const [debounced, setDebounced] = useState(value);
13
-
14
- useEffect(() => {
15
- const id = setTimeout(() => setDebounced(value), delay);
16
- return () => clearTimeout(id);
17
- }, [value, delay]);
18
-
19
- return debounced;
20
- }
@@ -1,43 +0,0 @@
1
- import { useEffect } from "react";
2
- import { useURL } from "expo-linking";
3
- import { router, type Href } from "expo-router";
4
-
5
- import { authClient } from "@/lib/auth-client";
6
- import { resolveDeepLink } from "@/lib/deep-link";
7
-
8
- const ROUTES: Record<string, Href> = {
9
- "/linked": "/linked" as Href,
10
- };
11
-
12
- /**
13
- * Listens for deep link URLs and pushes to the matching route.
14
- *
15
- * Only runs once authenticated. Invalid or disallowed links are ignored.
16
- * Query params are forwarded as route params.
17
- */
18
- export function useDeepLinkHandler() {
19
- // See note in app/_layout.tsx: Better Auth session is the canonical signal.
20
- // `useConvexAuth` is unreliable due to the bridge's sessionId churn.
21
- const { data: session } = authClient.useSession();
22
- const isAuthenticated = !!session?.session;
23
- const url = useURL();
24
-
25
- useEffect(() => {
26
- if (!isAuthenticated || !url) return;
27
-
28
- let resolved;
29
- try {
30
- resolved = resolveDeepLink(url);
31
- } catch (err) {
32
- if (__DEV__) console.warn("[DeepLink] parse failed:", err);
33
- return;
34
- }
35
-
36
- if (!resolved.path) return;
37
-
38
- const target = ROUTES[resolved.path];
39
- if (!target) return;
40
-
41
- router.push({ pathname: target, params: resolved.params } as Href);
42
- }, [isAuthenticated, url]);
43
- }
@@ -1,11 +0,0 @@
1
- import { useNetworkState } from "expo-network";
2
-
3
- export function useNetwork() {
4
- const { isConnected, isInternetReachable } = useNetworkState();
5
-
6
- return {
7
- isConnected,
8
- isInternetReachable,
9
- isOffline: isConnected === false || isInternetReachable === false,
10
- };
11
- }
@@ -1,17 +0,0 @@
1
- // Runtime asset registry. The five files referenced below live in ./assets/.
2
- // To rebrand, replace the PNGs in place with your own renders at the same
3
- // dimensions and file names. See DESIGN.md for the surface specs.
4
- //
5
- // Surfaces:
6
- // icon iOS bundle icon, 1024x1024 (iOS rounds the corners)
7
- // brandIcon* in-app chiclet (welcome, sign-in, sign-up, loading)
8
- // splash* expo-splash-screen image, sits on configured bg color
9
- export const assets = {
10
- icon: require("@/assets/icon.png"),
11
- brandIconLight: require("@/assets/brand-icon-light.png"),
12
- brandIconDark: require("@/assets/brand-icon-dark.png"),
13
- splashLight: require("@/assets/splash-image-light.png"),
14
- splashDark: require("@/assets/splash-image-dark.png"),
15
- } as const;
16
-
17
- export const assetModules = Object.values(assets);
@@ -1,71 +0,0 @@
1
- import { parse, createURL } from "expo-linking";
2
-
3
- export const ALLOWED_DEEP_LINK_PATHS = [
4
- "/",
5
- "/welcome",
6
- "/settings",
7
- "/about",
8
- "/help",
9
- "/privacy",
10
- "/sign-in",
11
- "/sign-up",
12
- "/forgot-password",
13
- "/reset-password",
14
- "/linked",
15
- ] as const;
16
-
17
- export function isValidDeepLink(url: string): boolean {
18
- if (!url || typeof url !== "string") return false;
19
- if (url.includes("..")) return false;
20
-
21
- const { scheme, path } = parse(url);
22
- const isRelativePath = url.startsWith("/") && !url.startsWith("//");
23
- if (!isRelativePath && !scheme) return false;
24
-
25
- const normalizedPath = "/" + (path ?? "").replace(/^\//, "");
26
-
27
- return ALLOWED_DEEP_LINK_PATHS.some(
28
- (allowed) => normalizedPath === allowed || normalizedPath.startsWith(allowed + "/"),
29
- );
30
- }
31
-
32
- export type ResolvedDeepLink = {
33
- path: string | null;
34
- params: Record<string, string>;
35
- };
36
-
37
- /**
38
- * Pure helper that parses a deep link URL into `{ path, params }`.
39
- *
40
- * Returns `path: null` for invalid URLs, disallowed paths, or traversal attempts.
41
- * Array query values are joined with commas; nullish values are dropped.
42
- * Unit-testable: no React, no side effects.
43
- */
44
- export function resolveDeepLink(url: string): ResolvedDeepLink {
45
- const empty: ResolvedDeepLink = { path: null, params: {} };
46
- if (!url || typeof url !== "string") return empty;
47
-
48
- let parsed;
49
- try {
50
- parsed = parse(url);
51
- } catch {
52
- return empty;
53
- }
54
-
55
- if (!isValidDeepLink(url)) return empty;
56
-
57
- const normalizedPath = "/" + (parsed.path ?? "").replace(/^\//, "").replace(/\/+$/, "");
58
- const path = normalizedPath === "/" ? "/" : normalizedPath;
59
-
60
- const params: Record<string, string> = {};
61
- if (parsed.queryParams) {
62
- for (const [key, value] of Object.entries(parsed.queryParams)) {
63
- if (value == null) continue;
64
- params[key] = Array.isArray(value) ? value.join(",") : value;
65
- }
66
- }
67
-
68
- return { path, params };
69
- }
70
-
71
- export { createURL };
@@ -1,91 +0,0 @@
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.
File without changes
File without changes
File without changes