@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.
Files changed (109) hide show
  1. package/.env.example +18 -0
  2. package/.eslintrc.js +55 -0
  3. package/.github/workflows/ci.yml +184 -0
  4. package/.github/workflows/eas-build.yml +55 -0
  5. package/.github/workflows/eas-update.yml +50 -0
  6. package/.gitignore +62 -0
  7. package/.prettierrc +11 -0
  8. package/.storybook/main.ts +28 -0
  9. package/.storybook/preview.tsx +30 -0
  10. package/CHANGELOG.md +106 -0
  11. package/CONTRIBUTING.md +377 -0
  12. package/README.md +399 -0
  13. package/__tests__/components/Button.test.tsx +74 -0
  14. package/__tests__/hooks/useAuth.test.tsx +499 -0
  15. package/__tests__/services/api.test.ts +535 -0
  16. package/__tests__/utils/cn.test.ts +39 -0
  17. package/app/(auth)/_layout.tsx +36 -0
  18. package/app/(auth)/home.tsx +117 -0
  19. package/app/(auth)/profile.tsx +152 -0
  20. package/app/(auth)/settings.tsx +147 -0
  21. package/app/(public)/_layout.tsx +21 -0
  22. package/app/(public)/forgot-password.tsx +127 -0
  23. package/app/(public)/login.tsx +120 -0
  24. package/app/(public)/onboarding.tsx +5 -0
  25. package/app/(public)/register.tsx +139 -0
  26. package/app/_layout.tsx +97 -0
  27. package/app/index.tsx +21 -0
  28. package/app.config.ts +72 -0
  29. package/assets/images/.gitkeep +7 -0
  30. package/assets/images/adaptive-icon.png +0 -0
  31. package/assets/images/favicon.png +0 -0
  32. package/assets/images/icon.png +0 -0
  33. package/assets/images/notification-icon.png +0 -0
  34. package/assets/images/splash.png +0 -0
  35. package/babel.config.js +10 -0
  36. package/components/ErrorBoundary.tsx +169 -0
  37. package/components/forms/FormInput.tsx +78 -0
  38. package/components/forms/index.ts +1 -0
  39. package/components/onboarding/OnboardingScreen.tsx +370 -0
  40. package/components/onboarding/index.ts +2 -0
  41. package/components/ui/AnimatedButton.tsx +156 -0
  42. package/components/ui/AnimatedCard.tsx +108 -0
  43. package/components/ui/Avatar.tsx +316 -0
  44. package/components/ui/Badge.tsx +416 -0
  45. package/components/ui/BottomSheet.tsx +307 -0
  46. package/components/ui/Button.stories.tsx +115 -0
  47. package/components/ui/Button.tsx +104 -0
  48. package/components/ui/Card.stories.tsx +84 -0
  49. package/components/ui/Card.tsx +32 -0
  50. package/components/ui/Checkbox.tsx +261 -0
  51. package/components/ui/Input.stories.tsx +106 -0
  52. package/components/ui/Input.tsx +117 -0
  53. package/components/ui/Modal.tsx +98 -0
  54. package/components/ui/OptimizedImage.tsx +369 -0
  55. package/components/ui/Select.tsx +240 -0
  56. package/components/ui/Skeleton.tsx +180 -0
  57. package/components/ui/index.ts +18 -0
  58. package/constants/config.ts +54 -0
  59. package/docs/adr/001-state-management.md +79 -0
  60. package/docs/adr/002-styling-approach.md +130 -0
  61. package/docs/adr/003-data-fetching.md +155 -0
  62. package/docs/adr/004-auth-adapter-pattern.md +144 -0
  63. package/docs/adr/README.md +78 -0
  64. package/eas.json +47 -0
  65. package/global.css +10 -0
  66. package/hooks/index.ts +25 -0
  67. package/hooks/useApi.ts +236 -0
  68. package/hooks/useAuth.tsx +290 -0
  69. package/hooks/useBiometrics.ts +295 -0
  70. package/hooks/useDeepLinking.ts +256 -0
  71. package/hooks/useNotifications.ts +138 -0
  72. package/hooks/useOffline.ts +69 -0
  73. package/hooks/usePerformance.ts +434 -0
  74. package/hooks/useTheme.tsx +85 -0
  75. package/hooks/useUpdates.ts +358 -0
  76. package/i18n/index.ts +77 -0
  77. package/i18n/locales/en.json +101 -0
  78. package/i18n/locales/fr.json +101 -0
  79. package/jest.config.js +32 -0
  80. package/maestro/README.md +113 -0
  81. package/maestro/config.yaml +35 -0
  82. package/maestro/flows/login.yaml +62 -0
  83. package/maestro/flows/navigation.yaml +68 -0
  84. package/maestro/flows/offline.yaml +60 -0
  85. package/maestro/flows/register.yaml +94 -0
  86. package/metro.config.js +6 -0
  87. package/nativewind-env.d.ts +1 -0
  88. package/package.json +170 -0
  89. package/scripts/init.ps1 +162 -0
  90. package/scripts/init.sh +174 -0
  91. package/services/analytics.ts +428 -0
  92. package/services/api.ts +340 -0
  93. package/services/authAdapter.ts +333 -0
  94. package/services/index.ts +22 -0
  95. package/services/queryClient.ts +97 -0
  96. package/services/sentry.ts +131 -0
  97. package/services/storage.ts +82 -0
  98. package/stores/appStore.ts +54 -0
  99. package/stores/index.ts +2 -0
  100. package/stores/notificationStore.ts +40 -0
  101. package/tailwind.config.js +47 -0
  102. package/tsconfig.json +26 -0
  103. package/types/index.ts +42 -0
  104. package/types/user.ts +63 -0
  105. package/utils/accessibility.ts +446 -0
  106. package/utils/cn.ts +14 -0
  107. package/utils/index.ts +43 -0
  108. package/utils/toast.ts +113 -0
  109. package/utils/validation.ts +67 -0
