@ramonclaudio/create-vexpo 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/dist/index.js +8 -7
- package/dist/templates/default/.eas/workflows/asc-events.yml +9 -6
- package/dist/templates/default/.eas/workflows/deploy-production.yml +28 -15
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +3 -2
- package/dist/templates/default/.eas/workflows/pr-preview.yml +12 -21
- package/dist/templates/default/.eas/workflows/release.yml +3 -7
- package/dist/templates/default/.eas/workflows/rollback.yml +54 -28
- package/dist/templates/default/.eas/workflows/rollout.yml +27 -33
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +1 -5
- package/dist/templates/default/.eas/workflows/testflight.yml +3 -7
- package/dist/templates/default/.github/workflows/check.yml +20 -12
- package/dist/templates/default/.maestro/launch.yaml +19 -10
- package/dist/templates/default/AGENTS.md +25 -8
- package/dist/templates/default/DESIGN.md +14 -10
- package/dist/templates/default/README.md +83 -78
- package/dist/templates/default/SETUP.md +159 -152
- package/dist/templates/default/__tests__/convex/_auth-harness.test.ts +112 -0
- package/dist/templates/default/__tests__/convex/_harness.ts +132 -0
- package/dist/templates/default/__tests__/convex/appAttest.test.ts +172 -0
- package/dist/templates/default/__tests__/convex/appAttestStore.test.ts +48 -0
- package/dist/templates/default/__tests__/convex/pushTokens-remove.test.ts +106 -0
- package/dist/templates/default/__tests__/convex/pushTokens-upsert.test.ts +146 -0
- package/dist/templates/default/__tests__/convex/users-deleteAccount.test.ts +140 -0
- package/dist/templates/default/__tests__/convex/users-deleteAvatar.test.ts +104 -0
- package/dist/templates/default/__tests__/convex/users-getMe.test.ts +98 -0
- package/dist/templates/default/__tests__/convex/users-getUser.test.ts +120 -0
- package/dist/templates/default/__tests__/convex/users-hardDeleteExpired.test.ts +67 -0
- package/dist/templates/default/__tests__/convex/users-restoreAccount.test.ts +96 -0
- package/dist/templates/default/__tests__/convex/users-updateAvatar.test.ts +92 -0
- package/dist/templates/default/__tests__/convex/users-updateProfile.test.ts +126 -0
- package/dist/templates/default/__tests__/convex/webhook.test.ts +31 -0
- package/dist/templates/default/__tests__/lib/deep-link.test.ts +51 -6
- package/dist/templates/default/__tests__/lib/schemas.test.ts +205 -0
- package/dist/templates/default/__tests__/lib/text-style.test.ts +31 -0
- package/dist/templates/default/_env.example +7 -7
- package/dist/templates/default/_gitattributes +1 -1
- package/dist/templates/default/_gitignore +17 -2
- package/dist/templates/default/_npmrc +7 -0
- package/dist/templates/default/_oxlintrc.json +1 -1
- package/dist/templates/default/app-store/accessibility.config.json +20 -0
- package/dist/templates/default/app-store/privacy.config.json +27 -0
- package/dist/templates/default/app.config.ts +105 -33
- package/dist/templates/default/app.json +1 -9
- package/dist/templates/default/convex/_generated/api.d.ts +12 -0
- package/dist/templates/default/convex/admin.ts +0 -13
- package/dist/templates/default/convex/appAttest.ts +467 -0
- package/dist/templates/default/convex/appAttestStore.ts +141 -0
- package/dist/templates/default/convex/apple.ts +53 -0
- package/dist/templates/default/convex/auth.ts +6 -45
- package/dist/templates/default/convex/constants.ts +2 -7
- package/dist/templates/default/convex/crons.ts +12 -5
- package/dist/templates/default/convex/email.ts +4 -24
- package/dist/templates/default/convex/env.ts +0 -4
- package/dist/templates/default/convex/errors.ts +0 -7
- package/dist/templates/default/convex/functions.ts +0 -26
- package/dist/templates/default/convex/http.ts +3 -5
- package/dist/templates/default/convex/log.ts +2 -25
- package/dist/templates/default/convex/pushSender.ts +145 -0
- package/dist/templates/default/convex/pushTokens.ts +110 -13
- package/dist/templates/default/convex/rateLimit.ts +8 -39
- package/dist/templates/default/convex/schema.ts +48 -5
- package/dist/templates/default/convex/tsconfig.json +1 -0
- package/dist/templates/default/convex/users.ts +143 -61
- package/dist/templates/default/convex/validators.ts +1 -38
- package/dist/templates/default/convex/webhook.ts +1 -31
- package/dist/templates/default/convex.json +1 -2
- package/dist/templates/default/metro.config.js +9 -1
- package/dist/templates/default/package.json +67 -70
- package/dist/templates/default/plugins/README.md +5 -1
- package/dist/templates/default/scripts/README.md +9 -9
- package/dist/templates/default/scripts/_run.mjs +3 -20
- package/dist/templates/default/scripts/clean.ts +81 -69
- package/dist/templates/default/scripts/gen-update-cert.mjs +98 -0
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home)/index.tsx +21 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home,search)/_layout.tsx +9 -8
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(search)/index.tsx +26 -24
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/_layout.tsx +3 -4
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/_layout.tsx +10 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/index.tsx +81 -51
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/preferences.tsx +72 -12
- package/dist/templates/default/src/app/(app)/_layout.tsx +147 -0
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/_layout.tsx +4 -5
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/forgot-password.tsx +15 -9
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/reset-password.tsx +88 -14
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-in.tsx +65 -35
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-up.tsx +131 -196
- package/dist/templates/default/src/app/(app)/debug.tsx +479 -0
- package/dist/templates/default/{app → src/app}/(app)/help.tsx +76 -64
- package/dist/templates/default/{app → src/app}/(app)/linked.tsx +21 -27
- package/dist/templates/default/{app → src/app}/(app)/privacy.tsx +35 -8
- package/dist/templates/default/src/app/(app)/profile/change-password.tsx +264 -0
- package/dist/templates/default/{app/(app)/profile.tsx → src/app/(app)/profile/index.tsx} +179 -255
- package/dist/templates/default/src/app/(app)/restore-account.tsx +192 -0
- package/dist/templates/default/src/app/(app)/sessions.tsx +287 -0
- package/dist/templates/default/src/app/(app)/welcome.tsx +194 -0
- package/dist/templates/default/src/app/+native-intent.tsx +25 -0
- package/dist/templates/default/src/app/+not-found.tsx +43 -0
- package/dist/templates/default/{app → src/app}/_layout.tsx +28 -37
- package/dist/templates/default/src/components/auth/apple-button.tsx +51 -0
- package/dist/templates/default/{components → src/components}/auth/otp-verification.tsx +79 -58
- package/dist/templates/default/{components → src/components}/auth/password-field.tsx +74 -18
- package/dist/templates/default/src/components/auth/segmented-toggle.tsx +71 -0
- package/dist/templates/default/src/components/ui/content-unavailable.tsx +81 -0
- package/dist/templates/default/src/components/ui/convex-error.tsx +21 -0
- package/dist/templates/default/src/components/ui/error-boundary.tsx +89 -0
- package/dist/templates/default/{components → src/components}/ui/loading-screen.tsx +5 -4
- package/dist/templates/default/{components → src/components}/ui/material.tsx +50 -17
- package/dist/templates/default/src/components/ui/offline-banner.tsx +59 -0
- package/dist/templates/default/{components → src/components}/ui/prominent-button.tsx +8 -11
- package/dist/templates/default/{components → src/components}/ui/skeleton.tsx +31 -13
- package/dist/templates/default/src/components/ui/status-text.tsx +64 -0
- package/dist/templates/default/src/components/ui/update-banner.tsx +85 -0
- package/dist/templates/default/{constants → src/constants}/layout.ts +0 -6
- package/dist/templates/default/{constants → src/constants}/theme.ts +49 -64
- package/dist/templates/default/{constants → src/constants}/ui.ts +13 -4
- package/dist/templates/default/src/hooks/use-debounce.ts +12 -0
- package/dist/templates/default/src/hooks/use-deep-link.ts +51 -0
- package/dist/templates/default/src/hooks/use-delete-account.ts +35 -0
- package/dist/templates/default/src/hooks/use-motion-screen-options.ts +13 -0
- package/dist/templates/default/src/hooks/use-network.ts +34 -0
- package/dist/templates/default/{hooks → src/hooks}/use-notifications.ts +39 -30
- package/dist/templates/default/src/hooks/use-reduce-transparency.ts +30 -0
- package/dist/templates/default/{hooks → src/hooks}/use-theme.ts +0 -5
- package/dist/templates/default/src/lib/appAttest.ts +78 -0
- package/dist/templates/default/src/lib/assets.ts +9 -0
- package/dist/templates/default/src/lib/deep-link.ts +82 -0
- package/dist/templates/default/{lib → src/lib}/dev-menu.ts +0 -4
- package/dist/templates/default/{lib → src/lib}/device.ts +1 -13
- package/dist/templates/default/{lib → src/lib}/dynamic-font.ts +13 -10
- package/dist/templates/default/src/lib/dynamic-symbol-size.ts +33 -0
- package/dist/templates/default/src/lib/masks.ts +21 -0
- package/dist/templates/default/src/lib/native-state.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/notifications.ts +7 -45
- package/dist/templates/default/{lib → src/lib}/preferences.ts +0 -2
- package/dist/templates/default/{lib → src/lib}/schemas.ts +19 -16
- package/dist/templates/default/src/lib/text-style.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/updates.ts +0 -7
- package/dist/templates/default/store.config.json +1 -1
- package/dist/templates/default/tsconfig.json +3 -1
- package/dist/templates/default/vitest.config.ts +8 -1
- package/package.json +5 -5
- package/dist/templates/default/app/(app)/_layout.tsx +0 -73
- package/dist/templates/default/app/(app)/debug.tsx +0 -389
- package/dist/templates/default/app/(app)/sessions.tsx +0 -191
- package/dist/templates/default/app/(app)/welcome.tsx +0 -140
- package/dist/templates/default/app/+native-intent.tsx +0 -14
- package/dist/templates/default/app/+not-found.tsx +0 -51
- package/dist/templates/default/bun.lock +0 -1860
- package/dist/templates/default/components/auth/segmented-toggle.tsx +0 -47
- package/dist/templates/default/components/ui/convex-error.tsx +0 -32
- package/dist/templates/default/components/ui/error-boundary.tsx +0 -57
- package/dist/templates/default/components/ui/offline-banner.tsx +0 -58
- package/dist/templates/default/components/ui/status-text.tsx +0 -49
- package/dist/templates/default/components/ui/update-banner.tsx +0 -82
- package/dist/templates/default/fingerprint.config.js +0 -9
- package/dist/templates/default/hooks/use-debounce.ts +0 -20
- package/dist/templates/default/hooks/use-deep-link.ts +0 -43
- package/dist/templates/default/hooks/use-network.ts +0 -11
- package/dist/templates/default/lib/assets.ts +0 -17
- package/dist/templates/default/lib/deep-link.ts +0 -71
- package/dist/templates/default/patches/PR-368.patch +0 -91
- package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-navigation-tracking.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-onboarding.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-reduced-motion.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-updates.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/a11y.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/app.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/auth-client.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/convex-auth.tsx +0 -0
- /package/dist/templates/default/{lib → src/lib}/env.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/haptics.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/storage.ts +0 -0
|
@@ -1,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't worry. Let'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'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,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.
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|