@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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
2
|
+
import { Button, Host, Text } from "@expo/ui/swift-ui";
|
|
3
|
+
import {
|
|
4
|
+
accessibilityHint,
|
|
5
|
+
accessibilityLabel,
|
|
6
|
+
buttonStyle,
|
|
7
|
+
contentShape,
|
|
8
|
+
disabled as disabledModifier,
|
|
9
|
+
foregroundStyle,
|
|
10
|
+
frame,
|
|
11
|
+
padding,
|
|
12
|
+
shapes,
|
|
13
|
+
} from "@expo/ui/swift-ui/modifiers";
|
|
14
|
+
|
|
15
|
+
import { Material } from "@/components/ui/material";
|
|
16
|
+
import { useAppUpdates } from "@/hooks/use-updates";
|
|
17
|
+
import { Spacing, FontSize, TouchTarget } from "@/constants/layout";
|
|
18
|
+
import { Radius } from "@/constants/theme";
|
|
19
|
+
import { ZIndex } from "@/constants/ui";
|
|
20
|
+
import { useColors } from "@/hooks/use-theme";
|
|
21
|
+
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
22
|
+
|
|
23
|
+
export function UpdateBanner({ testID }: { testID?: string } = {}) {
|
|
24
|
+
const updates = useAppUpdates();
|
|
25
|
+
const insets = useSafeAreaInsets();
|
|
26
|
+
const colors = useColors();
|
|
27
|
+
const dfont = useDynamicFont();
|
|
28
|
+
|
|
29
|
+
const showProgress = updates.isDownloading;
|
|
30
|
+
const showError = !!updates.downloadError;
|
|
31
|
+
if (!showProgress && !showError) return null;
|
|
32
|
+
|
|
33
|
+
const tint = showError ? (colors.destructive as string) : (colors.primary as string);
|
|
34
|
+
const fg = showError
|
|
35
|
+
? (colors.destructiveForeground as string)
|
|
36
|
+
: (colors.primaryForeground as string);
|
|
37
|
+
const pct =
|
|
38
|
+
showProgress && updates.downloadProgress != null
|
|
39
|
+
? ` ${Math.round(updates.downloadProgress * 100)}%`
|
|
40
|
+
: "";
|
|
41
|
+
const label = showError ? "Update failed. Tap to retry." : `Updating${pct}`;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Material
|
|
45
|
+
accessibilityLiveRegion="polite"
|
|
46
|
+
accessibilityRole="alert"
|
|
47
|
+
variant="chrome"
|
|
48
|
+
tintColor={tint}
|
|
49
|
+
isInteractive={showError}
|
|
50
|
+
style={{
|
|
51
|
+
position: "absolute",
|
|
52
|
+
bottom: insets.bottom + Spacing.xs,
|
|
53
|
+
left: Spacing.md,
|
|
54
|
+
right: Spacing.md,
|
|
55
|
+
zIndex: ZIndex.updateBanner,
|
|
56
|
+
borderRadius: Radius.full,
|
|
57
|
+
overflow: "hidden",
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<Host matchContents>
|
|
62
|
+
<Button
|
|
63
|
+
testID="update-banner-retry"
|
|
64
|
+
modifiers={[
|
|
65
|
+
buttonStyle("plain"),
|
|
66
|
+
padding({ vertical: Spacing.sm, horizontal: Spacing.lg }),
|
|
67
|
+
frame({ minHeight: TouchTarget.min }),
|
|
68
|
+
contentShape(shapes.rectangle()),
|
|
69
|
+
disabledModifier(!showError),
|
|
70
|
+
accessibilityLabel(label),
|
|
71
|
+
...(showError ? [accessibilityHint("Re-attempts the update download")] : []),
|
|
72
|
+
]}
|
|
73
|
+
onPress={showError ? () => updates.downloadAndApply() : () => {}}
|
|
74
|
+
>
|
|
75
|
+
<Text
|
|
76
|
+
testID={testID}
|
|
77
|
+
modifiers={[dfont({ size: FontSize["3xl"], weight: "bold" }), foregroundStyle(fg)]}
|
|
78
|
+
>
|
|
79
|
+
{label}
|
|
80
|
+
</Text>
|
|
81
|
+
</Button>
|
|
82
|
+
</Host>
|
|
83
|
+
</Material>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -60,12 +60,6 @@ export const Breakpoint = {
|
|
|
60
60
|
export const TAB_BAR_HEIGHT = 80;
|
|
61
61
|
export const TAB_BAR_CLEARANCE = TAB_BAR_HEIGHT + Spacing.lg;
|
|
62
62
|
|
|
63
|
-
// Single source of truth for prominent action buttons across the auth flow,
|
|
64
|
-
// onboarding, error states, and OTP. Keeps Sign In, Sign Up, Send Reset
|
|
65
|
-
// Code, Reset Password, Verify, Try Again, and Sign in with Apple visually
|
|
66
|
-
// identical: same height, same capsule corner radius, same Geist label
|
|
67
|
-
// size and weight. Color comes from the shadcn palette (`primary` /
|
|
68
|
-
// `primaryForeground`), never hardcoded.
|
|
69
63
|
export const Button = {
|
|
70
64
|
height: 50,
|
|
71
65
|
cornerRadius: 25,
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
+
// SDK 56 bans application-code imports from @react-navigation/* in favor of
|
|
2
|
+
// the expo-router re-exports. Theme types come from expo-router/react-navigation,
|
|
3
|
+
// DefaultTheme from expo-router itself. Don't add @react-navigation/native back.
|
|
1
4
|
import { DefaultTheme as RNDefaultTheme } from "expo-router";
|
|
2
5
|
import type { Theme as RNTheme } from "expo-router/react-navigation";
|
|
3
6
|
import { DynamicColorIOS } from "react-native";
|
|
4
7
|
|
|
5
|
-
import { FontFamily } from "@/constants/layout";
|
|
6
|
-
|
|
7
8
|
// `DynamicColorIOS` returns an `OpaqueColorValue`. React Native's StyleSheet
|
|
8
9
|
// processor resolves it natively, but several @expo/ui props and our own
|
|
9
10
|
// `as string` call-sites expect a string. The runtime payload behaves like
|
|
10
11
|
// any other ColorValue for RN, so we cast at the boundary instead of
|
|
11
12
|
// littering every call-site with `as unknown as string`.
|
|
12
13
|
|
|
13
|
-
// Shadcn `b1VlJDbW` preset (luma + neutral + Geist + Hugeicons + radius default).
|
|
14
|
-
// Source: shadcn-ui/ui apps/v4/registry/themes.ts, "neutral" entry.
|
|
15
|
-
// OKLCH values converted to sRGB hex via the standard Björn Ottosson matrix.
|
|
16
14
|
// Each token carries a light + dark variant plus a high-contrast pair for the
|
|
17
15
|
// iOS Increase Contrast accessibility setting (HIG: "If you define a custom
|
|
18
16
|
// color, make sure to supply light and dark variants, and an increased
|
|
@@ -26,40 +24,48 @@ type Tone = {
|
|
|
26
24
|
|
|
27
25
|
const tone = (t: Tone): string => DynamicColorIOS(t) as unknown as string;
|
|
28
26
|
|
|
29
|
-
// Shadcn neutral palette in hex. Indexed by Tailwind v4 neutral step.
|
|
30
27
|
const NEUTRAL = {
|
|
31
28
|
white: "#FFFFFF",
|
|
32
29
|
black: "#000000",
|
|
33
|
-
n50: "#FAFAFA",
|
|
34
|
-
n100: "#F5F5F5",
|
|
35
|
-
n150: "#EBEBEB",
|
|
36
|
-
n200: "#E5E5E5",
|
|
37
|
-
n300: "#D4D4D4",
|
|
38
|
-
n400: "#A1A1A1",
|
|
39
|
-
n500: "#737373",
|
|
40
|
-
n600: "#525252",
|
|
41
|
-
n700: "#404040",
|
|
42
|
-
n800: "#262626",
|
|
43
|
-
n850: "#1C1C1C",
|
|
44
|
-
n900: "#171717",
|
|
45
|
-
n950: "#0A0A0A",
|
|
30
|
+
n50: "#FAFAFA",
|
|
31
|
+
n100: "#F5F5F5",
|
|
32
|
+
n150: "#EBEBEB",
|
|
33
|
+
n200: "#E5E5E5",
|
|
34
|
+
n300: "#D4D4D4",
|
|
35
|
+
n400: "#A1A1A1",
|
|
36
|
+
n500: "#737373",
|
|
37
|
+
n600: "#525252",
|
|
38
|
+
n700: "#404040",
|
|
39
|
+
n800: "#262626",
|
|
40
|
+
n850: "#1C1C1C",
|
|
41
|
+
n900: "#171717",
|
|
42
|
+
n950: "#0A0A0A",
|
|
46
43
|
} as const;
|
|
47
44
|
|
|
48
45
|
const DESTRUCTIVE = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
// Light darkened from #E7000B to clear WCAG AA (4.5:1) for sub-17pt text on
|
|
47
|
+
// the muted (n100) capsule fill, not just on the white page background.
|
|
48
|
+
light: "#B30009",
|
|
49
|
+
dark: "#FF6467",
|
|
50
|
+
hcLight: "#990007",
|
|
52
51
|
hcDark: "#FFA0A2",
|
|
53
52
|
} as const;
|
|
54
53
|
|
|
54
|
+
const WARNING = {
|
|
55
|
+
light: "#B45309",
|
|
56
|
+
dark: "#F59E0B",
|
|
57
|
+
hcLight: "#92400E",
|
|
58
|
+
hcDark: "#FCD34D",
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
55
61
|
// shadcn dark `border` is `oklch(1 0 0 / 10%)`, `input` is `15%`. These need
|
|
56
62
|
// the alpha to hover over translucent layers. iOS DynamicColorIOS accepts
|
|
57
63
|
// 8-digit hex, so we encode RGBA inline.
|
|
58
64
|
const ALPHA_DARK = {
|
|
59
|
-
border: "#FFFFFF1A",
|
|
60
|
-
borderHC: "#FFFFFF40",
|
|
61
|
-
input: "#FFFFFF26",
|
|
62
|
-
inputHC: "#FFFFFF59",
|
|
65
|
+
border: "#FFFFFF1A",
|
|
66
|
+
borderHC: "#FFFFFF40",
|
|
67
|
+
input: "#FFFFFF26",
|
|
68
|
+
inputHC: "#FFFFFF59",
|
|
63
69
|
} as const;
|
|
64
70
|
|
|
65
71
|
const t = {
|
|
@@ -130,9 +136,11 @@ const t = {
|
|
|
130
136
|
highContrastDark: NEUTRAL.n850,
|
|
131
137
|
}),
|
|
132
138
|
mutedForeground: tone({
|
|
133
|
-
|
|
139
|
+
// n500 -> n600 so secondary text clears 4.5:1 on the muted card by default,
|
|
140
|
+
// not only under Increase Contrast.
|
|
141
|
+
light: NEUTRAL.n600,
|
|
134
142
|
dark: NEUTRAL.n400,
|
|
135
|
-
highContrastLight: NEUTRAL.
|
|
143
|
+
highContrastLight: NEUTRAL.n700,
|
|
136
144
|
highContrastDark: NEUTRAL.n300,
|
|
137
145
|
}),
|
|
138
146
|
accent: tone({
|
|
@@ -217,7 +225,7 @@ const t = {
|
|
|
217
225
|
}),
|
|
218
226
|
sidebarPrimary: tone({
|
|
219
227
|
light: NEUTRAL.n900,
|
|
220
|
-
dark: "#1447E6",
|
|
228
|
+
dark: "#1447E6",
|
|
221
229
|
highContrastLight: NEUTRAL.black,
|
|
222
230
|
highContrastDark: "#3D6FFA",
|
|
223
231
|
}),
|
|
@@ -252,8 +260,6 @@ const t = {
|
|
|
252
260
|
highContrastDark: NEUTRAL.n400,
|
|
253
261
|
}),
|
|
254
262
|
|
|
255
|
-
// Translucent fills. Same hue as primary, layered for surface tinting,
|
|
256
|
-
// press states, focus glows.
|
|
257
263
|
primaryFill: tone({
|
|
258
264
|
light: "rgba(23,23,23,0.06)",
|
|
259
265
|
dark: "rgba(229,229,229,0.10)",
|
|
@@ -292,23 +298,14 @@ const t = {
|
|
|
292
298
|
}),
|
|
293
299
|
} as const;
|
|
294
300
|
|
|
295
|
-
// Shadcn neutral tokens plus the handful of aliases the app actually
|
|
296
|
-
// references. Add an entry here the first time you need it. don't ship
|
|
297
|
-
// dead palette rows.
|
|
298
301
|
export const Colors = {
|
|
299
302
|
...t,
|
|
300
303
|
|
|
301
|
-
// Separator is a fork of the border token so navigation chrome can swap
|
|
302
|
-
// it independently without touching shadcn `border`.
|
|
303
304
|
separator: t.border,
|
|
304
305
|
|
|
305
|
-
// Tab bar inactive/active states. Mapped to muted-foreground / primary
|
|
306
|
-
// so the bar reads as part of the navigation chrome.
|
|
307
306
|
tabIconDefault: t.mutedForeground,
|
|
308
307
|
tabIconSelected: t.primary,
|
|
309
308
|
|
|
310
|
-
// Tertiary label (third-rank caption text) sits between muted and
|
|
311
|
-
// background. too faint for body copy, dark enough to read.
|
|
312
309
|
tertiaryLabel: tone({
|
|
313
310
|
light: NEUTRAL.n400,
|
|
314
311
|
dark: NEUTRAL.n500,
|
|
@@ -316,7 +313,6 @@ export const Colors = {
|
|
|
316
313
|
highContrastDark: NEUTRAL.n400,
|
|
317
314
|
}),
|
|
318
315
|
|
|
319
|
-
// Inverse of destructive (white text on destructive fill).
|
|
320
316
|
destructiveForeground: tone({
|
|
321
317
|
light: NEUTRAL.white,
|
|
322
318
|
dark: NEUTRAL.n900,
|
|
@@ -324,14 +320,21 @@ export const Colors = {
|
|
|
324
320
|
highContrastDark: NEUTRAL.black,
|
|
325
321
|
}),
|
|
326
322
|
|
|
327
|
-
// Status green for "available" / "completed" markers (HIG-aligned, not
|
|
328
|
-
// shadcn. shadcn doesn't define a success token).
|
|
329
323
|
success: tone({
|
|
330
|
-
|
|
324
|
+
// Light darkened from #16A34A (~3.3:1 on white) to clear WCAG AA for the
|
|
325
|
+
// sub-17pt success copy in SuccessText and the username-available row.
|
|
326
|
+
light: "#15803D",
|
|
331
327
|
dark: "#22C55E",
|
|
332
|
-
highContrastLight: "#
|
|
328
|
+
highContrastLight: "#166534",
|
|
333
329
|
highContrastDark: "#4ADE80",
|
|
334
330
|
}),
|
|
331
|
+
|
|
332
|
+
warning: tone({
|
|
333
|
+
light: WARNING.light,
|
|
334
|
+
dark: WARNING.dark,
|
|
335
|
+
highContrastLight: WARNING.hcLight,
|
|
336
|
+
highContrastDark: WARNING.hcDark,
|
|
337
|
+
}),
|
|
335
338
|
} as const;
|
|
336
339
|
|
|
337
340
|
export const HeaderTint = Colors.foreground;
|
|
@@ -341,9 +344,7 @@ export type ColorPalette = typeof Colors;
|
|
|
341
344
|
// React Navigation `Theme` consumers (NavigationThemeProvider, header tint,
|
|
342
345
|
// back chevron, screen background) read flat color strings, not
|
|
343
346
|
// DynamicColorIOS values. We export one theme per appearance and pick at
|
|
344
|
-
// the root layout based on `useColorScheme()
|
|
345
|
-
// (back chevron, badge, header text) tracks the shadcn neutral palette
|
|
346
|
-
// instead of iOS systemBlue and the React Navigation defaults.
|
|
347
|
+
// the root layout based on `useColorScheme()`.
|
|
347
348
|
export const NavigationLight: RNTheme = {
|
|
348
349
|
dark: false,
|
|
349
350
|
colors: {
|
|
@@ -370,14 +371,6 @@ export const NavigationDark: RNTheme = {
|
|
|
370
371
|
fonts: RNDefaultTheme.fonts,
|
|
371
372
|
};
|
|
372
373
|
|
|
373
|
-
// shadcn radius scale, --radius = 0.625rem = 10px.
|
|
374
|
-
// sm = radius * 0.6 = 6
|
|
375
|
-
// md = radius * 0.8 = 8
|
|
376
|
-
// lg = radius * 1.0 = 10 (shadcn default)
|
|
377
|
-
// xl = radius * 1.4 = 14
|
|
378
|
-
// 2xl = radius * 1.8 = 18
|
|
379
|
-
// 3xl = radius * 2.2 = 22
|
|
380
|
-
// 4xl = radius * 2.6 = 26
|
|
381
374
|
const RADIUS_BASE = 10;
|
|
382
375
|
export const Radius = {
|
|
383
376
|
none: 0,
|
|
@@ -391,11 +384,3 @@ export const Radius = {
|
|
|
391
384
|
"4xl": Math.round(RADIUS_BASE * 2.6),
|
|
392
385
|
full: 9999,
|
|
393
386
|
} as const;
|
|
394
|
-
|
|
395
|
-
export const Typography = {
|
|
396
|
-
default: { fontSize: 16, lineHeight: 24, fontFamily: FontFamily.regular },
|
|
397
|
-
defaultSemiBold: { fontSize: 16, lineHeight: 24, fontFamily: FontFamily.semiBold },
|
|
398
|
-
title: { fontSize: 30, lineHeight: 38, fontFamily: FontFamily.bold, letterSpacing: -0.5 },
|
|
399
|
-
subtitle: { fontSize: 20, lineHeight: 26, fontFamily: FontFamily.semiBold },
|
|
400
|
-
link: { fontSize: 16, lineHeight: 24, fontFamily: FontFamily.regular },
|
|
401
|
-
};
|
|
@@ -40,6 +40,19 @@ export const Duration = {
|
|
|
40
40
|
splash: 1000,
|
|
41
41
|
} as const;
|
|
42
42
|
|
|
43
|
+
// Dynamic Type ceilings for fixed-geometry controls. upstream expo/expo#46007
|
|
44
|
+
// opts the app into native Dynamic Type through the `textStyle` font path, this
|
|
45
|
+
// bounds it where a control can't reflow. `dynamicTypeSize({ max })` from
|
|
46
|
+
// upstream expo/expo#46540 caps growth while still honoring the user's setting.
|
|
47
|
+
export const DynamicType = {
|
|
48
|
+
// Segmented toggle and the "This device" session badge: small controls that
|
|
49
|
+
// tolerate one accessibility step, then stop.
|
|
50
|
+
control: "accessibility1",
|
|
51
|
+
// The OTP field is tightest: six 24pt monospaced glyphs with kerning(8) in a
|
|
52
|
+
// capsule that can't wrap, so it caps below the accessibility sizes.
|
|
53
|
+
otp: "xxLarge",
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
43
56
|
export const Size = {
|
|
44
57
|
checkbox: 24,
|
|
45
58
|
iconContainer: 40,
|
|
@@ -68,10 +81,6 @@ export const Keyboard = {
|
|
|
68
81
|
verticalOffset: 100,
|
|
69
82
|
} as const;
|
|
70
83
|
|
|
71
|
-
export const Accessibility = {
|
|
72
|
-
maxFontSizeMultiplier: 2,
|
|
73
|
-
} as const;
|
|
74
|
-
|
|
75
84
|
export const EmptyState = {
|
|
76
85
|
paddingVertical: 60,
|
|
77
86
|
} as const;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
4
|
+
const [debounced, setDebounced] = useState(value);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const id = setTimeout(() => setDebounced(value), delay);
|
|
8
|
+
return () => clearTimeout(id);
|
|
9
|
+
}, [value, delay]);
|
|
10
|
+
|
|
11
|
+
return debounced;
|
|
12
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useEffect, useRef } 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
|
+
/**
|
|
9
|
+
* Resumes a deep link that arrived BEFORE the user authenticated. Links that
|
|
10
|
+
* arrive while authenticated are navigated by `+native-intent.tsx` (expo-router
|
|
11
|
+
* runs `redirectSystemPath` and drives the navigator itself, query included),
|
|
12
|
+
* so this hook must not re-navigate them or every tap would double-push and
|
|
13
|
+
* stack a phantom copy of the destination. It handles only the deferred case:
|
|
14
|
+
* an incoming URL whose navigation the auth guard blocked, replayed once
|
|
15
|
+
* sign-in completes.
|
|
16
|
+
*/
|
|
17
|
+
export function useDeepLinkHandler() {
|
|
18
|
+
// See note in app/_layout.tsx: Better Auth session is the canonical signal.
|
|
19
|
+
// `useConvexAuth` is unreliable due to the bridge's sessionId churn.
|
|
20
|
+
const { data: session } = authClient.useSession();
|
|
21
|
+
const isAuthenticated = !!session?.session;
|
|
22
|
+
const url = useURL();
|
|
23
|
+
const pendingUrl = useRef<string | null>(null);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!url) return;
|
|
27
|
+
|
|
28
|
+
if (!isAuthenticated) {
|
|
29
|
+
// native-intent's navigation to a protected route was blocked by the
|
|
30
|
+
// auth guard; remember the link so we can resume it after sign-in.
|
|
31
|
+
pendingUrl.current = url;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Authenticated. Act only on a link that arrived while unauthenticated;
|
|
36
|
+
// links that arrive while authed are already navigated by native-intent.
|
|
37
|
+
if (pendingUrl.current !== url) return;
|
|
38
|
+
pendingUrl.current = null;
|
|
39
|
+
|
|
40
|
+
let resolved;
|
|
41
|
+
try {
|
|
42
|
+
resolved = resolveDeepLink(url);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (__DEV__) console.warn("[DeepLink] parse failed:", err);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!resolved.href) return;
|
|
49
|
+
router.push({ pathname: resolved.href, params: resolved.params } as Href);
|
|
50
|
+
}, [isAuthenticated, url]);
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import * as LocalAuthentication from "expo-local-authentication";
|
|
3
|
+
import { useMutation } from "convex/react";
|
|
4
|
+
|
|
5
|
+
import { api } from "@/convex/_generated/api";
|
|
6
|
+
import { authClient } from "@/lib/auth-client";
|
|
7
|
+
import { formatError } from "@/components/ui/convex-error";
|
|
8
|
+
import { haptics } from "@/lib/haptics";
|
|
9
|
+
|
|
10
|
+
// Face ID gate, soft-delete mutation, and sign-out for account deletion, shared
|
|
11
|
+
// by the profile and settings screens. The mutation can reject (rate limit,
|
|
12
|
+
// network, server); on failure the caller surfaces `deleteError` instead of
|
|
13
|
+
// leaving the user on an unchanged screen after confirming a destructive action.
|
|
14
|
+
export function useDeleteAccount() {
|
|
15
|
+
const deleteAccountMutation = useMutation(api.users.deleteAccount);
|
|
16
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
const deleteAccount = useCallback(async () => {
|
|
19
|
+
haptics.error();
|
|
20
|
+
const result = await LocalAuthentication.authenticateAsync({
|
|
21
|
+
promptMessage: "Confirm with Face ID",
|
|
22
|
+
});
|
|
23
|
+
if (!result.success) return;
|
|
24
|
+
try {
|
|
25
|
+
setDeleteError(null);
|
|
26
|
+
await deleteAccountMutation();
|
|
27
|
+
await authClient.signOut();
|
|
28
|
+
} catch (err) {
|
|
29
|
+
haptics.error();
|
|
30
|
+
setDeleteError(formatError(err));
|
|
31
|
+
}
|
|
32
|
+
}, [deleteAccountMutation]);
|
|
33
|
+
|
|
34
|
+
return { deleteAccount, deleteError };
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useReducedMotion } from "@/hooks/use-reduced-motion";
|
|
2
|
+
|
|
3
|
+
export function useMotionScreenOptions<A extends string>(
|
|
4
|
+
animation: A,
|
|
5
|
+
animationDuration?: number,
|
|
6
|
+
): {
|
|
7
|
+
animation: A | "fade";
|
|
8
|
+
animationDuration: number | undefined;
|
|
9
|
+
} {
|
|
10
|
+
const reduceMotion = useReducedMotion();
|
|
11
|
+
if (reduceMotion) return { animation: "fade", animationDuration: 150 };
|
|
12
|
+
return { animation, animationDuration };
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useNetworkState } from "expo-network";
|
|
3
|
+
|
|
4
|
+
// expo-network's iOS module uses a temporary `NWPathMonitor` for its initial
|
|
5
|
+
// probe (`getNetworkStateAsync`) with a 5s timeout. On simulator and during
|
|
6
|
+
// cold-start the temp monitor sometimes never fires, so the probe returns
|
|
7
|
+
// `isConnected: false` while the device is actually online. The persistent
|
|
8
|
+
// listener corrects it on the next change event, but until then the banner
|
|
9
|
+
// would flash "You're offline" on a working network. Gate the banner on a
|
|
10
|
+
// short settle window so transient probe failures don't surface.
|
|
11
|
+
const OFFLINE_SETTLE_MS = 3000;
|
|
12
|
+
|
|
13
|
+
export function useNetwork() {
|
|
14
|
+
const { isConnected, isInternetReachable } = useNetworkState();
|
|
15
|
+
// On iOS expo-network sets `isInternetReachable === isConnected`; we keep
|
|
16
|
+
// both checks for cross-platform parity but they collapse to one signal.
|
|
17
|
+
const probablyOffline = isConnected === false || isInternetReachable === false;
|
|
18
|
+
const [settledOffline, setSettledOffline] = useState(false);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!probablyOffline) {
|
|
22
|
+
setSettledOffline(false);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const id = setTimeout(() => setSettledOffline(true), OFFLINE_SETTLE_MS);
|
|
26
|
+
return () => clearTimeout(id);
|
|
27
|
+
}, [probablyOffline]);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
isConnected,
|
|
31
|
+
isInternetReachable,
|
|
32
|
+
isOffline: settledOffline,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -2,10 +2,10 @@ import { useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import * as Notifications from "expo-notifications";
|
|
3
3
|
import { useConvexAuth } from "convex/react";
|
|
4
4
|
import { useMutation } from "convex/react";
|
|
5
|
-
import { router } from "expo-router";
|
|
5
|
+
import { router, type Href } from "expo-router";
|
|
6
6
|
|
|
7
7
|
import { api } from "@/convex/_generated/api";
|
|
8
|
-
import {
|
|
8
|
+
import { resolveDeepLink } from "@/lib/deep-link";
|
|
9
9
|
import {
|
|
10
10
|
getExpoPushToken,
|
|
11
11
|
requestPermission,
|
|
@@ -18,20 +18,16 @@ interface UseNotificationsOptions {
|
|
|
18
18
|
onNotificationsDropped?: () => void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* If a notification's payload includes a `url` string, route to it after
|
|
23
|
-
* deep-link validation. Add custom action handling (categories, button taps,
|
|
24
|
-
* inline replies) here when your app needs it.
|
|
25
|
-
*/
|
|
26
21
|
function handleNotificationResponse(response: Notifications.NotificationResponse) {
|
|
27
22
|
const url = response.notification.request.content.data?.url;
|
|
28
23
|
if (typeof url !== "string") return;
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const { href, params } = resolveDeepLink(url);
|
|
26
|
+
if (!href) {
|
|
27
|
+
if (__DEV__) console.warn("[Notification] Blocked navigation to:", url);
|
|
28
|
+
return;
|
|
34
29
|
}
|
|
30
|
+
router.push({ pathname: href, params } as Href);
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
export function useNotifications(options?: UseNotificationsOptions) {
|
|
@@ -40,7 +36,6 @@ export function useNotifications(options?: UseNotificationsOptions) {
|
|
|
40
36
|
const registered = useRef(false);
|
|
41
37
|
const [expoPushToken, setExpoPushToken] = useState<string | null>(null);
|
|
42
38
|
|
|
43
|
-
// Register push token when authenticated
|
|
44
39
|
useEffect(() => {
|
|
45
40
|
if (!isAuthenticated) {
|
|
46
41
|
registered.current = false;
|
|
@@ -51,19 +46,37 @@ export function useNotifications(options?: UseNotificationsOptions) {
|
|
|
51
46
|
registered.current = true;
|
|
52
47
|
|
|
53
48
|
(async () => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
try {
|
|
50
|
+
const { granted } = await requestPermission();
|
|
51
|
+
if (!granted) return;
|
|
52
|
+
|
|
53
|
+
const token = await getExpoPushToken();
|
|
54
|
+
if (!token) return;
|
|
55
|
+
|
|
56
|
+
setExpoPushToken(token);
|
|
57
|
+
await upsertToken({ token, deviceType: "ios" });
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Reset so a transient failure (permission throw, network) retries on
|
|
60
|
+
// the next render instead of silently dropping push registration for
|
|
61
|
+
// the session. The early returns above intentionally keep it true.
|
|
62
|
+
registered.current = false;
|
|
63
|
+
if (__DEV__) console.warn("[Notification] registration failed:", e);
|
|
64
|
+
}
|
|
62
65
|
})();
|
|
63
66
|
}, [isAuthenticated, upsertToken]);
|
|
64
67
|
|
|
65
|
-
// Event listeners
|
|
66
68
|
useEffect(() => {
|
|
69
|
+
// Cold-start deep link: the launch tap arrives via the last-response
|
|
70
|
+
// getter, not the runtime listener below, so handle it once here.
|
|
71
|
+
// (useLastNotificationResponse would fire for BOTH cold-start and runtime,
|
|
72
|
+
// double-navigating every runtime tap.)
|
|
73
|
+
Notifications.getLastNotificationResponseAsync().then((initial) => {
|
|
74
|
+
if (!initial) return;
|
|
75
|
+
handleNotificationResponse(initial);
|
|
76
|
+
options?.onNotificationResponse?.(initial);
|
|
77
|
+
clearLastNotificationResponse();
|
|
78
|
+
});
|
|
79
|
+
|
|
67
80
|
const receivedSub = Notifications.addNotificationReceivedListener((notification) => {
|
|
68
81
|
if (__DEV__) console.log("[Notification] Received:", notification.request.identifier);
|
|
69
82
|
options?.onNotificationReceived?.(notification);
|
|
@@ -83,7 +96,11 @@ export function useNotifications(options?: UseNotificationsOptions) {
|
|
|
83
96
|
const tokenSub = Notifications.addPushTokenListener(async (token) => {
|
|
84
97
|
if (__DEV__) console.log("[Notification] Token rotated:", token.data);
|
|
85
98
|
if (isAuthenticated && typeof token.data === "string") {
|
|
86
|
-
|
|
99
|
+
try {
|
|
100
|
+
await upsertToken({ token: token.data, deviceType: "ios" });
|
|
101
|
+
} catch (e) {
|
|
102
|
+
if (__DEV__) console.warn("[Notification] token upsert failed:", e);
|
|
103
|
+
}
|
|
87
104
|
}
|
|
88
105
|
});
|
|
89
106
|
|
|
@@ -95,13 +112,5 @@ export function useNotifications(options?: UseNotificationsOptions) {
|
|
|
95
112
|
};
|
|
96
113
|
}, [isAuthenticated, options, upsertToken]);
|
|
97
114
|
|
|
98
|
-
// Cold-start deep linking
|
|
99
|
-
const lastResponse = Notifications.useLastNotificationResponse();
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
if (!lastResponse) return;
|
|
102
|
-
handleNotificationResponse(lastResponse);
|
|
103
|
-
clearLastNotificationResponse();
|
|
104
|
-
}, [lastResponse]);
|
|
105
|
-
|
|
106
115
|
return { expoPushToken };
|
|
107
116
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { AccessibilityInfo } from "react-native";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tracks the iOS `Settings → Accessibility → Display & Text Size → Reduce
|
|
6
|
+
* Transparency` flag. iOS 26 `GlassView` honors the setting natively, but the
|
|
7
|
+
* iOS 16.4-25 `BlurView` fallback in `<Material>` does not, so the surface
|
|
8
|
+
* stays translucent for users who explicitly asked for solid backgrounds.
|
|
9
|
+
*
|
|
10
|
+
* Consults `AccessibilityInfo.isReduceTransparencyEnabled()` on mount and
|
|
11
|
+
* keeps the value live via the `reduceTransparencyChanged` event so a runtime
|
|
12
|
+
* toggle in Settings re-renders consumers without a relaunch.
|
|
13
|
+
*/
|
|
14
|
+
export function useReduceTransparency(): boolean {
|
|
15
|
+
const [on, setOn] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
AccessibilityInfo.isReduceTransparencyEnabled().then((value) => {
|
|
20
|
+
if (!cancelled) setOn(value);
|
|
21
|
+
});
|
|
22
|
+
const sub = AccessibilityInfo.addEventListener("reduceTransparencyChanged", setOn);
|
|
23
|
+
return () => {
|
|
24
|
+
cancelled = true;
|
|
25
|
+
sub.remove();
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
return on;
|
|
30
|
+
}
|
|
@@ -42,11 +42,6 @@ export function useColors(): ColorPalette {
|
|
|
42
42
|
return Colors;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Theme-aware asset selector. Pass the light variant first, dark second.
|
|
46
|
-
// Returns whichever matches the active appearance (which honors the in-app
|
|
47
|
-
// override from `setTheme` in addition to the system setting).
|
|
48
|
-
//
|
|
49
|
-
// const icon = useThemedAsset(assets.brandIconLight, assets.brandIconDark);
|
|
50
45
|
export function useThemedAsset<L, D>(light: L, dark: D): L | D {
|
|
51
46
|
const scheme = useColorScheme();
|
|
52
47
|
return scheme === "dark" ? dark : light;
|