@croacroa/react-native-template 1.0.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 +18 -0
- package/.eslintrc.js +55 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/eas-build.yml +55 -0
- package/.github/workflows/eas-update.yml +50 -0
- package/.gitignore +62 -0
- package/.prettierrc +11 -0
- package/.storybook/main.ts +28 -0
- package/.storybook/preview.tsx +30 -0
- package/CHANGELOG.md +106 -0
- package/CONTRIBUTING.md +377 -0
- package/README.md +399 -0
- package/__tests__/components/Button.test.tsx +74 -0
- package/__tests__/hooks/useAuth.test.tsx +499 -0
- package/__tests__/services/api.test.ts +535 -0
- package/__tests__/utils/cn.test.ts +39 -0
- package/app/(auth)/_layout.tsx +36 -0
- package/app/(auth)/home.tsx +117 -0
- package/app/(auth)/profile.tsx +152 -0
- package/app/(auth)/settings.tsx +147 -0
- package/app/(public)/_layout.tsx +21 -0
- package/app/(public)/forgot-password.tsx +127 -0
- package/app/(public)/login.tsx +120 -0
- package/app/(public)/onboarding.tsx +5 -0
- package/app/(public)/register.tsx +139 -0
- package/app/_layout.tsx +97 -0
- package/app/index.tsx +21 -0
- package/app.config.ts +72 -0
- package/assets/images/.gitkeep +7 -0
- 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/babel.config.js +10 -0
- package/components/ErrorBoundary.tsx +169 -0
- package/components/forms/FormInput.tsx +78 -0
- package/components/forms/index.ts +1 -0
- package/components/onboarding/OnboardingScreen.tsx +370 -0
- package/components/onboarding/index.ts +2 -0
- package/components/ui/AnimatedButton.tsx +156 -0
- package/components/ui/AnimatedCard.tsx +108 -0
- package/components/ui/Avatar.tsx +316 -0
- package/components/ui/Badge.tsx +416 -0
- package/components/ui/BottomSheet.tsx +307 -0
- package/components/ui/Button.stories.tsx +115 -0
- package/components/ui/Button.tsx +104 -0
- package/components/ui/Card.stories.tsx +84 -0
- package/components/ui/Card.tsx +32 -0
- package/components/ui/Checkbox.tsx +261 -0
- package/components/ui/Input.stories.tsx +106 -0
- package/components/ui/Input.tsx +117 -0
- package/components/ui/Modal.tsx +98 -0
- package/components/ui/OptimizedImage.tsx +369 -0
- package/components/ui/Select.tsx +240 -0
- package/components/ui/Skeleton.tsx +180 -0
- package/components/ui/index.ts +18 -0
- package/constants/config.ts +54 -0
- package/docs/adr/001-state-management.md +79 -0
- package/docs/adr/002-styling-approach.md +130 -0
- package/docs/adr/003-data-fetching.md +155 -0
- package/docs/adr/004-auth-adapter-pattern.md +144 -0
- package/docs/adr/README.md +78 -0
- package/eas.json +47 -0
- package/global.css +10 -0
- package/hooks/index.ts +25 -0
- package/hooks/useApi.ts +236 -0
- package/hooks/useAuth.tsx +290 -0
- package/hooks/useBiometrics.ts +295 -0
- package/hooks/useDeepLinking.ts +256 -0
- package/hooks/useNotifications.ts +138 -0
- package/hooks/useOffline.ts +69 -0
- package/hooks/usePerformance.ts +434 -0
- package/hooks/useTheme.tsx +85 -0
- package/hooks/useUpdates.ts +358 -0
- package/i18n/index.ts +77 -0
- package/i18n/locales/en.json +101 -0
- package/i18n/locales/fr.json +101 -0
- package/jest.config.js +32 -0
- package/maestro/README.md +113 -0
- package/maestro/config.yaml +35 -0
- package/maestro/flows/login.yaml +62 -0
- package/maestro/flows/navigation.yaml +68 -0
- package/maestro/flows/offline.yaml +60 -0
- package/maestro/flows/register.yaml +94 -0
- package/metro.config.js +6 -0
- package/nativewind-env.d.ts +1 -0
- package/package.json +170 -0
- package/scripts/init.ps1 +162 -0
- package/scripts/init.sh +174 -0
- package/services/analytics.ts +428 -0
- package/services/api.ts +340 -0
- package/services/authAdapter.ts +333 -0
- package/services/index.ts +22 -0
- package/services/queryClient.ts +97 -0
- package/services/sentry.ts +131 -0
- package/services/storage.ts +82 -0
- package/stores/appStore.ts +54 -0
- package/stores/index.ts +2 -0
- package/stores/notificationStore.ts +40 -0
- package/tailwind.config.js +47 -0
- package/tsconfig.json +26 -0
- package/types/index.ts +42 -0
- package/types/user.ts +63 -0
- package/utils/accessibility.ts +446 -0
- package/utils/cn.ts +14 -0
- package/utils/index.ts +43 -0
- package/utils/toast.ts +113 -0
- package/utils/validation.ts +67 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
|
|
3
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
4
|
+
import NetInfo from "@react-native-community/netinfo";
|
|
5
|
+
import { onlineManager, focusManager } from "@tanstack/react-query";
|
|
6
|
+
import { AppState, Platform } from "react-native";
|
|
7
|
+
import type { AppStateStatus } from "react-native";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configure online state management
|
|
11
|
+
* TanStack Query will pause queries when offline and resume when online
|
|
12
|
+
*/
|
|
13
|
+
export function setupOnlineManager() {
|
|
14
|
+
onlineManager.setEventListener((setOnline) => {
|
|
15
|
+
return NetInfo.addEventListener((state) => {
|
|
16
|
+
setOnline(!!state.isConnected);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configure focus management
|
|
23
|
+
* Refetch queries when app comes to foreground
|
|
24
|
+
*/
|
|
25
|
+
export function setupFocusManager() {
|
|
26
|
+
function onAppStateChange(status: AppStateStatus) {
|
|
27
|
+
if (Platform.OS !== "web") {
|
|
28
|
+
focusManager.setFocused(status === "active");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const subscription = AppState.addEventListener("change", onAppStateChange);
|
|
33
|
+
return () => subscription.remove();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create the query client with production-ready defaults
|
|
38
|
+
*/
|
|
39
|
+
export function createQueryClient() {
|
|
40
|
+
return new QueryClient({
|
|
41
|
+
defaultOptions: {
|
|
42
|
+
queries: {
|
|
43
|
+
// Cache data for 5 minutes by default
|
|
44
|
+
staleTime: 1000 * 60 * 5,
|
|
45
|
+
// Keep unused data in cache for 30 minutes
|
|
46
|
+
gcTime: 1000 * 60 * 30,
|
|
47
|
+
// Retry failed requests up to 3 times
|
|
48
|
+
retry: 3,
|
|
49
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
50
|
+
// Don't refetch on window focus in dev (annoying)
|
|
51
|
+
refetchOnWindowFocus: !__DEV__,
|
|
52
|
+
// Refetch on reconnect
|
|
53
|
+
refetchOnReconnect: true,
|
|
54
|
+
// Keep previous data while fetching new data
|
|
55
|
+
placeholderData: (previousData: unknown) => previousData,
|
|
56
|
+
},
|
|
57
|
+
mutations: {
|
|
58
|
+
// Retry mutations once on failure
|
|
59
|
+
retry: 1,
|
|
60
|
+
retryDelay: 1000,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create the async storage persister for offline support
|
|
68
|
+
*/
|
|
69
|
+
export function createPersister() {
|
|
70
|
+
return createAsyncStoragePersister({
|
|
71
|
+
storage: AsyncStorage,
|
|
72
|
+
key: "REACT_QUERY_CACHE",
|
|
73
|
+
// Throttle writes to storage (1 second)
|
|
74
|
+
throttleTime: 1000,
|
|
75
|
+
// Serialize/deserialize functions
|
|
76
|
+
serialize: JSON.stringify,
|
|
77
|
+
deserialize: JSON.parse,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Default persist options
|
|
83
|
+
*/
|
|
84
|
+
export const persistOptions = {
|
|
85
|
+
persister: createPersister(),
|
|
86
|
+
// Maximum age of persisted data (24 hours)
|
|
87
|
+
maxAge: 1000 * 60 * 60 * 24,
|
|
88
|
+
// Only persist successful queries
|
|
89
|
+
dehydrateOptions: {
|
|
90
|
+
shouldDehydrateQuery: (query: { state: { status: string } }) => {
|
|
91
|
+
return query.state.status === "success";
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Pre-configured query client singleton
|
|
97
|
+
export const queryClient = createQueryClient();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/react-native";
|
|
2
|
+
import { IS_DEV, IS_PROD, APP_VERSION } from "@/constants/config";
|
|
3
|
+
|
|
4
|
+
// TODO: Replace with your actual Sentry DSN
|
|
5
|
+
// Get it from: https://sentry.io -> Project Settings -> Client Keys (DSN)
|
|
6
|
+
const SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN || "";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initialize Sentry error tracking
|
|
10
|
+
* Call this early in app startup (before RootLayout)
|
|
11
|
+
*/
|
|
12
|
+
export function initSentry() {
|
|
13
|
+
if (!SENTRY_DSN) {
|
|
14
|
+
if (IS_DEV) {
|
|
15
|
+
console.log("[Sentry] No DSN configured, skipping initialization");
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Sentry.init({
|
|
21
|
+
dsn: SENTRY_DSN,
|
|
22
|
+
debug: IS_DEV,
|
|
23
|
+
enabled: !IS_DEV, // Only enable in non-dev environments
|
|
24
|
+
environment: IS_PROD ? "production" : "staging",
|
|
25
|
+
release: APP_VERSION,
|
|
26
|
+
|
|
27
|
+
// Performance Monitoring
|
|
28
|
+
tracesSampleRate: IS_PROD ? 0.2 : 1.0, // 20% in prod, 100% in staging
|
|
29
|
+
|
|
30
|
+
// Session Replay (if needed)
|
|
31
|
+
// replaysSessionSampleRate: 0.1,
|
|
32
|
+
// replaysOnErrorSampleRate: 1.0,
|
|
33
|
+
|
|
34
|
+
// Integrations
|
|
35
|
+
integrations: [
|
|
36
|
+
Sentry.reactNativeTracingIntegration(),
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
// Filter out certain errors
|
|
40
|
+
beforeSend(event) {
|
|
41
|
+
// Don't send events in dev
|
|
42
|
+
if (IS_DEV) {
|
|
43
|
+
console.log("[Sentry] Would send event:", event);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Filter out network errors that are expected
|
|
48
|
+
if (event.message?.includes("Network request failed")) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return event;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Capture an exception with optional context
|
|
59
|
+
*/
|
|
60
|
+
export function captureException(
|
|
61
|
+
error: Error,
|
|
62
|
+
context?: Record<string, unknown>
|
|
63
|
+
) {
|
|
64
|
+
if (IS_DEV) {
|
|
65
|
+
console.error("[Sentry] Exception:", error, context);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Sentry.captureException(error, {
|
|
70
|
+
extra: context,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Capture a message with optional level
|
|
76
|
+
*/
|
|
77
|
+
export function captureMessage(
|
|
78
|
+
message: string,
|
|
79
|
+
level: Sentry.SeverityLevel = "info"
|
|
80
|
+
) {
|
|
81
|
+
if (IS_DEV) {
|
|
82
|
+
console.log(`[Sentry] ${level}: ${message}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Sentry.captureMessage(message, level);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set user context for error tracking
|
|
91
|
+
*/
|
|
92
|
+
export function setUser(user: { id: string; email?: string; name?: string } | null) {
|
|
93
|
+
if (user) {
|
|
94
|
+
Sentry.setUser({
|
|
95
|
+
id: user.id,
|
|
96
|
+
email: user.email,
|
|
97
|
+
username: user.name,
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
Sentry.setUser(null);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Add breadcrumb for debugging
|
|
106
|
+
*/
|
|
107
|
+
export function addBreadcrumb(
|
|
108
|
+
category: string,
|
|
109
|
+
message: string,
|
|
110
|
+
data?: Record<string, unknown>
|
|
111
|
+
) {
|
|
112
|
+
Sentry.addBreadcrumb({
|
|
113
|
+
category,
|
|
114
|
+
message,
|
|
115
|
+
data,
|
|
116
|
+
level: "info",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Start a performance transaction
|
|
122
|
+
*/
|
|
123
|
+
export function startTransaction(name: string, op: string) {
|
|
124
|
+
return Sentry.startInactiveSpan({
|
|
125
|
+
name,
|
|
126
|
+
op,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Re-export Sentry for advanced usage
|
|
131
|
+
export { Sentry };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
2
|
+
import * as SecureStore from "expo-secure-store";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Regular storage for non-sensitive data
|
|
6
|
+
* Uses AsyncStorage under the hood
|
|
7
|
+
*/
|
|
8
|
+
export const storage = {
|
|
9
|
+
async get<T>(key: string): Promise<T | null> {
|
|
10
|
+
try {
|
|
11
|
+
const value = await AsyncStorage.getItem(key);
|
|
12
|
+
return value ? JSON.parse(value) : null;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error(`Failed to get ${key} from storage:`, error);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async set<T>(key: string, value: T): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
await AsyncStorage.setItem(key, JSON.stringify(value));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(`Failed to set ${key} in storage:`, error);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async remove(key: string): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
await AsyncStorage.removeItem(key);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(`Failed to remove ${key} from storage:`, error);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async clear(): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
await AsyncStorage.clear();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("Failed to clear storage:", error);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async getAllKeys(): Promise<readonly string[]> {
|
|
44
|
+
try {
|
|
45
|
+
return await AsyncStorage.getAllKeys();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("Failed to get all keys from storage:", error);
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Secure storage for sensitive data (tokens, credentials)
|
|
55
|
+
* Uses expo-secure-store (Keychain on iOS, EncryptedSharedPreferences on Android)
|
|
56
|
+
*/
|
|
57
|
+
export const secureStorage = {
|
|
58
|
+
async get(key: string): Promise<string | null> {
|
|
59
|
+
try {
|
|
60
|
+
return await SecureStore.getItemAsync(key);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(`Failed to get ${key} from secure storage:`, error);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async set(key: string, value: string): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
await SecureStore.setItemAsync(key, value);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(`Failed to set ${key} in secure storage:`, error);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async remove(key: string): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
await SecureStore.deleteItemAsync(key);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(`Failed to remove ${key} from secure storage:`, error);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { persist, createJSONStorage } from "zustand/middleware";
|
|
3
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
4
|
+
|
|
5
|
+
interface AppState {
|
|
6
|
+
// App-wide loading state
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
setIsLoading: (loading: boolean) => void;
|
|
9
|
+
|
|
10
|
+
// Onboarding
|
|
11
|
+
hasCompletedOnboarding: boolean;
|
|
12
|
+
setHasCompletedOnboarding: (completed: boolean) => void;
|
|
13
|
+
|
|
14
|
+
// Feature flags (example)
|
|
15
|
+
featureFlags: Record<string, boolean>;
|
|
16
|
+
setFeatureFlag: (key: string, value: boolean) => void;
|
|
17
|
+
|
|
18
|
+
// Reset all state
|
|
19
|
+
reset: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const initialState = {
|
|
23
|
+
isLoading: false,
|
|
24
|
+
hasCompletedOnboarding: false,
|
|
25
|
+
featureFlags: {},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const useAppStore = create<AppState>()(
|
|
29
|
+
persist(
|
|
30
|
+
(set) => ({
|
|
31
|
+
...initialState,
|
|
32
|
+
|
|
33
|
+
setIsLoading: (loading) => set({ isLoading: loading }),
|
|
34
|
+
|
|
35
|
+
setHasCompletedOnboarding: (completed) =>
|
|
36
|
+
set({ hasCompletedOnboarding: completed }),
|
|
37
|
+
|
|
38
|
+
setFeatureFlag: (key, value) =>
|
|
39
|
+
set((state) => ({
|
|
40
|
+
featureFlags: { ...state.featureFlags, [key]: value },
|
|
41
|
+
})),
|
|
42
|
+
|
|
43
|
+
reset: () => set(initialState),
|
|
44
|
+
}),
|
|
45
|
+
{
|
|
46
|
+
name: "app-storage",
|
|
47
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
48
|
+
partialize: (state) => ({
|
|
49
|
+
hasCompletedOnboarding: state.hasCompletedOnboarding,
|
|
50
|
+
featureFlags: state.featureFlags,
|
|
51
|
+
}),
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
);
|
package/stores/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { persist, createJSONStorage } from "zustand/middleware";
|
|
3
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
4
|
+
import * as Notifications from "expo-notifications";
|
|
5
|
+
|
|
6
|
+
interface NotificationState {
|
|
7
|
+
pushToken: string | null;
|
|
8
|
+
isEnabled: boolean;
|
|
9
|
+
lastNotification: Notifications.Notification | null;
|
|
10
|
+
setPushToken: (token: string | null) => void;
|
|
11
|
+
setIsEnabled: (enabled: boolean) => void;
|
|
12
|
+
toggleNotifications: () => void;
|
|
13
|
+
setLastNotification: (notification: Notifications.Notification | null) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useNotificationStore = create<NotificationState>()(
|
|
17
|
+
persist(
|
|
18
|
+
(set, get) => ({
|
|
19
|
+
pushToken: null,
|
|
20
|
+
isEnabled: true,
|
|
21
|
+
lastNotification: null,
|
|
22
|
+
|
|
23
|
+
setPushToken: (token) => set({ pushToken: token }),
|
|
24
|
+
|
|
25
|
+
setIsEnabled: (enabled) => set({ isEnabled: enabled }),
|
|
26
|
+
|
|
27
|
+
toggleNotifications: () => set({ isEnabled: !get().isEnabled }),
|
|
28
|
+
|
|
29
|
+
setLastNotification: (notification) =>
|
|
30
|
+
set({ lastNotification: notification }),
|
|
31
|
+
}),
|
|
32
|
+
{
|
|
33
|
+
name: "notification-storage",
|
|
34
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
35
|
+
partialize: (state) => ({
|
|
36
|
+
isEnabled: state.isEnabled,
|
|
37
|
+
}),
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
content: [
|
|
4
|
+
"./app/**/*.{js,jsx,ts,tsx}",
|
|
5
|
+
"./components/**/*.{js,jsx,ts,tsx}",
|
|
6
|
+
],
|
|
7
|
+
presets: [require("nativewind/preset")],
|
|
8
|
+
theme: {
|
|
9
|
+
extend: {
|
|
10
|
+
colors: {
|
|
11
|
+
// Croacroa emerald green
|
|
12
|
+
primary: {
|
|
13
|
+
50: "#ecfdf5",
|
|
14
|
+
100: "#d1fae5",
|
|
15
|
+
200: "#a7f3d0",
|
|
16
|
+
300: "#6ee7b7",
|
|
17
|
+
400: "#34d399",
|
|
18
|
+
500: "#10b981",
|
|
19
|
+
600: "#059669",
|
|
20
|
+
700: "#047857",
|
|
21
|
+
800: "#065f46",
|
|
22
|
+
900: "#064e3b",
|
|
23
|
+
},
|
|
24
|
+
background: {
|
|
25
|
+
light: "#ffffff",
|
|
26
|
+
dark: "#0f172a",
|
|
27
|
+
},
|
|
28
|
+
surface: {
|
|
29
|
+
light: "#f8fafc",
|
|
30
|
+
dark: "#1e293b",
|
|
31
|
+
},
|
|
32
|
+
text: {
|
|
33
|
+
light: "#0f172a",
|
|
34
|
+
dark: "#f8fafc",
|
|
35
|
+
},
|
|
36
|
+
muted: {
|
|
37
|
+
light: "#64748b",
|
|
38
|
+
dark: "#94a3b8",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
fontFamily: {
|
|
42
|
+
sans: ["Inter", "system-ui", "sans-serif"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
plugins: [],
|
|
47
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "expo/tsconfig.base",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"strict": true,
|
|
5
|
+
"baseUrl": ".",
|
|
6
|
+
"paths": {
|
|
7
|
+
"@/*": ["./*"],
|
|
8
|
+
"@/components/*": ["components/*"],
|
|
9
|
+
"@/hooks/*": ["hooks/*"],
|
|
10
|
+
"@/stores/*": ["stores/*"],
|
|
11
|
+
"@/services/*": ["services/*"],
|
|
12
|
+
"@/theme/*": ["theme/*"],
|
|
13
|
+
"@/constants/*": ["constants/*"],
|
|
14
|
+
"@/utils/*": ["utils/*"],
|
|
15
|
+
"@/types/*": ["types/*"]
|
|
16
|
+
},
|
|
17
|
+
"types": ["jest", "@testing-library/jest-native"]
|
|
18
|
+
},
|
|
19
|
+
"include": [
|
|
20
|
+
"**/*.ts",
|
|
21
|
+
"**/*.tsx",
|
|
22
|
+
".expo/types/**/*.ts",
|
|
23
|
+
"expo-env.d.ts"
|
|
24
|
+
],
|
|
25
|
+
"exclude": ["node_modules"]
|
|
26
|
+
}
|
package/types/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Re-export all types from a single entry point
|
|
2
|
+
export * from "./user";
|
|
3
|
+
|
|
4
|
+
// API Response types
|
|
5
|
+
export interface ApiResponse<T> {
|
|
6
|
+
data: T;
|
|
7
|
+
message?: string;
|
|
8
|
+
success: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PaginatedResponse<T> {
|
|
12
|
+
data: T[];
|
|
13
|
+
page: number;
|
|
14
|
+
pageSize: number;
|
|
15
|
+
total: number;
|
|
16
|
+
totalPages: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Error types
|
|
20
|
+
export interface ApiError {
|
|
21
|
+
message: string;
|
|
22
|
+
code?: string;
|
|
23
|
+
status: number;
|
|
24
|
+
details?: Record<string, string[]>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Navigation types (extend as needed)
|
|
28
|
+
export type RootStackParamList = {
|
|
29
|
+
"(public)/login": undefined;
|
|
30
|
+
"(public)/register": undefined;
|
|
31
|
+
"(public)/forgot-password": undefined;
|
|
32
|
+
"(auth)/home": undefined;
|
|
33
|
+
"(auth)/profile": undefined;
|
|
34
|
+
"(auth)/settings": undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Notification types
|
|
38
|
+
export interface NotificationData {
|
|
39
|
+
type: "message" | "alert" | "promotion";
|
|
40
|
+
screen?: string;
|
|
41
|
+
params?: Record<string, unknown>;
|
|
42
|
+
}
|
package/types/user.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized User types - Single source of truth
|
|
3
|
+
* Import from here across the entire app
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface User {
|
|
7
|
+
id: string;
|
|
8
|
+
email: string;
|
|
9
|
+
name: string;
|
|
10
|
+
avatar?: string;
|
|
11
|
+
createdAt?: string;
|
|
12
|
+
updatedAt?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AuthTokens {
|
|
16
|
+
accessToken: string;
|
|
17
|
+
refreshToken: string;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthState {
|
|
22
|
+
user: User | null;
|
|
23
|
+
tokens: AuthTokens | null;
|
|
24
|
+
isAuthenticated: boolean;
|
|
25
|
+
isLoading: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// API DTOs
|
|
29
|
+
export interface LoginRequest {
|
|
30
|
+
email: string;
|
|
31
|
+
password: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LoginResponse {
|
|
35
|
+
user: User;
|
|
36
|
+
accessToken: string;
|
|
37
|
+
refreshToken: string;
|
|
38
|
+
expiresIn: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RegisterRequest {
|
|
42
|
+
name: string;
|
|
43
|
+
email: string;
|
|
44
|
+
password: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RegisterResponse extends LoginResponse {}
|
|
48
|
+
|
|
49
|
+
export interface RefreshTokenRequest {
|
|
50
|
+
refreshToken: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RefreshTokenResponse {
|
|
54
|
+
accessToken: string;
|
|
55
|
+
refreshToken: string;
|
|
56
|
+
expiresIn: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface UpdateUserRequest {
|
|
60
|
+
name?: string;
|
|
61
|
+
email?: string;
|
|
62
|
+
avatar?: string;
|
|
63
|
+
}
|