@croacroa/react-native-template 2.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -0
- package/.eslintrc.js +8 -0
- package/.github/workflows/ci.yml +187 -187
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/.github/workflows/npm-publish.yml +57 -0
- package/CHANGELOG.md +195 -106
- package/CONTRIBUTING.md +377 -377
- package/LICENSE +21 -0
- package/README.md +446 -399
- package/__tests__/accessibility/components.test.tsx +285 -0
- package/__tests__/components/Button.test.tsx +2 -4
- package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
- package/__tests__/components/snapshots.test.tsx +131 -131
- package/__tests__/helpers/a11y.ts +54 -0
- package/__tests__/hooks/useAnalytics.test.ts +100 -0
- package/__tests__/hooks/useAnimations.test.ts +70 -0
- package/__tests__/hooks/useAuth.test.tsx +71 -28
- package/__tests__/hooks/useMedia.test.ts +318 -0
- package/__tests__/hooks/usePayments.test.tsx +307 -0
- package/__tests__/hooks/usePermission.test.ts +230 -0
- package/__tests__/hooks/useWebSocket.test.ts +329 -0
- package/__tests__/integration/auth-api.test.tsx +224 -227
- package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
- package/__tests__/services/api.test.ts +24 -6
- package/app/(auth)/home.tsx +11 -9
- package/app/(auth)/profile.tsx +8 -6
- package/app/(auth)/settings.tsx +11 -9
- package/app/(public)/forgot-password.tsx +25 -15
- package/app/(public)/login.tsx +48 -12
- package/app/(public)/onboarding.tsx +5 -5
- package/app/(public)/register.tsx +24 -15
- package/app/_layout.tsx +6 -3
- package/app.config.ts +27 -2
- package/assets/images/.gitkeep +7 -7
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/components/ErrorBoundary.tsx +73 -28
- package/components/auth/SocialLoginButtons.tsx +168 -0
- package/components/forms/FormInput.tsx +5 -3
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/AnalyticsProvider.tsx +67 -0
- package/components/providers/SuspenseBoundary.tsx +359 -357
- package/components/providers/index.ts +24 -21
- package/components/ui/AnimatedButton.tsx +1 -9
- package/components/ui/AnimatedList.tsx +98 -0
- package/components/ui/AnimatedScreen.tsx +89 -0
- package/components/ui/Avatar.tsx +319 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Button.tsx +11 -3
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/FeatureGate.tsx +57 -0
- package/components/ui/ForceUpdateScreen.tsx +108 -0
- package/components/ui/ImagePickerButton.tsx +180 -0
- package/components/ui/Input.stories.tsx +2 -10
- package/components/ui/Input.tsx +2 -10
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Paywall.tsx +253 -0
- package/components/ui/PermissionGate.tsx +155 -0
- package/components/ui/PurchaseButton.tsx +84 -0
- package/components/ui/Select.tsx +240 -240
- package/components/ui/Skeleton.tsx +3 -1
- package/components/ui/Toast.tsx +427 -0
- package/components/ui/UploadProgress.tsx +189 -0
- package/components/ui/VirtualizedList.tsx +288 -285
- package/components/ui/index.ts +28 -23
- package/constants/config.ts +135 -97
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/docs/guides/analytics-posthog.md +121 -0
- package/docs/guides/auth-supabase.md +162 -0
- package/docs/guides/feature-flags-launchdarkly.md +150 -0
- package/docs/guides/payments-revenuecat.md +169 -0
- package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
- package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
- package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
- package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
- package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
- package/eas.json +2 -1
- package/hooks/index.ts +70 -27
- package/hooks/useAnimatedEntry.ts +204 -0
- package/hooks/useApi.ts +64 -4
- package/hooks/useAuth.tsx +7 -3
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useChannel.ts +111 -0
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useExperiment.ts +36 -0
- package/hooks/useFeatureFlag.ts +59 -0
- package/hooks/useForceUpdate.ts +91 -0
- package/hooks/useImagePicker.ts +281 -0
- package/hooks/useInAppReview.ts +64 -0
- package/hooks/useMFA.ts +509 -499
- package/hooks/useParallax.ts +142 -0
- package/hooks/usePerformance.ts +434 -434
- package/hooks/usePermission.ts +190 -0
- package/hooks/usePresence.ts +129 -0
- package/hooks/useProducts.ts +36 -0
- package/hooks/usePurchase.ts +103 -0
- package/hooks/useRateLimit.ts +70 -0
- package/hooks/useSubscription.ts +49 -0
- package/hooks/useTrackEvent.ts +52 -0
- package/hooks/useTrackScreen.ts +40 -0
- package/hooks/useUpdates.ts +358 -358
- package/hooks/useUpload.ts +165 -0
- package/hooks/useWebSocket.ts +111 -0
- package/i18n/index.ts +197 -194
- package/i18n/locales/ar.json +170 -101
- package/i18n/locales/de.json +170 -101
- package/i18n/locales/en.json +170 -101
- package/i18n/locales/es.json +170 -101
- package/i18n/locales/fr.json +170 -101
- package/jest.config.js +1 -1
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -92
- package/maestro/flows/mfa-setup.yaml +86 -86
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -101
- package/maestro/flows/offline-sync.yaml +128 -128
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +188 -175
- package/scripts/generate-placeholders.js +38 -0
- package/services/analytics/adapters/console.ts +50 -0
- package/services/analytics/analytics-adapter.ts +94 -0
- package/services/analytics/types.ts +73 -0
- package/services/analytics.ts +428 -428
- package/services/api.ts +419 -340
- package/services/auth/social/apple.ts +110 -0
- package/services/auth/social/google.ts +159 -0
- package/services/auth/social/social-auth.ts +100 -0
- package/services/auth/social/types.ts +80 -0
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +652 -626
- package/services/feature-flags/adapters/mock.ts +108 -0
- package/services/feature-flags/feature-flag-adapter.ts +174 -0
- package/services/feature-flags/types.ts +79 -0
- package/services/force-update.ts +140 -0
- package/services/index.ts +116 -54
- package/services/media/compression.ts +91 -0
- package/services/media/media-picker.ts +151 -0
- package/services/media/media-upload.ts +160 -0
- package/services/payments/adapters/mock.ts +159 -0
- package/services/payments/payment-adapter.ts +118 -0
- package/services/payments/types.ts +131 -0
- package/services/permissions/permission-manager.ts +284 -0
- package/services/permissions/types.ts +104 -0
- package/services/realtime/types.ts +100 -0
- package/services/realtime/websocket-manager.ts +441 -0
- package/services/security.ts +289 -286
- package/services/sentry.ts +4 -4
- package/stores/appStore.ts +9 -0
- package/stores/notificationStore.ts +3 -1
- package/tailwind.config.js +47 -47
- package/tsconfig.json +37 -13
- package/types/user.ts +1 -1
- package/utils/accessibility.ts +446 -446
- package/utils/animations/presets.ts +182 -0
- package/utils/animations/transitions.ts +62 -0
- package/utils/index.ts +63 -52
- package/utils/toast.ts +9 -2
- package/utils/validation.ts +4 -1
- package/utils/withAccessibility.tsx +272 -272
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
View,
|
|
3
|
+
Text,
|
|
4
|
+
Pressable,
|
|
5
|
+
KeyboardAvoidingView,
|
|
6
|
+
Platform,
|
|
7
|
+
ScrollView,
|
|
8
|
+
} from "react-native";
|
|
2
9
|
import { Link, router } from "expo-router";
|
|
3
10
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
11
|
import { useForm } from "react-hook-form";
|
|
5
12
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
6
13
|
import Animated, { FadeInDown } from "react-native-reanimated";
|
|
14
|
+
import { useTranslation } from "react-i18next";
|
|
7
15
|
|
|
8
16
|
import { FormInput } from "@/components/forms/FormInput";
|
|
9
17
|
import { AnimatedButton } from "@/components/ui/AnimatedButton";
|
|
@@ -12,6 +20,7 @@ import { registerSchema, RegisterFormData } from "@/utils/validation";
|
|
|
12
20
|
|
|
13
21
|
export default function RegisterScreen() {
|
|
14
22
|
const { signUp } = useAuth();
|
|
23
|
+
const { t } = useTranslation();
|
|
15
24
|
|
|
16
25
|
const {
|
|
17
26
|
control,
|
|
@@ -53,10 +62,10 @@ export default function RegisterScreen() {
|
|
|
53
62
|
className="mb-8"
|
|
54
63
|
>
|
|
55
64
|
<Text className="text-3xl font-bold text-text-light dark:text-text-dark">
|
|
56
|
-
|
|
65
|
+
{t("auth.createAccount")}
|
|
57
66
|
</Text>
|
|
58
67
|
<Text className="mt-2 text-muted-light dark:text-muted-dark">
|
|
59
|
-
|
|
68
|
+
{t("auth.joinUs")}
|
|
60
69
|
</Text>
|
|
61
70
|
</Animated.View>
|
|
62
71
|
|
|
@@ -68,8 +77,8 @@ export default function RegisterScreen() {
|
|
|
68
77
|
<FormInput
|
|
69
78
|
name="name"
|
|
70
79
|
control={control}
|
|
71
|
-
label="
|
|
72
|
-
placeholder="
|
|
80
|
+
label={t("auth.name")}
|
|
81
|
+
placeholder={t("auth.enterName")}
|
|
73
82
|
autoComplete="name"
|
|
74
83
|
leftIcon="person-outline"
|
|
75
84
|
/>
|
|
@@ -77,8 +86,8 @@ export default function RegisterScreen() {
|
|
|
77
86
|
<FormInput
|
|
78
87
|
name="email"
|
|
79
88
|
control={control}
|
|
80
|
-
label="
|
|
81
|
-
placeholder="
|
|
89
|
+
label={t("auth.email")}
|
|
90
|
+
placeholder={t("auth.enterEmail")}
|
|
82
91
|
keyboardType="email-address"
|
|
83
92
|
autoCapitalize="none"
|
|
84
93
|
autoComplete="email"
|
|
@@ -88,19 +97,19 @@ export default function RegisterScreen() {
|
|
|
88
97
|
<FormInput
|
|
89
98
|
name="password"
|
|
90
99
|
control={control}
|
|
91
|
-
label="
|
|
92
|
-
placeholder="
|
|
100
|
+
label={t("auth.password")}
|
|
101
|
+
placeholder={t("auth.createPasswordPlaceholder")}
|
|
93
102
|
secureTextEntry
|
|
94
103
|
autoComplete="new-password"
|
|
95
104
|
leftIcon="lock-closed-outline"
|
|
96
|
-
hint="
|
|
105
|
+
hint={t("auth.passwordHintFull")}
|
|
97
106
|
/>
|
|
98
107
|
|
|
99
108
|
<FormInput
|
|
100
109
|
name="confirmPassword"
|
|
101
110
|
control={control}
|
|
102
|
-
label="
|
|
103
|
-
placeholder="
|
|
111
|
+
label={t("auth.confirmPassword")}
|
|
112
|
+
placeholder={t("auth.confirmPasswordPlaceholder")}
|
|
104
113
|
secureTextEntry
|
|
105
114
|
autoComplete="new-password"
|
|
106
115
|
leftIcon="lock-closed-outline"
|
|
@@ -111,7 +120,7 @@ export default function RegisterScreen() {
|
|
|
111
120
|
isLoading={isSubmitting}
|
|
112
121
|
className="mt-4"
|
|
113
122
|
>
|
|
114
|
-
|
|
123
|
+
{t("auth.createAccount")}
|
|
115
124
|
</AnimatedButton>
|
|
116
125
|
</Animated.View>
|
|
117
126
|
|
|
@@ -121,12 +130,12 @@ export default function RegisterScreen() {
|
|
|
121
130
|
className="mt-8 flex-row justify-center"
|
|
122
131
|
>
|
|
123
132
|
<Text className="text-muted-light dark:text-muted-dark">
|
|
124
|
-
|
|
133
|
+
{t("auth.haveAccount")}{" "}
|
|
125
134
|
</Text>
|
|
126
135
|
<Link href="/(public)/login" asChild>
|
|
127
136
|
<Pressable>
|
|
128
137
|
<Text className="font-semibold text-primary-600 dark:text-primary-400">
|
|
129
|
-
|
|
138
|
+
{t("auth.signIn")}
|
|
130
139
|
</Text>
|
|
131
140
|
</Pressable>
|
|
132
141
|
</Link>
|
package/app/_layout.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
|
8
8
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
9
9
|
|
|
10
10
|
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
|
11
|
+
import { AnalyticsProvider } from "@/components/providers/AnalyticsProvider";
|
|
11
12
|
import { AuthProvider } from "@/hooks/useAuth";
|
|
12
13
|
import { ThemeProvider, useTheme } from "@/hooks/useTheme";
|
|
13
14
|
import { useNotifications } from "@/hooks/useNotifications";
|
|
@@ -85,9 +86,11 @@ export default function RootLayout() {
|
|
|
85
86
|
persistOptions={persistOptions}
|
|
86
87
|
>
|
|
87
88
|
<ThemeProvider>
|
|
88
|
-
<
|
|
89
|
-
<
|
|
90
|
-
|
|
89
|
+
<AnalyticsProvider>
|
|
90
|
+
<AuthProvider>
|
|
91
|
+
<RootLayoutContent />
|
|
92
|
+
</AuthProvider>
|
|
93
|
+
</AnalyticsProvider>
|
|
91
94
|
</ThemeProvider>
|
|
92
95
|
</PersistQueryClientProvider>
|
|
93
96
|
</SafeAreaProvider>
|
package/app.config.ts
CHANGED
|
@@ -18,7 +18,7 @@ const getAppName = () => {
|
|
|
18
18
|
export default ({ config }: ConfigContext): ExpoConfig => ({
|
|
19
19
|
...config,
|
|
20
20
|
name: getAppName(),
|
|
21
|
-
slug: "
|
|
21
|
+
slug: "react-native-template",
|
|
22
22
|
version: "1.0.0",
|
|
23
23
|
orientation: "portrait",
|
|
24
24
|
icon: "./assets/images/icon.png",
|
|
@@ -35,6 +35,16 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
|
|
35
35
|
bundleIdentifier: getUniqueIdentifier(),
|
|
36
36
|
infoPlist: {
|
|
37
37
|
UIBackgroundModes: ["remote-notification", "fetch", "processing"],
|
|
38
|
+
ITSAppUsesNonExemptEncryption: false,
|
|
39
|
+
NSCameraUsageDescription: "This app uses the camera to take photos.",
|
|
40
|
+
NSPhotoLibraryUsageDescription:
|
|
41
|
+
"This app accesses your photo library to select images.",
|
|
42
|
+
NSLocationWhenInUseUsageDescription:
|
|
43
|
+
"This app uses your location to show nearby results.",
|
|
44
|
+
NSContactsUsageDescription:
|
|
45
|
+
"This app accesses your contacts to find friends.",
|
|
46
|
+
NSMicrophoneUsageDescription:
|
|
47
|
+
"This app uses the microphone to record audio.",
|
|
38
48
|
},
|
|
39
49
|
},
|
|
40
50
|
android: {
|
|
@@ -47,6 +57,13 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
|
|
47
57
|
"NOTIFICATIONS",
|
|
48
58
|
"RECEIVE_BOOT_COMPLETED",
|
|
49
59
|
"FOREGROUND_SERVICE",
|
|
60
|
+
"CAMERA",
|
|
61
|
+
"READ_CONTACTS",
|
|
62
|
+
"ACCESS_FINE_LOCATION",
|
|
63
|
+
"ACCESS_COARSE_LOCATION",
|
|
64
|
+
"READ_MEDIA_IMAGES",
|
|
65
|
+
"READ_MEDIA_VIDEO",
|
|
66
|
+
"RECORD_AUDIO",
|
|
50
67
|
],
|
|
51
68
|
},
|
|
52
69
|
web: {
|
|
@@ -87,6 +104,8 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
|
|
87
104
|
plugins: [
|
|
88
105
|
"expo-router",
|
|
89
106
|
"expo-secure-store",
|
|
107
|
+
"expo-image-picker",
|
|
108
|
+
"@sentry/react-native",
|
|
90
109
|
[
|
|
91
110
|
"expo-notifications",
|
|
92
111
|
{
|
|
@@ -103,13 +122,19 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
|
|
103
122
|
},
|
|
104
123
|
],
|
|
105
124
|
"expo-task-manager",
|
|
125
|
+
[
|
|
126
|
+
"expo-camera",
|
|
127
|
+
{
|
|
128
|
+
cameraPermission: "This app uses the camera to take photos.",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
106
131
|
],
|
|
107
132
|
experiments: {
|
|
108
133
|
typedRoutes: true,
|
|
109
134
|
},
|
|
110
135
|
extra: {
|
|
111
136
|
eas: {
|
|
112
|
-
projectId: "
|
|
137
|
+
projectId: "e683c5b3-fb16-4a31-b578-01f3fca1e67a",
|
|
113
138
|
},
|
|
114
139
|
},
|
|
115
140
|
});
|
package/assets/images/.gitkeep
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Placeholder for app assets
|
|
2
|
-
# Replace these files with your own:
|
|
3
|
-
# - icon.png (1024x1024)
|
|
4
|
-
# - splash.png (1284x2778)
|
|
5
|
-
# - adaptive-icon.png (1024x1024)
|
|
6
|
-
# - favicon.png (48x48)
|
|
7
|
-
# - notification-icon.png (96x96, white on transparent)
|
|
1
|
+
# Placeholder for app assets
|
|
2
|
+
# Replace these files with your own:
|
|
3
|
+
# - icon.png (1024x1024)
|
|
4
|
+
# - splash.png (1284x2778)
|
|
5
|
+
# - adaptive-icon.png (1024x1024)
|
|
6
|
+
# - favicon.png (48x48)
|
|
7
|
+
# - notification-icon.png (96x96, white on transparent)
|
|
Binary file
|
|
Binary file
|
package/assets/images/icon.png
CHANGED
|
Binary file
|
|
Binary file
|
package/assets/images/splash.png
CHANGED
|
Binary file
|
|
@@ -2,7 +2,9 @@ import { Component, ErrorInfo, ReactNode } from "react";
|
|
|
2
2
|
import { View, Text, Pressable, ScrollView } from "react-native";
|
|
3
3
|
import { Ionicons } from "@expo/vector-icons";
|
|
4
4
|
import * as Updates from "expo-updates";
|
|
5
|
-
import
|
|
5
|
+
import i18next from "i18next";
|
|
6
|
+
import { captureException, addBreadcrumb, Sentry } from "@/services/sentry";
|
|
7
|
+
import { useAppStore } from "@/stores/appStore";
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
children: ReactNode;
|
|
@@ -13,12 +15,18 @@ interface State {
|
|
|
13
15
|
hasError: boolean;
|
|
14
16
|
error: Error | null;
|
|
15
17
|
errorInfo: ErrorInfo | null;
|
|
18
|
+
crashCount: number;
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
const MAX_SOFT_RESETS = 3;
|
|
22
|
+
|
|
18
23
|
/**
|
|
19
24
|
* Global Error Boundary component
|
|
20
25
|
* Catches JavaScript errors anywhere in the child component tree
|
|
21
|
-
* and displays a fallback UI instead of crashing the whole app
|
|
26
|
+
* and displays a fallback UI instead of crashing the whole app.
|
|
27
|
+
*
|
|
28
|
+
* After multiple consecutive soft-reset failures, automatically
|
|
29
|
+
* offers a hard reset (clears stores + full app restart).
|
|
22
30
|
*/
|
|
23
31
|
export class ErrorBoundary extends Component<Props, State> {
|
|
24
32
|
constructor(props: Props) {
|
|
@@ -27,6 +35,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
27
35
|
hasError: false,
|
|
28
36
|
error: null,
|
|
29
37
|
errorInfo: null,
|
|
38
|
+
crashCount: 0,
|
|
30
39
|
};
|
|
31
40
|
}
|
|
32
41
|
|
|
@@ -35,9 +44,17 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
38
|
-
this.setState(
|
|
47
|
+
this.setState((prev) => {
|
|
48
|
+
const crashCount = prev.crashCount + 1;
|
|
49
|
+
const lastCrashTime = new Date().toISOString();
|
|
50
|
+
|
|
51
|
+
// Set Sentry context for crash recovery debugging
|
|
52
|
+
Sentry.setContext("crash_recovery", { crashCount, lastCrashTime });
|
|
53
|
+
|
|
54
|
+
return { errorInfo, crashCount };
|
|
55
|
+
});
|
|
39
56
|
|
|
40
|
-
// Log to
|
|
57
|
+
// Log to error reporting service
|
|
41
58
|
this.logError(error, errorInfo);
|
|
42
59
|
}
|
|
43
60
|
|
|
@@ -57,30 +74,41 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
57
74
|
console.error("Component stack:", errorInfo.componentStack);
|
|
58
75
|
}
|
|
59
76
|
|
|
60
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Hard reset: clears all Zustand persisted state and reloads the app.
|
|
79
|
+
*/
|
|
80
|
+
private handleHardReset = async () => {
|
|
61
81
|
try {
|
|
62
|
-
//
|
|
82
|
+
// Reset Zustand stores
|
|
83
|
+
useAppStore.getState().reset();
|
|
84
|
+
|
|
85
|
+
// Full app restart via expo-updates
|
|
63
86
|
if (!__DEV__) {
|
|
64
87
|
await Updates.reloadAsync();
|
|
65
88
|
} else {
|
|
66
|
-
// In dev, just reset the error state
|
|
89
|
+
// In dev, just reset the error state and crash counter
|
|
67
90
|
this.setState({
|
|
68
91
|
hasError: false,
|
|
69
92
|
error: null,
|
|
70
93
|
errorInfo: null,
|
|
94
|
+
crashCount: 0,
|
|
71
95
|
});
|
|
72
96
|
}
|
|
73
|
-
} catch
|
|
74
|
-
//
|
|
97
|
+
} catch {
|
|
98
|
+
// Last resort: reset error state so user isn't stuck
|
|
75
99
|
this.setState({
|
|
76
100
|
hasError: false,
|
|
77
101
|
error: null,
|
|
78
102
|
errorInfo: null,
|
|
103
|
+
crashCount: 0,
|
|
79
104
|
});
|
|
80
105
|
}
|
|
81
106
|
};
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Soft reset: simply re-renders the children by clearing the error state.
|
|
110
|
+
*/
|
|
111
|
+
private handleSoftReset = () => {
|
|
84
112
|
this.setState({
|
|
85
113
|
hasError: false,
|
|
86
114
|
error: null,
|
|
@@ -94,6 +122,8 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
94
122
|
return this.props.fallback;
|
|
95
123
|
}
|
|
96
124
|
|
|
125
|
+
const showHardReset = this.state.crashCount >= MAX_SOFT_RESETS;
|
|
126
|
+
|
|
97
127
|
return (
|
|
98
128
|
<View className="flex-1 items-center justify-center bg-background-light px-6 dark:bg-background-dark">
|
|
99
129
|
{/* Error Icon */}
|
|
@@ -103,13 +133,12 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
103
133
|
|
|
104
134
|
{/* Title */}
|
|
105
135
|
<Text className="mb-2 text-center text-2xl font-bold text-text-light dark:text-text-dark">
|
|
106
|
-
|
|
136
|
+
{i18next.t("errors.crashTitle")}
|
|
107
137
|
</Text>
|
|
108
138
|
|
|
109
139
|
{/* Description */}
|
|
110
140
|
<Text className="mb-8 text-center text-muted-light dark:text-muted-dark">
|
|
111
|
-
|
|
112
|
-
any inconvenience.
|
|
141
|
+
{i18next.t("errors.crashMessage")}
|
|
113
142
|
</Text>
|
|
114
143
|
|
|
115
144
|
{/* Error details (dev only) */}
|
|
@@ -128,21 +157,37 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
128
157
|
|
|
129
158
|
{/* Action buttons */}
|
|
130
159
|
<View className="w-full gap-3">
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
{showHardReset ? (
|
|
161
|
+
// After MAX_SOFT_RESETS consecutive failures, show hard reset
|
|
162
|
+
<Pressable
|
|
163
|
+
onPress={this.handleHardReset}
|
|
164
|
+
className="w-full items-center rounded-xl bg-red-600 py-4"
|
|
165
|
+
>
|
|
166
|
+
<Text className="font-semibold text-white">
|
|
167
|
+
{i18next.t("errors.restartApp")}
|
|
168
|
+
</Text>
|
|
169
|
+
</Pressable>
|
|
170
|
+
) : (
|
|
171
|
+
<>
|
|
172
|
+
<Pressable
|
|
173
|
+
onPress={this.handleSoftReset}
|
|
174
|
+
className="w-full items-center rounded-xl bg-primary-600 py-4"
|
|
175
|
+
>
|
|
176
|
+
<Text className="font-semibold text-white">
|
|
177
|
+
{i18next.t("errors.tryAgain")}
|
|
178
|
+
</Text>
|
|
179
|
+
</Pressable>
|
|
180
|
+
|
|
181
|
+
<Pressable
|
|
182
|
+
onPress={this.handleHardReset}
|
|
183
|
+
className="w-full items-center rounded-xl border-2 border-gray-300 py-4 dark:border-gray-600"
|
|
184
|
+
>
|
|
185
|
+
<Text className="font-semibold text-text-light dark:text-text-dark">
|
|
186
|
+
{i18next.t("errors.restartApp")}
|
|
187
|
+
</Text>
|
|
188
|
+
</Pressable>
|
|
189
|
+
</>
|
|
190
|
+
)}
|
|
146
191
|
</View>
|
|
147
192
|
</View>
|
|
148
193
|
);
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Social login buttons component
|
|
3
|
+
* Renders styled Google and Apple sign-in buttons with loading states.
|
|
4
|
+
* Apple button is only shown on iOS devices.
|
|
5
|
+
* @module components/auth/SocialLoginButtons
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
Text,
|
|
12
|
+
Pressable,
|
|
13
|
+
ActivityIndicator,
|
|
14
|
+
useColorScheme,
|
|
15
|
+
type ViewProps,
|
|
16
|
+
} from "react-native";
|
|
17
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
18
|
+
import { useTranslation } from "react-i18next";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
SocialAuth,
|
|
22
|
+
isAppleSignInAvailable,
|
|
23
|
+
} from "@/services/auth/social/social-auth";
|
|
24
|
+
import type { SocialAuthResult } from "@/services/auth/social/types";
|
|
25
|
+
import { cn } from "@/utils/cn";
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Types
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
interface SocialLoginButtonsProps extends ViewProps {
|
|
32
|
+
/** Callback when social sign-in succeeds */
|
|
33
|
+
onSuccess: (result: SocialAuthResult) => void;
|
|
34
|
+
/** Callback when social sign-in fails */
|
|
35
|
+
onError?: (error: Error) => void;
|
|
36
|
+
/** Disable all buttons (e.g., while another form is submitting) */
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Component
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Social login buttons for Google and Apple Sign-In.
|
|
46
|
+
*
|
|
47
|
+
* - Google button is always shown
|
|
48
|
+
* - Apple button is only shown on iOS
|
|
49
|
+
* - Each button has its own loading state
|
|
50
|
+
* - Supports dark mode via NativeWind
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <SocialLoginButtons
|
|
55
|
+
* onSuccess={(result) => {
|
|
56
|
+
* // Send result.idToken to your backend
|
|
57
|
+
* }}
|
|
58
|
+
* onError={(error) => {
|
|
59
|
+
* Alert.alert('Error', error.message);
|
|
60
|
+
* }}
|
|
61
|
+
* />
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function SocialLoginButtons({
|
|
65
|
+
onSuccess,
|
|
66
|
+
onError,
|
|
67
|
+
disabled = false,
|
|
68
|
+
className,
|
|
69
|
+
...viewProps
|
|
70
|
+
}: SocialLoginButtonsProps) {
|
|
71
|
+
const [loadingProvider, setLoadingProvider] = useState<string | null>(null);
|
|
72
|
+
const colorScheme = useColorScheme();
|
|
73
|
+
const { t } = useTranslation();
|
|
74
|
+
|
|
75
|
+
const showApple = isAppleSignInAvailable();
|
|
76
|
+
const isLoading = loadingProvider !== null;
|
|
77
|
+
const isDark = colorScheme === "dark";
|
|
78
|
+
|
|
79
|
+
const handleSignIn = async (provider: "google" | "apple") => {
|
|
80
|
+
if (isLoading || disabled) return;
|
|
81
|
+
|
|
82
|
+
setLoadingProvider(provider);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = await SocialAuth.signIn(provider);
|
|
86
|
+
|
|
87
|
+
if (result) {
|
|
88
|
+
onSuccess(result);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const authError =
|
|
92
|
+
error instanceof Error ? error : new Error("Social sign-in failed");
|
|
93
|
+
onError?.(authError);
|
|
94
|
+
} finally {
|
|
95
|
+
setLoadingProvider(null);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Apple button colors are inverted in dark mode
|
|
100
|
+
const appleIconColor = isDark ? "#000000" : "#FFFFFF";
|
|
101
|
+
const appleSpinnerColor = isDark ? "#000000" : "#FFFFFF";
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<View className={cn("gap-3", className)} {...viewProps}>
|
|
105
|
+
{/* Google Sign-In Button */}
|
|
106
|
+
<Pressable
|
|
107
|
+
onPress={() => handleSignIn("google")}
|
|
108
|
+
disabled={isLoading || disabled}
|
|
109
|
+
accessibilityRole="button"
|
|
110
|
+
accessibilityLabel="Continue with Google"
|
|
111
|
+
className={cn(
|
|
112
|
+
"flex-row items-center justify-center rounded-xl border border-gray-300 bg-white px-4 py-3.5",
|
|
113
|
+
"dark:border-gray-600 dark:bg-gray-100",
|
|
114
|
+
"active:bg-gray-50 dark:active:bg-gray-200",
|
|
115
|
+
(isLoading || disabled) && "opacity-50"
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{loadingProvider === "google" ? (
|
|
119
|
+
<ActivityIndicator size="small" color="#4285F4" />
|
|
120
|
+
) : (
|
|
121
|
+
<>
|
|
122
|
+
<Ionicons
|
|
123
|
+
name="logo-google"
|
|
124
|
+
size={20}
|
|
125
|
+
color="#4285F4"
|
|
126
|
+
style={{ marginRight: 12 }}
|
|
127
|
+
/>
|
|
128
|
+
<Text className="text-base font-semibold text-gray-700">
|
|
129
|
+
{t("socialAuth.continueWithGoogle")}
|
|
130
|
+
</Text>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</Pressable>
|
|
134
|
+
|
|
135
|
+
{/* Apple Sign-In Button (iOS only) */}
|
|
136
|
+
{showApple && (
|
|
137
|
+
<Pressable
|
|
138
|
+
onPress={() => handleSignIn("apple")}
|
|
139
|
+
disabled={isLoading || disabled}
|
|
140
|
+
accessibilityRole="button"
|
|
141
|
+
accessibilityLabel="Continue with Apple"
|
|
142
|
+
className={cn(
|
|
143
|
+
"flex-row items-center justify-center rounded-xl bg-black px-4 py-3.5",
|
|
144
|
+
"dark:bg-white",
|
|
145
|
+
"active:opacity-80",
|
|
146
|
+
(isLoading || disabled) && "opacity-50"
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
{loadingProvider === "apple" ? (
|
|
150
|
+
<ActivityIndicator size="small" color={appleSpinnerColor} />
|
|
151
|
+
) : (
|
|
152
|
+
<>
|
|
153
|
+
<Ionicons
|
|
154
|
+
name="logo-apple"
|
|
155
|
+
size={20}
|
|
156
|
+
color={appleIconColor}
|
|
157
|
+
style={{ marginRight: 12 }}
|
|
158
|
+
/>
|
|
159
|
+
<Text className="text-base font-semibold text-white dark:text-black">
|
|
160
|
+
{t("socialAuth.continueWithApple")}
|
|
161
|
+
</Text>
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
164
|
+
</Pressable>
|
|
165
|
+
)}
|
|
166
|
+
</View>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
import { Input } from "@/components/ui/Input";
|
|
11
11
|
import { Ionicons } from "@expo/vector-icons";
|
|
12
12
|
|
|
13
|
-
interface FormInputProps<T extends FieldValues>
|
|
14
|
-
|
|
13
|
+
interface FormInputProps<T extends FieldValues> extends Omit<
|
|
14
|
+
TextInputProps,
|
|
15
|
+
"value" | "onChangeText"
|
|
16
|
+
> {
|
|
15
17
|
name: Path<T>;
|
|
16
18
|
control: Control<T>;
|
|
17
19
|
rules?: RegisterOptions<T, Path<T>>;
|
|
@@ -29,7 +31,7 @@ interface FormInputProps<T extends FieldValues>
|
|
|
29
31
|
* Automatically handles validation errors and form state
|
|
30
32
|
*/
|
|
31
33
|
export const FormInput = forwardRef(function FormInputInner<
|
|
32
|
-
T extends FieldValues
|
|
34
|
+
T extends FieldValues,
|
|
33
35
|
>(
|
|
34
36
|
{
|
|
35
37
|
name,
|