@@ -0,0 +1,144 @@
1
+ # ADR-004: Auth Adapter Pattern for Authentication
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Date
8
+
9
+ 2024-01-15
10
+
11
+ ## Context
12
+
13
+ The template needs to support multiple authentication providers (Supabase, Firebase, Auth0, custom backends) without requiring significant code changes. We need:
14
+
15
+ 1. Easy switching between auth providers
16
+ 2. Consistent API regardless of provider
17
+ 3. Type safety
18
+ 4. Testability with mock implementations
19
+
20
+ ## Decision
21
+
22
+ Implement an **Adapter Pattern** for authentication that abstracts the auth provider behind a common interface.
23
+
24
+ ## Rationale
25
+
26
+ 1. **Flexibility**: Change auth providers without touching app code
27
+ 2. **Testing**: Easy to mock for unit tests
28
+ 3. **Consistency**: Same API for all providers
29
+ 4. **Gradual Migration**: Can switch providers incrementally
30
+
31
+ ## Implementation
32
+
33
+ ### Interface Definition
34
+
35
+ ```typescript
36
+ // services/authAdapter.ts
37
+ export interface AuthAdapter {
38
+ signIn(email: string, password: string): Promise<AuthResult>;
39
+ signUp(email: string, password: string, name: string): Promise<AuthResult>;
40
+ signOut(): Promise<void>;
41
+ refreshToken(refreshToken: string): Promise<AuthTokens>;
42
+ forgotPassword(email: string): Promise<void>;
43
+ resetPassword(token: string, newPassword: string): Promise<void>;
44
+ getSession(): Promise<AuthResult | null>;
45
+ onAuthStateChange?(callback: (user: User | null) => void): () => void;
46
+ }
47
+
48
+ export interface AuthResult {
49
+ user: User;
50
+ tokens: AuthTokens;
51
+ }
52
+ ```
53
+
54
+ ### Mock Implementation
55
+
56
+ ```typescript
57
+ export const mockAuthAdapter: AuthAdapter = {
58
+ async signIn(email, password) {
59
+ await delay(1000); // Simulate network
60
+ return {
61
+ user: { id: '1', email, name: email.split('@')[0] },
62
+ tokens: { accessToken: 'mock', refreshToken: 'mock', expiresAt: ... },
63
+ };
64
+ },
65
+ // ... other methods
66
+ };
67
+ ```
68
+
69
+ ### Supabase Implementation
70
+
71
+ ```typescript
72
+ export const supabaseAuthAdapter: AuthAdapter = {
73
+ async signIn(email, password) {
74
+ const { data, error } = await supabase.auth.signInWithPassword({
75
+ email,
76
+ password,
77
+ });
78
+ if (error) throw error;
79
+ return transformSupabaseSession(data);
80
+ },
81
+ // ... other methods
82
+ };
83
+ ```
84
+
85
+ ### Usage
86
+
87
+ ```typescript
88
+ // services/authAdapter.ts
89
+ // Change this line to switch providers
90
+ export const authAdapter: AuthAdapter = mockAuthAdapter;
91
+ // export const authAdapter: AuthAdapter = supabaseAuthAdapter;
92
+ // export const authAdapter: AuthAdapter = firebaseAuthAdapter;
93
+ ```
94
+
95
+ ## Consequences
96
+
97
+ ### Positive
98
+
99
+ - Provider-agnostic code
100
+ - Easy to test with mocks
101
+ - Clear contract for auth operations
102
+ - Can support multiple providers simultaneously
103
+
104
+ ### Negative
105
+
106
+ - Some provider-specific features may not fit the interface
107
+ - Additional abstraction layer
108
+ - Need to maintain multiple implementations
109
+
110
+ ### Mitigation
111
+
112
+ - Allow optional methods in interface
113
+ - Document provider-specific extensions
114
+ - Keep interface focused on common operations
115
+
116
+ ## Testing
117
+
118
+ ```typescript
119
+ // __tests__/auth.test.ts
120
+ import { mockAuthAdapter } from "@/services/authAdapter";
121
+
122
+ describe("Auth", () => {
123
+ it("signs in successfully", async () => {
124
+ const result = await mockAuthAdapter.signIn("test@example.com", "password");
125
+ expect(result.user.email).toBe("test@example.com");
126
+ expect(result.tokens.accessToken).toBeDefined();
127
+ });
128
+ });
129
+ ```
130
+
131
+ ## Migration Guide
132
+
133
+ To switch from mock to Supabase:
134
+
135
+ 1. Install Supabase: `npx expo install @supabase/supabase-js`
136
+ 2. Configure environment variables
137
+ 3. Implement `supabaseAuthAdapter`
138
+ 4. Update export in `authAdapter.ts`
139
+
140
+ ## References
141
+
142
+ - [Adapter Pattern](https://refactoring.guru/design-patterns/adapter)
143
+ - [Supabase Auth](https://supabase.com/docs/guides/auth)
144
+ - [Firebase Auth](https://firebase.google.com/docs/auth)
@@ -0,0 +1,78 @@
1
+ # Architecture Decision Records (ADRs)
2
+
3
+ This directory contains Architecture Decision Records (ADRs) documenting significant technical decisions made in this project.
4
+
5
+ ## What is an ADR?
6
+
7
+ An ADR is a document that captures an important architectural decision made along with its context and consequences.
8
+
9
+ ## ADR Template
10
+
11
+ ```markdown
12
+ # ADR-XXX: Title
13
+
14
+ ## Status
15
+
16
+ [Proposed | Accepted | Deprecated | Superseded by ADR-XXX]
17
+
18
+ ## Date
19
+
20
+ YYYY-MM-DD
21
+
22
+ ## Context
23
+
24
+ [What is the issue that we're seeing that is motivating this decision?]
25
+
26
+ ## Decision
27
+
28
+ [What is the change that we're proposing and/or doing?]
29
+
30
+ ## Consequences
31
+
32
+ ### Positive
33
+
34
+ [What are the benefits?]
35
+
36
+ ### Negative
37
+
38
+ [What are the drawbacks?]
39
+
40
+ ### Mitigation
41
+
42
+ [How do we address the drawbacks?]
43
+
44
+ ## References
45
+
46
+ [Links to relevant documentation, articles, or discussions]
47
+ ```
48
+
49
+ ## Index
50
+
51
+ | ADR | Title | Status | Date |
52
+ | ------------------------------------ | --------------------------------- | -------- | ---------- |
53
+ | [001](./001-state-management.md) | Use Zustand for State Management | Accepted | 2024-01-01 |
54
+ | [002](./002-styling-approach.md) | Use NativeWind for Styling | Accepted | 2024-01-01 |
55
+ | [003](./003-data-fetching.md) | Use React Query for Data Fetching | Accepted | 2024-01-01 |
56
+ | [004](./004-auth-adapter-pattern.md) | Auth Adapter Pattern | Accepted | 2024-01-15 |
57
+
58
+ ## When to Write an ADR
59
+
60
+ Write an ADR when:
61
+
62
+ 1. Making a significant architectural decision
63
+ 2. Choosing between multiple valid options
64
+ 3. The decision will be hard to change later
65
+ 4. Team members need to understand "why"
66
+
67
+ ## How to Create a New ADR
68
+
69
+ 1. Copy the template above
70
+ 2. Number it sequentially (e.g., `005-your-decision.md`)
71
+ 3. Fill in all sections
72
+ 4. Add it to the index in this README
73
+ 5. Submit for review with your PR
74
+
75
+ ## Resources
76
+
77
+ - [ADR GitHub Organization](https://adr.github.io/)
78
+ - [Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
package/eas.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "cli": {
3
+ "version": ">= 10.0.0"
4
+ },
5
+ "build": {
6
+ "development": {
7
+ "developmentClient": true,
8
+ "distribution": "internal",
9
+ "env": {
10
+ "APP_VARIANT": "development"
11
+ },
12
+ "ios": {
13
+ "simulator": true
14
+ },
15
+ "android": {
16
+ "buildType": "apk"
17
+ }
18
+ },
19
+ "preview": {
20
+ "distribution": "internal",
21
+ "env": {
22
+ "APP_VARIANT": "preview"
23
+ },
24
+ "android": {
25
+ "buildType": "apk"
26
+ }
27
+ },
28
+ "production": {
29
+ "autoIncrement": true,
30
+ "env": {
31
+ "APP_VARIANT": "production"
32
+ }
33
+ }
34
+ },
35
+ "submit": {
36
+ "production": {
37
+ "ios": {
38
+ "appleId": "your-apple-id@email.com",
39
+ "ascAppId": "your-app-store-connect-app-id"
40
+ },
41
+ "android": {
42
+ "serviceAccountKeyPath": "./google-services.json",
43
+ "track": "internal"
44
+ }
45
+ }
46
+ }
47
+ }
package/global.css ADDED
@@ -0,0 +1,10 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Custom utility classes */
6
+ @layer utilities {
7
+ .text-balance {
8
+ text-wrap: balance;
9
+ }
10
+ }
package/hooks/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export { useAuth, AuthProvider, getAuthToken } from "./useAuth";
2
+ export { useTheme, ThemeProvider } from "./useTheme";
3
+ export { useNotifications } from "./useNotifications";
4
+ export {
5
+ useCurrentUser,
6
+ useUser,
7
+ useUpdateUser,
8
+ queryKeys,
9
+ createCrudHooks,
10
+ postsApi,
11
+ } from "./useApi";
12
+ export {
13
+ useDeepLinking,
14
+ createDeepLink,
15
+ getDeepLinkPrefix,
16
+ } from "./useDeepLinking";
17
+ export { useBiometrics, getBiometricName } from "./useBiometrics";
18
+ export { useOffline, usePendingMutations } from "./useOffline";
19
+ export { useUpdates, getUpdateInfo, forceUpdate } from "./useUpdates";
20
+ export {
21
+ usePerformance,
22
+ measureAsync,
23
+ measureSync,
24
+ runAfterInteractions,
25
+ } from "./usePerformance";
@@ -0,0 +1,236 @@
1
+ import {
2
+ useQuery,
3
+ useMutation,
4
+ useQueryClient,
5
+ UseQueryOptions,
6
+ UseMutationOptions,
7
+ } from "@tanstack/react-query";
8
+ import { api } from "@/services/api";
9
+ import { toast, handleApiError } from "@/utils/toast";
10
+
11
+ // Query keys factory for type-safe and organized cache management
12
+ export const queryKeys = {
13
+ all: ["api"] as const,
14
+ users: {
15
+ all: ["users"] as const,
16
+ detail: (id: string) => ["users", id] as const,
17
+ me: () => ["users", "me"] as const,
18
+ },
19
+ posts: {
20
+ all: ["posts"] as const,
21
+ list: (filters: Record<string, unknown>) => ["posts", "list", filters] as const,
22
+ detail: (id: string) => ["posts", id] as const,
23
+ },
24
+ // Add more query keys as needed
25
+ } as const;
26
+
27
+ // Generic types for API responses
28
+ interface PaginatedResponse<T> {
29
+ data: T[];
30
+ page: number;
31
+ pageSize: number;
32
+ total: number;
33
+ totalPages: number;
34
+ }
35
+
36
+ // ===========================================
37
+ // User Hooks
38
+ // ===========================================
39
+
40
+ interface User {
41
+ id: string;
42
+ name: string;
43
+ email: string;
44
+ avatar?: string;
45
+ }
46
+
47
+ /**
48
+ * Fetch current user profile
49
+ */
50
+ export function useCurrentUser(
51
+ options?: Omit<UseQueryOptions<User, Error>, "queryKey" | "queryFn">
52
+ ) {
53
+ return useQuery({
54
+ queryKey: queryKeys.users.me(),
55
+ queryFn: () => api.get<User>("/users/me"),
56
+ staleTime: 1000 * 60 * 5, // 5 minutes
57
+ ...options,
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Fetch user by ID
63
+ */
64
+ export function useUser(
65
+ userId: string,
66
+ options?: Omit<UseQueryOptions<User, Error>, "queryKey" | "queryFn">
67
+ ) {
68
+ return useQuery({
69
+ queryKey: queryKeys.users.detail(userId),
70
+ queryFn: () => api.get<User>(`/users/${userId}`),
71
+ enabled: !!userId,
72
+ ...options,
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Update user profile
78
+ */
79
+ export function useUpdateUser() {
80
+ const queryClient = useQueryClient();
81
+
82
+ return useMutation({
83
+ mutationFn: (data: Partial<User>) => api.patch<User>("/users/me", data),
84
+ onSuccess: (updatedUser) => {
85
+ // Update cache
86
+ queryClient.setQueryData(queryKeys.users.me(), updatedUser);
87
+ toast.success("Profile updated");
88
+ },
89
+ onError: (error) => {
90
+ handleApiError(error, "Failed to update profile");
91
+ },
92
+ });
93
+ }
94
+
95
+ // ===========================================
96
+ // Generic CRUD Hooks Factory
97
+ // ===========================================
98
+
99
+ interface CrudHooksConfig<T, CreateDTO, UpdateDTO> {
100
+ baseKey: readonly string[];
101
+ endpoint: string;
102
+ entityName: string;
103
+ }
104
+
105
+ /**
106
+ * Factory to create CRUD hooks for any resource
107
+ */
108
+ export function createCrudHooks<
109
+ T extends { id: string },
110
+ CreateDTO = Omit<T, "id">,
111
+ UpdateDTO = Partial<T>
112
+ >(config: CrudHooksConfig<T, CreateDTO, UpdateDTO>) {
113
+ const { baseKey, endpoint, entityName } = config;
114
+
115
+ // Helper to build URL with query params
116
+ const buildUrl = (base: string, params?: Record<string, unknown>) => {
117
+ if (!params || Object.keys(params).length === 0) return base;
118
+ const searchParams = new URLSearchParams();
119
+ Object.entries(params).forEach(([key, value]) => {
120
+ if (value !== undefined && value !== null) {
121
+ searchParams.append(key, String(value));
122
+ }
123
+ });
124
+ return `${base}?${searchParams.toString()}`;
125
+ };
126
+
127
+ return {
128
+ // List all
129
+ useList: (
130
+ filters?: Record<string, unknown>,
131
+ options?: Omit<UseQueryOptions<T[], Error>, "queryKey" | "queryFn">
132
+ ) =>
133
+ useQuery({
134
+ queryKey: [...baseKey, "list", filters],
135
+ queryFn: () => api.get<T[]>(buildUrl(endpoint, filters)),
136
+ ...options,
137
+ }),
138
+
139
+ // Get by ID
140
+ useById: (
141
+ id: string,
142
+ options?: Omit<UseQueryOptions<T, Error>, "queryKey" | "queryFn">
143
+ ) =>
144
+ useQuery({
145
+ queryKey: [...baseKey, id],
146
+ queryFn: () => api.get<T>(`${endpoint}/${id}`),
147
+ enabled: !!id,
148
+ ...options,
149
+ }),
150
+
151
+ // Create
152
+ useCreate: (
153
+ options?: Omit<UseMutationOptions<T, Error, CreateDTO>, "mutationFn">
154
+ ) => {
155
+ const queryClient = useQueryClient();
156
+ return useMutation({
157
+ mutationFn: (data: CreateDTO) =>
158
+ api.post<T>(endpoint, data as Record<string, unknown>),
159
+ onSuccess: () => {
160
+ queryClient.invalidateQueries({ queryKey: baseKey });
161
+ toast.success(`${entityName} created`);
162
+ },
163
+ onError: (error) => {
164
+ handleApiError(error, `Failed to create ${entityName.toLowerCase()}`);
165
+ },
166
+ ...options,
167
+ });
168
+ },
169
+
170
+ // Update
171
+ useUpdate: (
172
+ options?: Omit<
173
+ UseMutationOptions<T, Error, { id: string; data: UpdateDTO }>,
174
+ "mutationFn"
175
+ >
176
+ ) => {
177
+ const queryClient = useQueryClient();
178
+ return useMutation({
179
+ mutationFn: ({ id, data }: { id: string; data: UpdateDTO }) =>
180
+ api.patch<T>(`${endpoint}/${id}`, data as Record<string, unknown>),
181
+ onSuccess: (_, { id }) => {
182
+ queryClient.invalidateQueries({ queryKey: [...baseKey, id] });
183
+ queryClient.invalidateQueries({ queryKey: [...baseKey, "list"] });
184
+ toast.success(`${entityName} updated`);
185
+ },
186
+ onError: (error) => {
187
+ handleApiError(error, `Failed to update ${entityName.toLowerCase()}`);
188
+ },
189
+ ...options,
190
+ });
191
+ },
192
+
193
+ // Delete
194
+ useDelete: (
195
+ options?: Omit<UseMutationOptions<void, Error, string>, "mutationFn">
196
+ ) => {
197
+ const queryClient = useQueryClient();
198
+ return useMutation({
199
+ mutationFn: (id: string) => api.delete<void>(`${endpoint}/${id}`),
200
+ onSuccess: () => {
201
+ queryClient.invalidateQueries({ queryKey: baseKey });
202
+ toast.success(`${entityName} deleted`);
203
+ },
204
+ onError: (error) => {
205
+ handleApiError(error, `Failed to delete ${entityName.toLowerCase()}`);
206
+ },
207
+ ...options,
208
+ });
209
+ },
210
+ };
211
+ }
212
+
213
+ // ===========================================
214
+ // Example: Posts CRUD hooks
215
+ // ===========================================
216
+
217
+ interface Post {
218
+ id: string;
219
+ title: string;
220
+ content: string;
221
+ authorId: string;
222
+ createdAt: string;
223
+ }
224
+
225
+ export const postsApi = createCrudHooks<Post>({
226
+ baseKey: queryKeys.posts.all,
227
+ endpoint: "/posts",
228
+ entityName: "Post",
229
+ });
230
+
231
+ // Usage:
232
+ // const { data: posts } = postsApi.useList();
233
+ // const { data: post } = postsApi.useById("123");
234
+ // const createPost = postsApi.useCreate();
235
+ // const updatePost = postsApi.useUpdate();
236
+ // const deletePost = postsApi.useDelete();