@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.
- 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 -21
- package/README.md +446 -402
- 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 -418
- package/components/ui/UploadProgress.tsx +189 -0
- package/components/ui/VirtualizedList.tsx +288 -285
- package/components/ui/index.ts +28 -30
- 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 -40
- package/hooks/useAnimatedEntry.ts +204 -0
- package/hooks/useApi.ts +5 -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 -375
- 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 -176
- 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
package/docs/adr/README.md
CHANGED
|
@@ -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
|