@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,7 +1,12 @@
|
|
|
1
1
|
import * as SecureStore from "expo-secure-store";
|
|
2
2
|
import { router } from "expo-router";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ApiClient,
|
|
6
|
+
getTokens,
|
|
7
|
+
saveTokens,
|
|
8
|
+
getValidAccessToken,
|
|
9
|
+
} from "@/services/api";
|
|
5
10
|
|
|
6
11
|
// Mock dependencies
|
|
7
12
|
jest.mock("@/utils/toast", () => ({
|
|
@@ -12,8 +17,13 @@ jest.mock("@/utils/toast", () => ({
|
|
|
12
17
|
},
|
|
13
18
|
}));
|
|
14
19
|
|
|
20
|
+
jest.mock("i18next", () => ({
|
|
21
|
+
t: jest.fn((key: string) => key),
|
|
22
|
+
}));
|
|
23
|
+
|
|
15
24
|
jest.mock("@/constants/config", () => ({
|
|
16
25
|
API_URL: "https://api.example.com",
|
|
26
|
+
API_CONFIG: { ENABLE_ETAG_CACHE: false },
|
|
17
27
|
}));
|
|
18
28
|
|
|
19
29
|
const mockSecureStore = SecureStore as jest.Mocked<typeof SecureStore>;
|
|
@@ -26,7 +36,7 @@ const mockTokens = {
|
|
|
26
36
|
expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
|
|
27
37
|
};
|
|
28
38
|
|
|
29
|
-
const
|
|
39
|
+
const _expiredTokens = {
|
|
30
40
|
accessToken: "expired_access_token",
|
|
31
41
|
refreshToken: "expired_refresh_token",
|
|
32
42
|
expiresAt: Date.now() - 1000, // Already expired
|
|
@@ -262,7 +272,9 @@ describe("ApiClient", () => {
|
|
|
262
272
|
"Authentication failed"
|
|
263
273
|
);
|
|
264
274
|
|
|
265
|
-
expect(mockSecureStore.deleteItemAsync).toHaveBeenCalledWith(
|
|
275
|
+
expect(mockSecureStore.deleteItemAsync).toHaveBeenCalledWith(
|
|
276
|
+
"auth_tokens"
|
|
277
|
+
);
|
|
266
278
|
expect(mockSecureStore.deleteItemAsync).toHaveBeenCalledWith("auth_user");
|
|
267
279
|
expect(toast.error).toHaveBeenCalledWith(
|
|
268
280
|
"Session expired",
|
|
@@ -446,7 +458,9 @@ describe("ApiClient", () => {
|
|
|
446
458
|
text: () => Promise.resolve(""),
|
|
447
459
|
});
|
|
448
460
|
|
|
449
|
-
const result = await apiClient.delete("/users/1", {
|
|
461
|
+
const result = await apiClient.delete("/users/1", {
|
|
462
|
+
requiresAuth: false,
|
|
463
|
+
});
|
|
450
464
|
|
|
451
465
|
expect(result).toEqual({});
|
|
452
466
|
});
|
|
@@ -491,7 +505,9 @@ describe("Token Utilities", () => {
|
|
|
491
505
|
});
|
|
492
506
|
|
|
493
507
|
it("should return parsed tokens when stored", async () => {
|
|
494
|
-
mockSecureStore.getItemAsync.mockResolvedValue(
|
|
508
|
+
mockSecureStore.getItemAsync.mockResolvedValue(
|
|
509
|
+
JSON.stringify(mockTokens)
|
|
510
|
+
);
|
|
495
511
|
|
|
496
512
|
const tokens = await getTokens();
|
|
497
513
|
|
|
@@ -525,7 +541,9 @@ describe("Token Utilities", () => {
|
|
|
525
541
|
});
|
|
526
542
|
|
|
527
543
|
it("should return token when not expired", async () => {
|
|
528
|
-
mockSecureStore.getItemAsync.mockResolvedValue(
|
|
544
|
+
mockSecureStore.getItemAsync.mockResolvedValue(
|
|
545
|
+
JSON.stringify(mockTokens)
|
|
546
|
+
);
|
|
529
547
|
|
|
530
548
|
const token = await getValidAccessToken();
|
|
531
549
|
|
package/app/(auth)/home.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { View, Text, ScrollView } from "react-native";
|
|
|
2
2
|
import { Link } from "expo-router";
|
|
3
3
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
4
|
import { Ionicons } from "@expo/vector-icons";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
5
6
|
|
|
6
7
|
import { Card } from "@/components/ui/Card";
|
|
7
8
|
import { Button } from "@/components/ui/Button";
|
|
@@ -11,6 +12,7 @@ import { useTheme } from "@/hooks/useTheme";
|
|
|
11
12
|
export default function HomeScreen() {
|
|
12
13
|
const { user } = useAuth();
|
|
13
14
|
const { isDark } = useTheme();
|
|
15
|
+
const { t } = useTranslation();
|
|
14
16
|
|
|
15
17
|
return (
|
|
16
18
|
<SafeAreaView className="flex-1 bg-background-light dark:bg-background-dark">
|
|
@@ -19,7 +21,7 @@ export default function HomeScreen() {
|
|
|
19
21
|
<View className="mb-6 flex-row items-center justify-between">
|
|
20
22
|
<View>
|
|
21
23
|
<Text className="text-muted-light dark:text-muted-dark">
|
|
22
|
-
|
|
24
|
+
{t("home.welcomeBack", { name: user?.name || "User" })}
|
|
23
25
|
</Text>
|
|
24
26
|
<Text className="text-2xl font-bold text-text-light dark:text-text-dark">
|
|
25
27
|
{user?.name || "User"}
|
|
@@ -38,7 +40,7 @@ export default function HomeScreen() {
|
|
|
38
40
|
|
|
39
41
|
{/* Quick Actions */}
|
|
40
42
|
<Text className="mb-3 text-lg font-semibold text-text-light dark:text-text-dark">
|
|
41
|
-
|
|
43
|
+
{t("home.quickActions")}
|
|
42
44
|
</Text>
|
|
43
45
|
<View className="mb-6 flex-row gap-3">
|
|
44
46
|
<Card className="flex-1 items-center p-4">
|
|
@@ -50,7 +52,7 @@ export default function HomeScreen() {
|
|
|
50
52
|
/>
|
|
51
53
|
</View>
|
|
52
54
|
<Text className="text-sm text-text-light dark:text-text-dark">
|
|
53
|
-
|
|
55
|
+
{t("home.newItem")}
|
|
54
56
|
</Text>
|
|
55
57
|
</Card>
|
|
56
58
|
<Card className="flex-1 items-center p-4">
|
|
@@ -62,7 +64,7 @@ export default function HomeScreen() {
|
|
|
62
64
|
/>
|
|
63
65
|
</View>
|
|
64
66
|
<Text className="text-sm text-text-light dark:text-text-dark">
|
|
65
|
-
|
|
67
|
+
{t("common.search")}
|
|
66
68
|
</Text>
|
|
67
69
|
</Card>
|
|
68
70
|
<Card className="flex-1 items-center p-4">
|
|
@@ -74,14 +76,14 @@ export default function HomeScreen() {
|
|
|
74
76
|
/>
|
|
75
77
|
</View>
|
|
76
78
|
<Text className="text-sm text-text-light dark:text-text-dark">
|
|
77
|
-
|
|
79
|
+
{t("home.stats")}
|
|
78
80
|
</Text>
|
|
79
81
|
</Card>
|
|
80
82
|
</View>
|
|
81
83
|
|
|
82
84
|
{/* Recent Activity */}
|
|
83
85
|
<Text className="mb-3 text-lg font-semibold text-text-light dark:text-text-dark">
|
|
84
|
-
|
|
86
|
+
{t("home.recentActivity")}
|
|
85
87
|
</Text>
|
|
86
88
|
<Card className="mb-4 p-4">
|
|
87
89
|
<View className="items-center py-8">
|
|
@@ -91,10 +93,10 @@ export default function HomeScreen() {
|
|
|
91
93
|
color={isDark ? "#64748b" : "#94a3b8"}
|
|
92
94
|
/>
|
|
93
95
|
<Text className="mt-2 text-muted-light dark:text-muted-dark">
|
|
94
|
-
|
|
96
|
+
{t("home.noRecentActivity")}
|
|
95
97
|
</Text>
|
|
96
98
|
<Text className="mt-1 text-sm text-muted-light dark:text-muted-dark">
|
|
97
|
-
|
|
99
|
+
{t("home.activityWillAppear")}
|
|
98
100
|
</Text>
|
|
99
101
|
</View>
|
|
100
102
|
</Card>
|
|
@@ -108,7 +110,7 @@ export default function HomeScreen() {
|
|
|
108
110
|
color={isDark ? "#f8fafc" : "#0f172a"}
|
|
109
111
|
style={{ marginRight: 8 }}
|
|
110
112
|
/>
|
|
111
|
-
|
|
113
|
+
{t("settings.title")}
|
|
112
114
|
</Button>
|
|
113
115
|
</Link>
|
|
114
116
|
</ScrollView>
|
package/app/(auth)/profile.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { View, Text, ScrollView, Pressable } from "react-native";
|
|
|
2
2
|
import { router } from "expo-router";
|
|
3
3
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
4
|
import { Ionicons } from "@expo/vector-icons";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
5
6
|
|
|
6
7
|
import { Card } from "@/components/ui/Card";
|
|
7
8
|
import { Button } from "@/components/ui/Button";
|
|
@@ -11,6 +12,7 @@ import { useTheme } from "@/hooks/useTheme";
|
|
|
11
12
|
export default function ProfileScreen() {
|
|
12
13
|
const { user, signOut } = useAuth();
|
|
13
14
|
const { isDark } = useTheme();
|
|
15
|
+
const { t } = useTranslation();
|
|
14
16
|
|
|
15
17
|
const handleSignOut = async () => {
|
|
16
18
|
await signOut();
|
|
@@ -30,7 +32,7 @@ export default function ProfileScreen() {
|
|
|
30
32
|
/>
|
|
31
33
|
</Pressable>
|
|
32
34
|
<Text className="text-xl font-semibold text-text-light dark:text-text-dark">
|
|
33
|
-
|
|
35
|
+
{t("profile.title")}
|
|
34
36
|
</Text>
|
|
35
37
|
</View>
|
|
36
38
|
|
|
@@ -60,7 +62,7 @@ export default function ProfileScreen() {
|
|
|
60
62
|
color={isDark ? "#94a3b8" : "#64748b"}
|
|
61
63
|
/>
|
|
62
64
|
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
63
|
-
|
|
65
|
+
{t("profile.editProfile")}
|
|
64
66
|
</Text>
|
|
65
67
|
</View>
|
|
66
68
|
<Ionicons
|
|
@@ -80,7 +82,7 @@ export default function ProfileScreen() {
|
|
|
80
82
|
color={isDark ? "#94a3b8" : "#64748b"}
|
|
81
83
|
/>
|
|
82
84
|
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
83
|
-
|
|
85
|
+
{t("navigation.notifications")}
|
|
84
86
|
</Text>
|
|
85
87
|
</View>
|
|
86
88
|
<Ionicons
|
|
@@ -100,7 +102,7 @@ export default function ProfileScreen() {
|
|
|
100
102
|
color={isDark ? "#94a3b8" : "#64748b"}
|
|
101
103
|
/>
|
|
102
104
|
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
103
|
-
|
|
105
|
+
{t("profile.privacySecurity")}
|
|
104
106
|
</Text>
|
|
105
107
|
</View>
|
|
106
108
|
<Ionicons
|
|
@@ -120,7 +122,7 @@ export default function ProfileScreen() {
|
|
|
120
122
|
color={isDark ? "#94a3b8" : "#64748b"}
|
|
121
123
|
/>
|
|
122
124
|
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
123
|
-
|
|
125
|
+
{t("profile.helpSupport")}
|
|
124
126
|
</Text>
|
|
125
127
|
</View>
|
|
126
128
|
<Ionicons
|
|
@@ -143,7 +145,7 @@ export default function ProfileScreen() {
|
|
|
143
145
|
color="#ef4444"
|
|
144
146
|
style={{ marginRight: 8 }}
|
|
145
147
|
/>
|
|
146
|
-
<Text className="text-red-500">
|
|
148
|
+
<Text className="text-red-500">{t("auth.signOut")}</Text>
|
|
147
149
|
</Button>
|
|
148
150
|
</View>
|
|
149
151
|
</ScrollView>
|
package/app/(auth)/settings.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { View, Text, ScrollView, Pressable, Switch } from "react-native";
|
|
|
2
2
|
import { router } from "expo-router";
|
|
3
3
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
4
|
import { Ionicons } from "@expo/vector-icons";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
5
6
|
|
|
6
7
|
import { Card } from "@/components/ui/Card";
|
|
7
8
|
import { useTheme } from "@/hooks/useTheme";
|
|
@@ -10,6 +11,7 @@ import { useNotificationStore } from "@/stores/notificationStore";
|
|
|
10
11
|
export default function SettingsScreen() {
|
|
11
12
|
const { isDark, toggleTheme } = useTheme();
|
|
12
13
|
const { isEnabled, toggleNotifications } = useNotificationStore();
|
|
14
|
+
const { t } = useTranslation();
|
|
13
15
|
|
|
14
16
|
return (
|
|
15
17
|
<SafeAreaView className="flex-1 bg-background-light dark:bg-background-dark">
|
|
@@ -24,14 +26,14 @@ export default function SettingsScreen() {
|
|
|
24
26
|
/>
|
|
25
27
|
</Pressable>
|
|
26
28
|
<Text className="text-xl font-semibold text-text-light dark:text-text-dark">
|
|
27
|
-
|
|
29
|
+
{t("settings.title")}
|
|
28
30
|
</Text>
|
|
29
31
|
</View>
|
|
30
32
|
|
|
31
33
|
<View className="px-4 pt-4">
|
|
32
34
|
{/* Appearance */}
|
|
33
35
|
<Text className="mb-3 text-sm font-medium uppercase text-muted-light dark:text-muted-dark">
|
|
34
|
-
|
|
36
|
+
{t("settings.appearance")}
|
|
35
37
|
</Text>
|
|
36
38
|
<Card className="mb-6">
|
|
37
39
|
<View className="flex-row items-center justify-between p-4">
|
|
@@ -42,7 +44,7 @@ export default function SettingsScreen() {
|
|
|
42
44
|
color={isDark ? "#94a3b8" : "#64748b"}
|
|
43
45
|
/>
|
|
44
46
|
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
45
|
-
|
|
47
|
+
{t("settings.darkMode")}
|
|
46
48
|
</Text>
|
|
47
49
|
</View>
|
|
48
50
|
<Switch
|
|
@@ -56,7 +58,7 @@ export default function SettingsScreen() {
|
|
|
56
58
|
|
|
57
59
|
{/* Notifications */}
|
|
58
60
|
<Text className="mb-3 text-sm font-medium uppercase text-muted-light dark:text-muted-dark">
|
|
59
|
-
|
|
61
|
+
{t("settings.notifications")}
|
|
60
62
|
</Text>
|
|
61
63
|
<Card className="mb-6">
|
|
62
64
|
<View className="flex-row items-center justify-between p-4">
|
|
@@ -67,7 +69,7 @@ export default function SettingsScreen() {
|
|
|
67
69
|
color={isDark ? "#94a3b8" : "#64748b"}
|
|
68
70
|
/>
|
|
69
71
|
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
70
|
-
|
|
72
|
+
{t("settings.pushNotifications")}
|
|
71
73
|
</Text>
|
|
72
74
|
</View>
|
|
73
75
|
<Switch
|
|
@@ -81,7 +83,7 @@ export default function SettingsScreen() {
|
|
|
81
83
|
|
|
82
84
|
{/* About */}
|
|
83
85
|
<Text className="mb-3 text-sm font-medium uppercase text-muted-light dark:text-muted-dark">
|
|
84
|
-
|
|
86
|
+
{t("settings.about")}
|
|
85
87
|
</Text>
|
|
86
88
|
<Card className="mb-6">
|
|
87
89
|
<Pressable className="flex-row items-center justify-between p-4">
|
|
@@ -92,7 +94,7 @@ export default function SettingsScreen() {
|
|
|
92
94
|
color={isDark ? "#94a3b8" : "#64748b"}
|
|
93
95
|
/>
|
|
94
96
|
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
95
|
-
|
|
97
|
+
{t("settings.appVersion")}
|
|
96
98
|
</Text>
|
|
97
99
|
</View>
|
|
98
100
|
<Text className="text-muted-light dark:text-muted-dark">
|
|
@@ -110,7 +112,7 @@ export default function SettingsScreen() {
|
|
|
110
112
|
color={isDark ? "#94a3b8" : "#64748b"}
|
|
111
113
|
/>
|
|
112
114
|
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
113
|
-
|
|
115
|
+
{t("settings.terms")}
|
|
114
116
|
</Text>
|
|
115
117
|
</View>
|
|
116
118
|
<Ionicons
|
|
@@ -130,7 +132,7 @@ export default function SettingsScreen() {
|
|
|
130
132
|
color={isDark ? "#94a3b8" : "#64748b"}
|
|
131
133
|
/>
|
|
132
134
|
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
133
|
-
|
|
135
|
+
{t("settings.privacy")}
|
|
134
136
|
</Text>
|
|
135
137
|
</View>
|
|
136
138
|
<Ionicons
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
Pressable,
|
|
6
|
+
KeyboardAvoidingView,
|
|
7
|
+
Platform,
|
|
8
|
+
} from "react-native";
|
|
3
9
|
import { Link, router } from "expo-router";
|
|
4
10
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
5
11
|
import { Ionicons } from "@expo/vector-icons";
|
|
12
|
+
import { useTranslation } from "react-i18next";
|
|
6
13
|
|
|
7
14
|
import { Input } from "@/components/ui/Input";
|
|
8
15
|
import { Button } from "@/components/ui/Button";
|
|
@@ -12,10 +19,11 @@ export default function ForgotPasswordScreen() {
|
|
|
12
19
|
const [error, setError] = useState("");
|
|
13
20
|
const [success, setSuccess] = useState(false);
|
|
14
21
|
const [isLoading, setIsLoading] = useState(false);
|
|
22
|
+
const { t } = useTranslation();
|
|
15
23
|
|
|
16
24
|
const handleResetPassword = async () => {
|
|
17
25
|
if (!email) {
|
|
18
|
-
setError("
|
|
26
|
+
setError(t("forgotPassword.emailRequired"));
|
|
19
27
|
return;
|
|
20
28
|
}
|
|
21
29
|
|
|
@@ -27,8 +35,8 @@ export default function ForgotPasswordScreen() {
|
|
|
27
35
|
// await api.resetPassword(email);
|
|
28
36
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
29
37
|
setSuccess(true);
|
|
30
|
-
} catch
|
|
31
|
-
setError("
|
|
38
|
+
} catch {
|
|
39
|
+
setError(t("forgotPassword.sendFailed"));
|
|
32
40
|
} finally {
|
|
33
41
|
setIsLoading(false);
|
|
34
42
|
}
|
|
@@ -42,16 +50,16 @@ export default function ForgotPasswordScreen() {
|
|
|
42
50
|
<Ionicons name="mail-outline" size={40} color="#22c55e" />
|
|
43
51
|
</View>
|
|
44
52
|
<Text className="text-center text-2xl font-bold text-text-light dark:text-text-dark">
|
|
45
|
-
|
|
53
|
+
{t("forgotPassword.checkEmail")}
|
|
46
54
|
</Text>
|
|
47
55
|
<Text className="mt-2 text-center text-muted-light dark:text-muted-dark">
|
|
48
|
-
|
|
56
|
+
{t("forgotPassword.resetSent", { email })}
|
|
49
57
|
</Text>
|
|
50
58
|
<Button
|
|
51
59
|
onPress={() => router.replace("/(public)/login")}
|
|
52
60
|
className="mt-8"
|
|
53
61
|
>
|
|
54
|
-
|
|
62
|
+
{t("forgotPassword.backToSignIn")}
|
|
55
63
|
</Button>
|
|
56
64
|
</View>
|
|
57
65
|
</SafeAreaView>
|
|
@@ -70,15 +78,17 @@ export default function ForgotPasswordScreen() {
|
|
|
70
78
|
className="mb-8 flex-row items-center"
|
|
71
79
|
>
|
|
72
80
|
<Ionicons name="arrow-back" size={24} color="#64748b" />
|
|
73
|
-
<Text className="ml-2 text-muted-light dark:text-muted-dark">
|
|
81
|
+
<Text className="ml-2 text-muted-light dark:text-muted-dark">
|
|
82
|
+
{t("common.back")}
|
|
83
|
+
</Text>
|
|
74
84
|
</Pressable>
|
|
75
85
|
|
|
76
86
|
<View className="mb-8">
|
|
77
87
|
<Text className="text-3xl font-bold text-text-light dark:text-text-dark">
|
|
78
|
-
|
|
88
|
+
{t("forgotPassword.title")}
|
|
79
89
|
</Text>
|
|
80
90
|
<Text className="mt-2 text-muted-light dark:text-muted-dark">
|
|
81
|
-
|
|
91
|
+
{t("forgotPassword.subtitle")}
|
|
82
92
|
</Text>
|
|
83
93
|
</View>
|
|
84
94
|
|
|
@@ -90,8 +100,8 @@ export default function ForgotPasswordScreen() {
|
|
|
90
100
|
|
|
91
101
|
<View className="gap-4">
|
|
92
102
|
<Input
|
|
93
|
-
label="
|
|
94
|
-
placeholder="
|
|
103
|
+
label={t("auth.email")}
|
|
104
|
+
placeholder={t("auth.enterEmail")}
|
|
95
105
|
value={email}
|
|
96
106
|
onChangeText={setEmail}
|
|
97
107
|
keyboardType="email-address"
|
|
@@ -104,18 +114,18 @@ export default function ForgotPasswordScreen() {
|
|
|
104
114
|
isLoading={isLoading}
|
|
105
115
|
className="mt-4"
|
|
106
116
|
>
|
|
107
|
-
|
|
117
|
+
{t("forgotPassword.sendResetLink")}
|
|
108
118
|
</Button>
|
|
109
119
|
</View>
|
|
110
120
|
|
|
111
121
|
<View className="mt-8 flex-row justify-center">
|
|
112
122
|
<Text className="text-muted-light dark:text-muted-dark">
|
|
113
|
-
|
|
123
|
+
{t("forgotPassword.rememberPassword")}{" "}
|
|
114
124
|
</Text>
|
|
115
125
|
<Link href="/(public)/login" asChild>
|
|
116
126
|
<Pressable>
|
|
117
127
|
<Text className="font-semibold text-primary-600 dark:text-primary-400">
|
|
118
|
-
|
|
128
|
+
{t("auth.signIn")}
|
|
119
129
|
</Text>
|
|
120
130
|
</Pressable>
|
|
121
131
|
</Link>
|
package/app/(public)/login.tsx
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
View,
|
|
3
|
+
Text,
|
|
4
|
+
Pressable,
|
|
5
|
+
KeyboardAvoidingView,
|
|
6
|
+
Platform,
|
|
7
|
+
} from "react-native";
|
|
2
8
|
import { Link, router } from "expo-router";
|
|
3
9
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
10
|
import { useForm } from "react-hook-form";
|
|
5
11
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
6
12
|
import Animated, { FadeInDown } from "react-native-reanimated";
|
|
13
|
+
import { useTranslation } from "react-i18next";
|
|
7
14
|
|
|
8
15
|
import { FormInput } from "@/components/forms/FormInput";
|
|
16
|
+
import { SocialLoginButtons } from "@/components/auth/SocialLoginButtons";
|
|
9
17
|
import { AnimatedButton } from "@/components/ui/AnimatedButton";
|
|
10
18
|
import { useAuth } from "@/hooks/useAuth";
|
|
11
19
|
import { loginSchema, LoginFormData } from "@/utils/validation";
|
|
12
20
|
|
|
13
21
|
export default function LoginScreen() {
|
|
14
22
|
const { signIn } = useAuth();
|
|
23
|
+
const { t } = useTranslation();
|
|
15
24
|
|
|
16
25
|
const {
|
|
17
26
|
control,
|
|
@@ -47,10 +56,10 @@ export default function LoginScreen() {
|
|
|
47
56
|
className="mb-8"
|
|
48
57
|
>
|
|
49
58
|
<Text className="text-3xl font-bold text-text-light dark:text-text-dark">
|
|
50
|
-
|
|
59
|
+
{t("auth.welcomeBack")}
|
|
51
60
|
</Text>
|
|
52
61
|
<Text className="mt-2 text-muted-light dark:text-muted-dark">
|
|
53
|
-
|
|
62
|
+
{t("auth.signInToContinue")}
|
|
54
63
|
</Text>
|
|
55
64
|
</Animated.View>
|
|
56
65
|
|
|
@@ -62,8 +71,8 @@ export default function LoginScreen() {
|
|
|
62
71
|
<FormInput
|
|
63
72
|
name="email"
|
|
64
73
|
control={control}
|
|
65
|
-
label="
|
|
66
|
-
placeholder="
|
|
74
|
+
label={t("auth.email")}
|
|
75
|
+
placeholder={t("auth.enterEmail")}
|
|
67
76
|
keyboardType="email-address"
|
|
68
77
|
autoCapitalize="none"
|
|
69
78
|
autoComplete="email"
|
|
@@ -73,8 +82,8 @@ export default function LoginScreen() {
|
|
|
73
82
|
<FormInput
|
|
74
83
|
name="password"
|
|
75
84
|
control={control}
|
|
76
|
-
label="
|
|
77
|
-
placeholder="
|
|
85
|
+
label={t("auth.password")}
|
|
86
|
+
placeholder={t("auth.enterPassword")}
|
|
78
87
|
secureTextEntry
|
|
79
88
|
autoComplete="password"
|
|
80
89
|
leftIcon="lock-closed-outline"
|
|
@@ -83,7 +92,7 @@ export default function LoginScreen() {
|
|
|
83
92
|
<Link href="/(public)/forgot-password" asChild>
|
|
84
93
|
<Pressable className="self-end">
|
|
85
94
|
<Text className="text-primary-600 dark:text-primary-400">
|
|
86
|
-
|
|
95
|
+
{t("auth.forgotPassword")}
|
|
87
96
|
</Text>
|
|
88
97
|
</Pressable>
|
|
89
98
|
</Link>
|
|
@@ -93,22 +102,49 @@ export default function LoginScreen() {
|
|
|
93
102
|
isLoading={isSubmitting}
|
|
94
103
|
className="mt-4"
|
|
95
104
|
>
|
|
96
|
-
|
|
105
|
+
{t("auth.signIn")}
|
|
97
106
|
</AnimatedButton>
|
|
98
107
|
</Animated.View>
|
|
99
108
|
|
|
100
|
-
{/*
|
|
109
|
+
{/* Divider */}
|
|
101
110
|
<Animated.View
|
|
102
111
|
entering={FadeInDown.delay(300).springify()}
|
|
112
|
+
className="my-6 flex-row items-center"
|
|
113
|
+
>
|
|
114
|
+
<View className="h-px flex-1 bg-muted-light/30 dark:bg-muted-dark/30" />
|
|
115
|
+
<Text className="mx-4 text-muted-light dark:text-muted-dark">
|
|
116
|
+
{t("socialAuth.orContinueWith")}
|
|
117
|
+
</Text>
|
|
118
|
+
<View className="h-px flex-1 bg-muted-light/30 dark:bg-muted-dark/30" />
|
|
119
|
+
</Animated.View>
|
|
120
|
+
|
|
121
|
+
{/* Social Login */}
|
|
122
|
+
<Animated.View entering={FadeInDown.delay(400).springify()}>
|
|
123
|
+
<SocialLoginButtons
|
|
124
|
+
onSuccess={(result) => {
|
|
125
|
+
if (__DEV__)
|
|
126
|
+
console.log(
|
|
127
|
+
"Social login succeeded:",
|
|
128
|
+
result.provider,
|
|
129
|
+
result.user.email
|
|
130
|
+
);
|
|
131
|
+
// TODO: Send result.idToken to your backend
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
</Animated.View>
|
|
135
|
+
|
|
136
|
+
{/* Footer */}
|
|
137
|
+
<Animated.View
|
|
138
|
+
entering={FadeInDown.delay(500).springify()}
|
|
103
139
|
className="mt-8 flex-row justify-center"
|
|
104
140
|
>
|
|
105
141
|
<Text className="text-muted-light dark:text-muted-dark">
|
|
106
|
-
|
|
142
|
+
{t("auth.noAccount")}{" "}
|
|
107
143
|
</Text>
|
|
108
144
|
<Link href="/(public)/register" asChild>
|
|
109
145
|
<Pressable>
|
|
110
146
|
<Text className="font-semibold text-primary-600 dark:text-primary-400">
|
|
111
|
-
|
|
147
|
+
{t("auth.signUp")}
|
|
112
148
|
</Text>
|
|
113
149
|
</Pressable>
|
|
114
150
|
</Link>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { OnboardingScreen } from "@/components/onboarding";
|
|
2
|
-
|
|
3
|
-
export default function Onboarding() {
|
|
4
|
-
return <OnboardingScreen />;
|
|
5
|
-
}
|
|
1
|
+
import { OnboardingScreen } from "@/components/onboarding";
|
|
2
|
+
|
|
3
|
+
export default function Onboarding() {
|
|
4
|
+
return <OnboardingScreen />;
|
|
5
|
+
}
|