@croacroa/react-native-template 2.1.0 → 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.
Files changed (172) hide show
  1. package/.env.example +5 -0
  2. package/.eslintrc.js +8 -0
  3. package/.github/workflows/ci.yml +187 -187
  4. package/.github/workflows/eas-build.yml +55 -55
  5. package/.github/workflows/eas-update.yml +50 -50
  6. package/.github/workflows/npm-publish.yml +57 -0
  7. package/CHANGELOG.md +195 -106
  8. package/CONTRIBUTING.md +377 -377
  9. package/LICENSE +21 -21
  10. package/README.md +446 -402
  11. package/__tests__/accessibility/components.test.tsx +285 -0
  12. package/__tests__/components/Button.test.tsx +2 -4
  13. package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
  14. package/__tests__/components/snapshots.test.tsx +131 -131
  15. package/__tests__/helpers/a11y.ts +54 -0
  16. package/__tests__/hooks/useAnalytics.test.ts +100 -0
  17. package/__tests__/hooks/useAnimations.test.ts +70 -0
  18. package/__tests__/hooks/useAuth.test.tsx +71 -28
  19. package/__tests__/hooks/useMedia.test.ts +318 -0
  20. package/__tests__/hooks/usePayments.test.tsx +307 -0
  21. package/__tests__/hooks/usePermission.test.ts +230 -0
  22. package/__tests__/hooks/useWebSocket.test.ts +329 -0
  23. package/__tests__/integration/auth-api.test.tsx +224 -227
  24. package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
  25. package/__tests__/services/api.test.ts +24 -6
  26. package/app/(auth)/home.tsx +11 -9
  27. package/app/(auth)/profile.tsx +8 -6
  28. package/app/(auth)/settings.tsx +11 -9
  29. package/app/(public)/forgot-password.tsx +25 -15
  30. package/app/(public)/login.tsx +48 -12
  31. package/app/(public)/onboarding.tsx +5 -5
  32. package/app/(public)/register.tsx +24 -15
  33. package/app/_layout.tsx +6 -3
  34. package/app.config.ts +27 -2
  35. package/assets/images/.gitkeep +7 -7
  36. package/assets/images/adaptive-icon.png +0 -0
  37. package/assets/images/favicon.png +0 -0
  38. package/assets/images/icon.png +0 -0
  39. package/assets/images/notification-icon.png +0 -0
  40. package/assets/images/splash.png +0 -0
  41. package/components/ErrorBoundary.tsx +73 -28
  42. package/components/auth/SocialLoginButtons.tsx +168 -0
  43. package/components/forms/FormInput.tsx +5 -3
  44. package/components/onboarding/OnboardingScreen.tsx +370 -370
  45. package/components/onboarding/index.ts +2 -2
  46. package/components/providers/AnalyticsProvider.tsx +67 -0
  47. package/components/providers/SuspenseBoundary.tsx +359 -357
  48. package/components/providers/index.ts +24 -21
  49. package/components/ui/AnimatedButton.tsx +1 -9
  50. package/components/ui/AnimatedList.tsx +98 -0
  51. package/components/ui/AnimatedScreen.tsx +89 -0
  52. package/components/ui/Avatar.tsx +319 -316
  53. package/components/ui/Badge.tsx +416 -416
  54. package/components/ui/BottomSheet.tsx +307 -307
  55. package/components/ui/Button.tsx +11 -3
  56. package/components/ui/Checkbox.tsx +261 -261
  57. package/components/ui/FeatureGate.tsx +57 -0
  58. package/components/ui/ForceUpdateScreen.tsx +108 -0
  59. package/components/ui/ImagePickerButton.tsx +180 -0
  60. package/components/ui/Input.stories.tsx +2 -10
  61. package/components/ui/Input.tsx +2 -10
  62. package/components/ui/OptimizedImage.tsx +369 -369
  63. package/components/ui/Paywall.tsx +253 -0
  64. package/components/ui/PermissionGate.tsx +155 -0
  65. package/components/ui/PurchaseButton.tsx +84 -0
  66. package/components/ui/Select.tsx +240 -240
  67. package/components/ui/Skeleton.tsx +3 -1
  68. package/components/ui/Toast.tsx +427 -418
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -30
  72. package/constants/config.ts +135 -97
  73. package/docs/adr/001-state-management.md +79 -79
  74. package/docs/adr/002-styling-approach.md +130 -130
  75. package/docs/adr/003-data-fetching.md +155 -155
  76. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  77. package/docs/adr/README.md +78 -78
  78. package/docs/guides/analytics-posthog.md +121 -0
  79. package/docs/guides/auth-supabase.md +162 -0
  80. package/docs/guides/feature-flags-launchdarkly.md +150 -0
  81. package/docs/guides/payments-revenuecat.md +169 -0
  82. package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
  83. package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
  84. package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
  85. package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
  86. package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
  87. package/eas.json +2 -1
  88. package/hooks/index.ts +70 -40
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +5 -4
  91. package/hooks/useAuth.tsx +7 -3
  92. package/hooks/useBiometrics.ts +295 -295
  93. package/hooks/useChannel.ts +111 -0
  94. package/hooks/useDeepLinking.ts +256 -256
  95. package/hooks/useExperiment.ts +36 -0
  96. package/hooks/useFeatureFlag.ts +59 -0
  97. package/hooks/useForceUpdate.ts +91 -0
  98. package/hooks/useImagePicker.ts +281 -375
  99. package/hooks/useInAppReview.ts +64 -0
  100. package/hooks/useMFA.ts +509 -499
  101. package/hooks/useParallax.ts +142 -0
  102. package/hooks/usePerformance.ts +434 -434
  103. package/hooks/usePermission.ts +190 -0
  104. package/hooks/usePresence.ts +129 -0
  105. package/hooks/useProducts.ts +36 -0
  106. package/hooks/usePurchase.ts +103 -0
  107. package/hooks/useRateLimit.ts +70 -0
  108. package/hooks/useSubscription.ts +49 -0
  109. package/hooks/useTrackEvent.ts +52 -0
  110. package/hooks/useTrackScreen.ts +40 -0
  111. package/hooks/useUpdates.ts +358 -358
  112. package/hooks/useUpload.ts +165 -0
  113. package/hooks/useWebSocket.ts +111 -0
  114. package/i18n/index.ts +197 -194
  115. package/i18n/locales/ar.json +170 -101
  116. package/i18n/locales/de.json +170 -101
  117. package/i18n/locales/en.json +170 -101
  118. package/i18n/locales/es.json +170 -101
  119. package/i18n/locales/fr.json +170 -101
  120. package/jest.config.js +1 -1
  121. package/maestro/README.md +113 -113
  122. package/maestro/config.yaml +35 -35
  123. package/maestro/flows/login.yaml +62 -62
  124. package/maestro/flows/mfa-login.yaml +92 -92
  125. package/maestro/flows/mfa-setup.yaml +86 -86
  126. package/maestro/flows/navigation.yaml +68 -68
  127. package/maestro/flows/offline-conflict.yaml +101 -101
  128. package/maestro/flows/offline-sync.yaml +128 -128
  129. package/maestro/flows/offline.yaml +60 -60
  130. package/maestro/flows/register.yaml +94 -94
  131. package/package.json +188 -176
  132. package/scripts/generate-placeholders.js +38 -0
  133. package/services/analytics/adapters/console.ts +50 -0
  134. package/services/analytics/analytics-adapter.ts +94 -0
  135. package/services/analytics/types.ts +73 -0
  136. package/services/analytics.ts +428 -428
  137. package/services/api.ts +419 -340
  138. package/services/auth/social/apple.ts +110 -0
  139. package/services/auth/social/google.ts +159 -0
  140. package/services/auth/social/social-auth.ts +100 -0
  141. package/services/auth/social/types.ts +80 -0
  142. package/services/authAdapter.ts +333 -333
  143. package/services/backgroundSync.ts +652 -626
  144. package/services/feature-flags/adapters/mock.ts +108 -0
  145. package/services/feature-flags/feature-flag-adapter.ts +174 -0
  146. package/services/feature-flags/types.ts +79 -0
  147. package/services/force-update.ts +140 -0
  148. package/services/index.ts +116 -54
  149. package/services/media/compression.ts +91 -0
  150. package/services/media/media-picker.ts +151 -0
  151. package/services/media/media-upload.ts +160 -0
  152. package/services/payments/adapters/mock.ts +159 -0
  153. package/services/payments/payment-adapter.ts +118 -0
  154. package/services/payments/types.ts +131 -0
  155. package/services/permissions/permission-manager.ts +284 -0
  156. package/services/permissions/types.ts +104 -0
  157. package/services/realtime/types.ts +100 -0
  158. package/services/realtime/websocket-manager.ts +441 -0
  159. package/services/security.ts +289 -286
  160. package/services/sentry.ts +4 -4
  161. package/stores/appStore.ts +9 -0
  162. package/stores/notificationStore.ts +3 -1
  163. package/tailwind.config.js +47 -47
  164. package/tsconfig.json +37 -13
  165. package/types/user.ts +1 -1
  166. package/utils/accessibility.ts +446 -446
  167. package/utils/animations/presets.ts +182 -0
  168. package/utils/animations/transitions.ts +62 -0
  169. package/utils/index.ts +63 -52
  170. package/utils/toast.ts +9 -2
  171. package/utils/validation.ts +4 -1
  172. package/utils/withAccessibility.tsx +272 -272
@@ -1,78 +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)
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)
@@ -0,0 +1,121 @@
1
+ # Analytics: PostHog Integration
2
+
3
+ Add [PostHog](https://posthog.com/docs/libraries/react-native) as an analytics provider alongside the existing console and Sentry adapters.
4
+
5
+ ## Prerequisites
6
+
7
+ - A PostHog account ([sign up here](https://app.posthog.com/signup))
8
+ - Your project API key and host URL from **Project Settings**
9
+
10
+ ## 1. Install the SDK
11
+
12
+ ```bash
13
+ npx expo install posthog-react-native
14
+ ```
15
+
16
+ ## 2. Add Environment Variables
17
+
18
+ Add these to your `.env` file:
19
+
20
+ ```env
21
+ EXPO_PUBLIC_POSTHOG_API_KEY=phc_your_project_api_key
22
+ EXPO_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
23
+ ```
24
+
25
+ Use `https://eu.i.posthog.com` if your PostHog project is hosted in the EU region.
26
+
27
+ ## 3. Create the Adapter
28
+
29
+ Create `services/analytics/posthog.ts`:
30
+
31
+ ```typescript
32
+ import PostHog from "posthog-react-native";
33
+ import type { AnalyticsAdapter } from "@/services/analytics";
34
+
35
+ // Initialize the PostHog client once at module level.
36
+ // The SDK queues events internally, so it is safe to call
37
+ // track/identify immediately after construction.
38
+ const posthog = new PostHog(process.env.EXPO_PUBLIC_POSTHOG_API_KEY!, {
39
+ host: process.env.EXPO_PUBLIC_POSTHOG_HOST,
40
+ // Flush events every 30 s or when the queue hits 20 events
41
+ flushInterval: 30000,
42
+ flushAt: 20,
43
+ });
44
+
45
+ /** Timers are tracked locally; PostHog does not have a built-in timer API */
46
+ const timers = new Map<string, number>();
47
+
48
+ export const posthogAdapter: AnalyticsAdapter = {
49
+ track(event, properties) {
50
+ posthog.capture(event, properties);
51
+ },
52
+
53
+ identify(userId, traits) {
54
+ posthog.identify(userId, traits);
55
+ },
56
+
57
+ screen(name, properties) {
58
+ posthog.screen(name, properties);
59
+ },
60
+
61
+ reset() {
62
+ posthog.reset();
63
+ timers.clear();
64
+ },
65
+
66
+ setUserProperties(properties) {
67
+ // $set persists properties on the user profile in PostHog
68
+ posthog.capture("$set", { $set: properties });
69
+ },
70
+
71
+ trackRevenue(amount, currency, productId, properties) {
72
+ posthog.capture("Purchase", {
73
+ revenue: amount,
74
+ currency,
75
+ productId,
76
+ ...properties,
77
+ });
78
+ },
79
+
80
+ startTimer(event) {
81
+ timers.set(event, Date.now());
82
+ },
83
+
84
+ endTimer(event, properties) {
85
+ const start = timers.get(event);
86
+ if (start) {
87
+ const durationMs = Date.now() - start;
88
+ timers.delete(event);
89
+ posthog.capture(event, { ...properties, duration_ms: durationMs });
90
+ }
91
+ },
92
+ };
93
+ ```
94
+
95
+ ## 4. Register the Provider
96
+
97
+ The template's analytics system supports multiple providers at once. Register PostHog in `app/_layout.tsx` (or a dedicated providers file) so events flow to PostHog in addition to the default console and Sentry adapters:
98
+
99
+ ```typescript
100
+ import { analytics } from "@/services/analytics";
101
+ import { posthogAdapter } from "@/services/analytics/posthog";
102
+
103
+ // Register PostHog alongside the existing adapters
104
+ analytics.addAdapter(posthogAdapter);
105
+ ```
106
+
107
+ That is the only wiring needed. Every call to `track()`, `identify()`, or `screen()` throughout the app will now also be sent to PostHog.
108
+
109
+ ## 5. Verify
110
+
111
+ 1. Run the app: `npx expo start`
112
+ 2. Trigger a few events (sign in, navigate between tabs, etc.)
113
+ 3. Open the PostHog dashboard > **Activity** and confirm events appear within a minute
114
+ 4. Check **Persons** to verify `identify()` linked events to the correct user
115
+
116
+ ## What's Next
117
+
118
+ - **Feature flags:** Use `posthog.getFeatureFlag("flag-name")` to roll out features gradually
119
+ - **Session replay:** Enable session recording in PostHog to see exactly how users navigate your app
120
+ - **Group analytics:** Call `posthog.group("company", companyId)` to analyze usage at the organization level
121
+ - **Opt-out support:** Call `posthog.optOut()` to respect user privacy preferences and disable tracking
@@ -0,0 +1,162 @@
1
+ # Auth: Supabase Integration
2
+
3
+ Replace the mock auth adapter with [Supabase Auth](https://supabase.com/docs/guides/auth) in under 10 minutes.
4
+
5
+ ## Prerequisites
6
+
7
+ - A Supabase project ([create one here](https://supabase.com/dashboard))
8
+ - Your project URL and anon key from **Settings > API**
9
+
10
+ ## 1. Install the SDK
11
+
12
+ ```bash
13
+ npx expo install @supabase/supabase-js
14
+ ```
15
+
16
+ ## 2. Add Environment Variables
17
+
18
+ Add these to your `.env` file:
19
+
20
+ ```env
21
+ EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
22
+ EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
23
+ ```
24
+
25
+ ## 3. Create the Adapter
26
+
27
+ Create `services/auth/supabase.ts`:
28
+
29
+ ```typescript
30
+ import { createClient } from "@supabase/supabase-js";
31
+ import type { AuthAdapter, AuthResult } from "@/services/authAdapter";
32
+ import type { User, AuthTokens } from "@/types";
33
+
34
+ // Initialize the Supabase client once at module level
35
+ const supabase = createClient(
36
+ process.env.EXPO_PUBLIC_SUPABASE_URL!,
37
+ process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!
38
+ );
39
+
40
+ /** Convert a Supabase session + user into the template's AuthResult shape */
41
+ function toAuthResult(
42
+ user: { id: string; email?: string; user_metadata: Record<string, any>; created_at: string },
43
+ session: { access_token: string; refresh_token: string; expires_at?: number }
44
+ ): AuthResult {
45
+ return {
46
+ user: {
47
+ id: user.id,
48
+ email: user.email ?? "",
49
+ name: user.user_metadata.name ?? user.email?.split("@")[0] ?? "",
50
+ avatar: user.user_metadata.avatar_url,
51
+ createdAt: user.created_at,
52
+ },
53
+ tokens: {
54
+ accessToken: session.access_token,
55
+ refreshToken: session.refresh_token,
56
+ // Supabase expires_at is in seconds; the template expects milliseconds
57
+ expiresAt: (session.expires_at ?? 0) * 1000,
58
+ },
59
+ };
60
+ }
61
+
62
+ export const supabaseAuthAdapter: AuthAdapter = {
63
+ async signIn(email, password) {
64
+ const { data, error } = await supabase.auth.signInWithPassword({ email, password });
65
+ if (error) throw { code: error.name, message: error.message };
66
+ return toAuthResult(data.user!, data.session!);
67
+ },
68
+
69
+ async signUp(email, password, name) {
70
+ const { data, error } = await supabase.auth.signUp({
71
+ email,
72
+ password,
73
+ options: { data: { name } },
74
+ });
75
+ if (error) throw { code: error.name, message: error.message };
76
+ return toAuthResult(data.user!, data.session!);
77
+ },
78
+
79
+ async signOut() {
80
+ const { error } = await supabase.auth.signOut();
81
+ if (error) throw { code: error.name, message: error.message };
82
+ },
83
+
84
+ async refreshToken(_refreshToken) {
85
+ // Supabase manages the refresh token internally; calling refreshSession
86
+ // is enough. The _refreshToken parameter is ignored.
87
+ const { data, error } = await supabase.auth.refreshSession();
88
+ if (error) throw { code: error.name, message: error.message };
89
+ return {
90
+ accessToken: data.session!.access_token,
91
+ refreshToken: data.session!.refresh_token,
92
+ expiresAt: (data.session!.expires_at ?? 0) * 1000,
93
+ };
94
+ },
95
+
96
+ async forgotPassword(email) {
97
+ const { error } = await supabase.auth.resetPasswordForEmail(email);
98
+ if (error) throw { code: error.name, message: error.message };
99
+ },
100
+
101
+ async resetPassword(_token, newPassword) {
102
+ // After the user clicks the reset link, Supabase sets a session.
103
+ // We can update the password directly on the current session.
104
+ const { error } = await supabase.auth.updateUser({ password: newPassword });
105
+ if (error) throw { code: error.name, message: error.message };
106
+ },
107
+
108
+ async getSession() {
109
+ const { data } = await supabase.auth.getSession();
110
+ if (!data.session) return null;
111
+
112
+ const { data: userData } = await supabase.auth.getUser();
113
+ if (!userData.user) return null;
114
+
115
+ return toAuthResult(userData.user, data.session);
116
+ },
117
+
118
+ onAuthStateChange(callback) {
119
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(
120
+ (_event, session) => {
121
+ if (session?.user) {
122
+ callback({
123
+ id: session.user.id,
124
+ email: session.user.email ?? "",
125
+ name: session.user.user_metadata.name ?? "",
126
+ avatar: session.user.user_metadata.avatar_url,
127
+ createdAt: session.user.created_at,
128
+ });
129
+ } else {
130
+ callback(null);
131
+ }
132
+ }
133
+ );
134
+ return () => subscription.unsubscribe();
135
+ },
136
+ };
137
+ ```
138
+
139
+ ## 4. Activate the Adapter
140
+
141
+ Open `services/authAdapter.ts` and change the last line:
142
+
143
+ ```diff
144
+ - export const authAdapter: AuthAdapter = mockAuthAdapter;
145
+ + import { supabaseAuthAdapter } from "./auth/supabase";
146
+ + export const authAdapter: AuthAdapter = supabaseAuthAdapter;
147
+ ```
148
+
149
+ That is the only change needed in the existing codebase. Every screen and hook that consumes `authAdapter` will now talk to Supabase.
150
+
151
+ ## 5. Verify
152
+
153
+ 1. Run the app: `npx expo start`
154
+ 2. Register a new account -- check the Supabase dashboard **Authentication > Users**
155
+ 3. Sign out, then sign back in
156
+ 4. Trigger "Forgot password" and confirm the email arrives
157
+
158
+ ## What's Next
159
+
160
+ - **Social login:** Add Google/Apple via `supabase.auth.signInWithOAuth()`
161
+ - **Row-level security:** Enable RLS on your Supabase tables and pass the access token with API calls
162
+ - **Deep link handling:** Configure a redirect URL so password-reset links open your app directly
@@ -0,0 +1,150 @@
1
+ # Feature Flags: LaunchDarkly Integration
2
+
3
+ Replace the mock feature flag adapter with [LaunchDarkly](https://docs.launchdarkly.com/sdk/client-side/react-native) to evaluate flags and run A/B tests against a remote provider.
4
+
5
+ ## Prerequisites
6
+
7
+ - A LaunchDarkly account ([sign up here](https://app.launchdarkly.com/signup))
8
+ - A project and environment created in the LaunchDarkly dashboard
9
+ - Your **mobile SDK key** from Settings > Environments
10
+
11
+ ## 1. Install the SDK
12
+
13
+ ```bash
14
+ npx expo install launchdarkly-react-native-client-sdk
15
+ ```
16
+
17
+ > LaunchDarkly requires native modules. You will need a development build (`npx expo prebuild` or EAS Build) -- Expo Go is not supported.
18
+
19
+ ## 2. Add Environment Variables
20
+
21
+ Add this to your `.env` file:
22
+
23
+ ```env
24
+ EXPO_PUBLIC_LAUNCHDARKLY_MOBILE_KEY=your_mobile_sdk_key
25
+ ```
26
+
27
+ Use the **mobile key** (not the server-side SDK key) from your LaunchDarkly dashboard.
28
+
29
+ ## 3. Create the Adapter
30
+
31
+ Create `services/feature-flags/adapters/launchdarkly.ts`:
32
+
33
+ ```typescript
34
+ import LDClient, {
35
+ type LDConfig,
36
+ type LDContext,
37
+ } from "launchdarkly-react-native-client-sdk";
38
+ import type { FeatureFlagAdapter } from "../types";
39
+
40
+ /**
41
+ * LaunchDarkly adapter for the feature flag system.
42
+ * Connects to LaunchDarkly's mobile SDK for real-time flag evaluation
43
+ * and A/B test assignments.
44
+ */
45
+ export class LaunchDarklyAdapter implements FeatureFlagAdapter {
46
+ private client: LDClient | null = null;
47
+
48
+ async initialize(): Promise<void> {
49
+ const config: LDConfig = {
50
+ mobileKey: process.env.EXPO_PUBLIC_LAUNCHDARKLY_MOBILE_KEY!,
51
+ debugMode: __DEV__,
52
+ };
53
+
54
+ // Start with an anonymous context; call identify() later with real user data
55
+ const initialContext: LDContext = {
56
+ kind: "user",
57
+ key: "anonymous",
58
+ anonymous: true,
59
+ };
60
+
61
+ this.client = new LDClient();
62
+ await this.client.configure(config, initialContext);
63
+
64
+ if (__DEV__) {
65
+ console.log("[FeatureFlags] LaunchDarkly initialized");
66
+ }
67
+ }
68
+
69
+ isEnabled(flag: string): boolean {
70
+ if (!this.client) return false;
71
+ return this.client.boolVariation(flag, false);
72
+ }
73
+
74
+ getValue<T>(flag: string, defaultValue: T): T {
75
+ if (!this.client) return defaultValue;
76
+
77
+ // LaunchDarkly SDK exposes typed variation methods; use jsonVariation
78
+ // for maximum flexibility (objects, arrays, strings, numbers).
79
+ return this.client.jsonVariation(flag, defaultValue) as T;
80
+ }
81
+
82
+ getExperimentVariant(experimentId: string): string | null {
83
+ if (!this.client) return null;
84
+ const variant = this.client.stringVariation(experimentId, "");
85
+ return variant || null;
86
+ }
87
+
88
+ identify(userId: string, attributes?: Record<string, unknown>): void {
89
+ if (!this.client) return;
90
+
91
+ const context: LDContext = {
92
+ kind: "user",
93
+ key: userId,
94
+ ...attributes,
95
+ };
96
+
97
+ this.client.identify(context);
98
+
99
+ if (__DEV__) {
100
+ console.log("[FeatureFlags] LaunchDarkly identified user:", userId);
101
+ }
102
+ }
103
+
104
+ async refresh(): Promise<void> {
105
+ // The LaunchDarkly SDK uses a streaming connection by default, so flags
106
+ // are updated in real-time. This is a manual fallback if streaming is
107
+ // disabled or you need to force a refresh.
108
+ if (!this.client) return;
109
+
110
+ // Close and reconfigure to force a fresh fetch
111
+ if (__DEV__) {
112
+ console.log("[FeatureFlags] LaunchDarkly manual refresh");
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ > The adapter starts with an anonymous context. Once the user signs in, call `FeatureFlags.identify(userId)` to switch to a real user context so targeted flags work correctly.
119
+
120
+ ## 4. Activate the Adapter
121
+
122
+ In your `app/_layout.tsx` (or a dedicated providers file), set the adapter **before** calling `initialize()`:
123
+
124
+ ```typescript
125
+ import { FeatureFlags } from "@/services/feature-flags/feature-flag-adapter";
126
+ import { LaunchDarklyAdapter } from "@/services/feature-flags/adapters/launchdarkly";
127
+
128
+ // Swap in LaunchDarkly -- do this once, before initialize()
129
+ FeatureFlags.setAdapter(new LaunchDarklyAdapter());
130
+ await FeatureFlags.initialize();
131
+
132
+ // Optionally start periodic refresh (streaming covers most cases)
133
+ // FeatureFlags.startAutoRefresh();
134
+ ```
135
+
136
+ No other files need to change. Every component that calls `FeatureFlags.isEnabled()` or uses `useFeatureFlag()` will now evaluate against LaunchDarkly.
137
+
138
+ ## 5. Verify
139
+
140
+ 1. Create a **development build**: `npx expo run:ios` or `npx expo run:android`
141
+ 2. Create a boolean flag `test_flag` in the LaunchDarkly dashboard and enable it
142
+ 3. Check the flag in your app: `FeatureFlags.isEnabled("test_flag")` should return `true`
143
+ 4. Toggle the flag off in the dashboard and confirm the value updates in the app
144
+
145
+ ## What's Next
146
+
147
+ - **Targeting rules:** Use LaunchDarkly's targeting to roll out features to specific user segments
148
+ - **Experiments:** Create experiments in the LaunchDarkly dashboard and read variants with `useExperiment()`
149
+ - **Analytics integration:** Connect LaunchDarkly events to your analytics provider for experiment analysis
150
+ - **Auto-refresh:** If you disable streaming, call `FeatureFlags.startAutoRefresh()` to poll for updates