@croacroa/react-native-template 2.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -0
- package/.eslintrc.js +8 -0
- package/.github/workflows/ci.yml +187 -187
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/.github/workflows/npm-publish.yml +57 -0
- package/CHANGELOG.md +195 -106
- package/CONTRIBUTING.md +377 -377
- package/LICENSE +21 -0
- package/README.md +446 -399
- package/__tests__/accessibility/components.test.tsx +285 -0
- package/__tests__/components/Button.test.tsx +2 -4
- package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
- package/__tests__/components/snapshots.test.tsx +131 -131
- package/__tests__/helpers/a11y.ts +54 -0
- package/__tests__/hooks/useAnalytics.test.ts +100 -0
- package/__tests__/hooks/useAnimations.test.ts +70 -0
- package/__tests__/hooks/useAuth.test.tsx +71 -28
- package/__tests__/hooks/useMedia.test.ts +318 -0
- package/__tests__/hooks/usePayments.test.tsx +307 -0
- package/__tests__/hooks/usePermission.test.ts +230 -0
- package/__tests__/hooks/useWebSocket.test.ts +329 -0
- package/__tests__/integration/auth-api.test.tsx +224 -227
- package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
- package/__tests__/services/api.test.ts +24 -6
- package/app/(auth)/home.tsx +11 -9
- package/app/(auth)/profile.tsx +8 -6
- package/app/(auth)/settings.tsx +11 -9
- package/app/(public)/forgot-password.tsx +25 -15
- package/app/(public)/login.tsx +48 -12
- package/app/(public)/onboarding.tsx +5 -5
- package/app/(public)/register.tsx +24 -15
- package/app/_layout.tsx +6 -3
- package/app.config.ts +27 -2
- package/assets/images/.gitkeep +7 -7
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/components/ErrorBoundary.tsx +73 -28
- package/components/auth/SocialLoginButtons.tsx +168 -0
- package/components/forms/FormInput.tsx +5 -3
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/AnalyticsProvider.tsx +67 -0
- package/components/providers/SuspenseBoundary.tsx +359 -357
- package/components/providers/index.ts +24 -21
- package/components/ui/AnimatedButton.tsx +1 -9
- package/components/ui/AnimatedList.tsx +98 -0
- package/components/ui/AnimatedScreen.tsx +89 -0
- package/components/ui/Avatar.tsx +319 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Button.tsx +11 -3
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/FeatureGate.tsx +57 -0
- package/components/ui/ForceUpdateScreen.tsx +108 -0
- package/components/ui/ImagePickerButton.tsx +180 -0
- package/components/ui/Input.stories.tsx +2 -10
- package/components/ui/Input.tsx +2 -10
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Paywall.tsx +253 -0
- package/components/ui/PermissionGate.tsx +155 -0
- package/components/ui/PurchaseButton.tsx +84 -0
- package/components/ui/Select.tsx +240 -240
- package/components/ui/Skeleton.tsx +3 -1
- package/components/ui/Toast.tsx +427 -0
- package/components/ui/UploadProgress.tsx +189 -0
- package/components/ui/VirtualizedList.tsx +288 -285
- package/components/ui/index.ts +28 -23
- package/constants/config.ts +135 -97
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/docs/guides/analytics-posthog.md +121 -0
- package/docs/guides/auth-supabase.md +162 -0
- package/docs/guides/feature-flags-launchdarkly.md +150 -0
- package/docs/guides/payments-revenuecat.md +169 -0
- package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
- package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
- package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
- package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
- package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
- package/eas.json +2 -1
- package/hooks/index.ts +70 -27
- package/hooks/useAnimatedEntry.ts +204 -0
- package/hooks/useApi.ts +64 -4
- package/hooks/useAuth.tsx +7 -3
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useChannel.ts +111 -0
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useExperiment.ts +36 -0
- package/hooks/useFeatureFlag.ts +59 -0
- package/hooks/useForceUpdate.ts +91 -0
- package/hooks/useImagePicker.ts +281 -0
- package/hooks/useInAppReview.ts +64 -0
- package/hooks/useMFA.ts +509 -499
- package/hooks/useParallax.ts +142 -0
- package/hooks/usePerformance.ts +434 -434
- package/hooks/usePermission.ts +190 -0
- package/hooks/usePresence.ts +129 -0
- package/hooks/useProducts.ts +36 -0
- package/hooks/usePurchase.ts +103 -0
- package/hooks/useRateLimit.ts +70 -0
- package/hooks/useSubscription.ts +49 -0
- package/hooks/useTrackEvent.ts +52 -0
- package/hooks/useTrackScreen.ts +40 -0
- package/hooks/useUpdates.ts +358 -358
- package/hooks/useUpload.ts +165 -0
- package/hooks/useWebSocket.ts +111 -0
- package/i18n/index.ts +197 -194
- package/i18n/locales/ar.json +170 -101
- package/i18n/locales/de.json +170 -101
- package/i18n/locales/en.json +170 -101
- package/i18n/locales/es.json +170 -101
- package/i18n/locales/fr.json +170 -101
- package/jest.config.js +1 -1
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -92
- package/maestro/flows/mfa-setup.yaml +86 -86
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -101
- package/maestro/flows/offline-sync.yaml +128 -128
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +188 -175
- package/scripts/generate-placeholders.js +38 -0
- package/services/analytics/adapters/console.ts +50 -0
- package/services/analytics/analytics-adapter.ts +94 -0
- package/services/analytics/types.ts +73 -0
- package/services/analytics.ts +428 -428
- package/services/api.ts +419 -340
- package/services/auth/social/apple.ts +110 -0
- package/services/auth/social/google.ts +159 -0
- package/services/auth/social/social-auth.ts +100 -0
- package/services/auth/social/types.ts +80 -0
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +652 -626
- package/services/feature-flags/adapters/mock.ts +108 -0
- package/services/feature-flags/feature-flag-adapter.ts +174 -0
- package/services/feature-flags/types.ts +79 -0
- package/services/force-update.ts +140 -0
- package/services/index.ts +116 -54
- package/services/media/compression.ts +91 -0
- package/services/media/media-picker.ts +151 -0
- package/services/media/media-upload.ts +160 -0
- package/services/payments/adapters/mock.ts +159 -0
- package/services/payments/payment-adapter.ts +118 -0
- package/services/payments/types.ts +131 -0
- package/services/permissions/permission-manager.ts +284 -0
- package/services/permissions/types.ts +104 -0
- package/services/realtime/types.ts +100 -0
- package/services/realtime/websocket-manager.ts +441 -0
- package/services/security.ts +289 -286
- package/services/sentry.ts +4 -4
- package/stores/appStore.ts +9 -0
- package/stores/notificationStore.ts +3 -1
- package/tailwind.config.js +47 -47
- package/tsconfig.json +37 -13
- package/types/user.ts +1 -1
- package/utils/accessibility.ts +446 -446
- package/utils/animations/presets.ts +182 -0
- package/utils/animations/transitions.ts +62 -0
- package/utils/index.ts +63 -52
- package/utils/toast.ts +9 -2
- package/utils/validation.ts +4 -1
- package/utils/withAccessibility.tsx +272 -272
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mock feature flag adapter for development
|
|
3
|
+
* Provides an in-memory flag store with configurable initial values.
|
|
4
|
+
* Use this adapter during development to test feature gates and experiments
|
|
5
|
+
* without a remote provider.
|
|
6
|
+
* @module services/feature-flags/adapters/mock
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FeatureFlagAdapter } from "../types";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Mock Adapter
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Development adapter that stores flags and experiments in memory.
|
|
17
|
+
* Flags can be pre-seeded via the constructor or changed at runtime with
|
|
18
|
+
* the `setFlag()` and `setExperiment()` helpers -- useful for unit tests.
|
|
19
|
+
*/
|
|
20
|
+
export class MockFeatureFlagAdapter implements FeatureFlagAdapter {
|
|
21
|
+
/** In-memory flag store */
|
|
22
|
+
private flags: Map<string, unknown> = new Map();
|
|
23
|
+
|
|
24
|
+
/** In-memory experiment assignments */
|
|
25
|
+
private experiments: Map<string, string> = new Map();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a mock adapter with optional initial data.
|
|
29
|
+
*
|
|
30
|
+
* @param initialFlags - Pre-seeded flag key-value pairs
|
|
31
|
+
* @param initialExperiments - Pre-seeded experiment assignments
|
|
32
|
+
*/
|
|
33
|
+
constructor(
|
|
34
|
+
initialFlags?: Record<string, unknown>,
|
|
35
|
+
initialExperiments?: Record<string, string>
|
|
36
|
+
) {
|
|
37
|
+
if (initialFlags) {
|
|
38
|
+
for (const [key, value] of Object.entries(initialFlags)) {
|
|
39
|
+
this.flags.set(key, value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (initialExperiments) {
|
|
44
|
+
for (const [key, variant] of Object.entries(initialExperiments)) {
|
|
45
|
+
this.experiments.set(key, variant);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async initialize(): Promise<void> {
|
|
51
|
+
if (__DEV__) {
|
|
52
|
+
console.log(
|
|
53
|
+
`[FeatureFlags] Initialized (mock adapter — ${this.flags.size} flags, ${this.experiments.size} experiments)`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
isEnabled(flag: string): boolean {
|
|
59
|
+
const value = this.flags.get(flag);
|
|
60
|
+
return typeof value === "boolean" ? value : false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getValue<T>(flag: string, defaultValue: T): T {
|
|
64
|
+
if (!this.flags.has(flag)) return defaultValue;
|
|
65
|
+
return this.flags.get(flag) as T;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getExperimentVariant(experimentId: string): string | null {
|
|
69
|
+
return this.experiments.get(experimentId) ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
identify(userId: string, attributes?: Record<string, unknown>): void {
|
|
73
|
+
if (__DEV__) {
|
|
74
|
+
console.log("[FeatureFlags] Identified user:", userId, attributes);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async refresh(): Promise<void> {
|
|
79
|
+
if (__DEV__) {
|
|
80
|
+
console.log("[FeatureFlags] Refreshed (mock adapter — no-op)");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --------------------------------------------------------------------------
|
|
85
|
+
// Test helpers
|
|
86
|
+
// --------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set or update a flag value at runtime.
|
|
90
|
+
* Useful for toggling flags during tests.
|
|
91
|
+
*
|
|
92
|
+
* @param flag - The flag key
|
|
93
|
+
* @param value - The value to set
|
|
94
|
+
*/
|
|
95
|
+
setFlag(flag: string, value: unknown): void {
|
|
96
|
+
this.flags.set(flag, value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Set or update an experiment assignment at runtime.
|
|
101
|
+
*
|
|
102
|
+
* @param experimentId - The experiment identifier
|
|
103
|
+
* @param variant - The variant name to assign
|
|
104
|
+
*/
|
|
105
|
+
setExperiment(experimentId: string, variant: string): void {
|
|
106
|
+
this.experiments.set(experimentId, variant);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Feature flag adapter manager
|
|
3
|
+
* Singleton-style module that delegates all feature flag and A/B testing calls
|
|
4
|
+
* to a pluggable adapter. Defaults to the mock adapter so flag checks work out
|
|
5
|
+
* of the box in development without any extra setup.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { FeatureFlags } from "@/services/feature-flags/feature-flag-adapter";
|
|
9
|
+
*
|
|
10
|
+
* // Swap the adapter for production (e.g. LaunchDarkly)
|
|
11
|
+
* FeatureFlags.setAdapter(new LaunchDarklyAdapter());
|
|
12
|
+
*
|
|
13
|
+
* // Initialize at app start
|
|
14
|
+
* await FeatureFlags.initialize();
|
|
15
|
+
*
|
|
16
|
+
* // Check a flag
|
|
17
|
+
* if (FeatureFlags.isEnabled("new_checkout")) { ... }
|
|
18
|
+
*
|
|
19
|
+
* @module services/feature-flags/feature-flag-adapter
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { FeatureFlagAdapter } from "./types";
|
|
23
|
+
import { MockFeatureFlagAdapter } from "./adapters/mock";
|
|
24
|
+
import { FEATURE_FLAGS } from "@/constants/config";
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Module-level state
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
/** The currently active feature flag adapter */
|
|
31
|
+
let activeAdapter: FeatureFlagAdapter = new MockFeatureFlagAdapter();
|
|
32
|
+
|
|
33
|
+
/** Handle for the auto-refresh interval, if running */
|
|
34
|
+
let refreshInterval: ReturnType<typeof setInterval> | null = null;
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Public API
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Central feature flags facade.
|
|
42
|
+
*
|
|
43
|
+
* Every method delegates to the active adapter so the underlying provider
|
|
44
|
+
* can be swapped without touching calling code.
|
|
45
|
+
*/
|
|
46
|
+
export const FeatureFlags = {
|
|
47
|
+
// --------------------------------------------------------------------------
|
|
48
|
+
// Configuration
|
|
49
|
+
// --------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Replace the active feature flag adapter.
|
|
53
|
+
* Call this before `initialize()` to switch providers.
|
|
54
|
+
*/
|
|
55
|
+
setAdapter(adapter: FeatureFlagAdapter): void {
|
|
56
|
+
activeAdapter = adapter;
|
|
57
|
+
|
|
58
|
+
if (__DEV__) {
|
|
59
|
+
console.log("[FeatureFlags] Adapter set:", adapter.constructor.name);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// --------------------------------------------------------------------------
|
|
64
|
+
// Lifecycle
|
|
65
|
+
// --------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/** Initialize the active adapter. Should be called once at app start. */
|
|
68
|
+
async initialize(): Promise<void> {
|
|
69
|
+
if (!FEATURE_FLAGS.ENABLED) return;
|
|
70
|
+
await activeAdapter.initialize();
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// --------------------------------------------------------------------------
|
|
74
|
+
// Flag evaluation
|
|
75
|
+
// --------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check whether a boolean feature flag is enabled.
|
|
79
|
+
*
|
|
80
|
+
* @param flag - The flag key
|
|
81
|
+
* @param defaultValue - Value to return when the adapter has no data
|
|
82
|
+
* @returns `true` if the flag is enabled
|
|
83
|
+
*/
|
|
84
|
+
isEnabled(flag: string, defaultValue = false): boolean {
|
|
85
|
+
if (!FEATURE_FLAGS.ENABLED) return defaultValue;
|
|
86
|
+
return activeAdapter.isEnabled(flag);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the value of a feature flag with an arbitrary type.
|
|
91
|
+
*
|
|
92
|
+
* @param flag - The flag key
|
|
93
|
+
* @param defaultValue - Value to return when the flag is missing
|
|
94
|
+
* @returns The flag value or the default
|
|
95
|
+
*/
|
|
96
|
+
getValue<T>(flag: string, defaultValue: T): T {
|
|
97
|
+
if (!FEATURE_FLAGS.ENABLED) return defaultValue;
|
|
98
|
+
return activeAdapter.getValue(flag, defaultValue);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// --------------------------------------------------------------------------
|
|
102
|
+
// Experiments
|
|
103
|
+
// --------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the assigned variant for an A/B test experiment.
|
|
107
|
+
*
|
|
108
|
+
* @param experimentId - The experiment identifier
|
|
109
|
+
* @returns The variant name or `null` if the user is not enrolled
|
|
110
|
+
*/
|
|
111
|
+
getExperimentVariant(experimentId: string): string | null {
|
|
112
|
+
if (!FEATURE_FLAGS.ENABLED) return null;
|
|
113
|
+
return activeAdapter.getExperimentVariant(experimentId);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// --------------------------------------------------------------------------
|
|
117
|
+
// User targeting
|
|
118
|
+
// --------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Identify the current user for targeted flag evaluation.
|
|
122
|
+
*
|
|
123
|
+
* @param userId - Unique identifier for the user
|
|
124
|
+
* @param attributes - Optional targeting attributes
|
|
125
|
+
*/
|
|
126
|
+
identify(userId: string, attributes?: Record<string, unknown>): void {
|
|
127
|
+
if (!FEATURE_FLAGS.ENABLED) return;
|
|
128
|
+
activeAdapter.identify(userId, attributes);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// --------------------------------------------------------------------------
|
|
132
|
+
// Refresh
|
|
133
|
+
// --------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/** Refresh flag values from the remote provider. */
|
|
136
|
+
async refresh(): Promise<void> {
|
|
137
|
+
if (!FEATURE_FLAGS.ENABLED) return;
|
|
138
|
+
await activeAdapter.refresh();
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Start auto-refreshing flags at the interval defined in config.
|
|
143
|
+
* Calls `refresh()` on the active adapter periodically.
|
|
144
|
+
*/
|
|
145
|
+
startAutoRefresh(): void {
|
|
146
|
+
if (refreshInterval) return; // already running
|
|
147
|
+
|
|
148
|
+
refreshInterval = setInterval(() => {
|
|
149
|
+
activeAdapter.refresh().catch((err) => {
|
|
150
|
+
if (__DEV__) {
|
|
151
|
+
console.warn("[FeatureFlags] Auto-refresh failed:", err);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}, FEATURE_FLAGS.REFRESH_INTERVAL_MS);
|
|
155
|
+
|
|
156
|
+
if (__DEV__) {
|
|
157
|
+
console.log(
|
|
158
|
+
`[FeatureFlags] Auto-refresh started (every ${FEATURE_FLAGS.REFRESH_INTERVAL_MS}ms)`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
/** Stop the auto-refresh interval. */
|
|
164
|
+
stopAutoRefresh(): void {
|
|
165
|
+
if (refreshInterval) {
|
|
166
|
+
clearInterval(refreshInterval);
|
|
167
|
+
refreshInterval = null;
|
|
168
|
+
|
|
169
|
+
if (__DEV__) {
|
|
170
|
+
console.log("[FeatureFlags] Auto-refresh stopped");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Feature flag and A/B testing type definitions
|
|
3
|
+
* Defines the adapter interface and configuration for the feature flag system.
|
|
4
|
+
* @module services/feature-flags/types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Adapter Interface
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Interface that all feature flag adapters must implement.
|
|
13
|
+
* Swap adapters to switch between providers (LaunchDarkly, Statsig, etc.)
|
|
14
|
+
* without changing application code.
|
|
15
|
+
*/
|
|
16
|
+
export interface FeatureFlagAdapter {
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the feature flag provider.
|
|
19
|
+
* Called once when the app starts.
|
|
20
|
+
*/
|
|
21
|
+
initialize(): Promise<void>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check whether a boolean feature flag is enabled.
|
|
25
|
+
*
|
|
26
|
+
* @param flag - The flag key (e.g. "new_onboarding")
|
|
27
|
+
* @returns `true` if the flag is enabled, `false` otherwise
|
|
28
|
+
*/
|
|
29
|
+
isEnabled(flag: string): boolean;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the value of a feature flag with an arbitrary type.
|
|
33
|
+
* Returns the provided default when the flag is not found.
|
|
34
|
+
*
|
|
35
|
+
* @param flag - The flag key
|
|
36
|
+
* @param defaultValue - Value to return if the flag is missing
|
|
37
|
+
* @returns The flag value or the default
|
|
38
|
+
*/
|
|
39
|
+
getValue<T>(flag: string, defaultValue: T): T;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the assigned variant for an A/B test experiment.
|
|
43
|
+
*
|
|
44
|
+
* @param experimentId - The experiment identifier
|
|
45
|
+
* @returns The variant name (e.g. "control", "variant_a") or `null` if the
|
|
46
|
+
* user is not enrolled
|
|
47
|
+
*/
|
|
48
|
+
getExperimentVariant(experimentId: string): string | null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Identify the current user so the provider can return
|
|
52
|
+
* user-targeted flags and experiments.
|
|
53
|
+
*
|
|
54
|
+
* @param userId - Unique identifier for the user
|
|
55
|
+
* @param attributes - Optional targeting attributes (plan, country, etc.)
|
|
56
|
+
*/
|
|
57
|
+
identify(userId: string, attributes?: Record<string, unknown>): void;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Refresh flag values from the remote provider.
|
|
61
|
+
*/
|
|
62
|
+
refresh(): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Configuration
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Runtime configuration for the feature flag system.
|
|
71
|
+
*/
|
|
72
|
+
export interface FeatureFlagConfig {
|
|
73
|
+
/** Whether feature flag evaluation is enabled */
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
/** How often (in ms) to auto-refresh flags from the remote provider */
|
|
76
|
+
refreshIntervalMs: number;
|
|
77
|
+
/** Whether to log debug information to the console */
|
|
78
|
+
debug: boolean;
|
|
79
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Force update service for mandatory native app updates
|
|
3
|
+
* Checks a remote endpoint for the minimum required app version and
|
|
4
|
+
* determines whether the user must update before continuing.
|
|
5
|
+
* @module services/force-update
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface ForceUpdateConfig {
|
|
13
|
+
/** API endpoint that returns { minimumVersion, storeUrl } */
|
|
14
|
+
checkUrl: string;
|
|
15
|
+
/** The currently running app version (e.g. "1.2.3") */
|
|
16
|
+
currentVersion: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ForceUpdateResponse {
|
|
20
|
+
/** Minimum version the server requires */
|
|
21
|
+
minimumVersion: string;
|
|
22
|
+
/** App Store or Play Store URL for the update */
|
|
23
|
+
storeUrl: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ForceUpdateResult {
|
|
27
|
+
/** Whether the user must update before using the app */
|
|
28
|
+
isUpdateRequired: boolean;
|
|
29
|
+
/** The version currently running */
|
|
30
|
+
currentVersion: string;
|
|
31
|
+
/** The minimum version required by the server */
|
|
32
|
+
minimumVersion: string;
|
|
33
|
+
/** Store URL to open for the update */
|
|
34
|
+
storeUrl: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Helpers
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compare two semver-style version strings segment by segment.
|
|
43
|
+
* Returns true when `current` is older than `minimum`.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* isVersionLessThan("1.2.3", "1.3.0") // true
|
|
47
|
+
* isVersionLessThan("2.0.0", "1.9.9") // false
|
|
48
|
+
* isVersionLessThan("1.2.3", "1.2.3") // false
|
|
49
|
+
*/
|
|
50
|
+
export function isVersionLessThan(current: string, minimum: string): boolean {
|
|
51
|
+
const currentParts = current.split(".").map(Number);
|
|
52
|
+
const minimumParts = minimum.split(".").map(Number);
|
|
53
|
+
|
|
54
|
+
const length = Math.max(currentParts.length, minimumParts.length);
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < length; i++) {
|
|
57
|
+
const curr = currentParts[i] ?? 0;
|
|
58
|
+
const min = minimumParts[i] ?? 0;
|
|
59
|
+
|
|
60
|
+
if (curr < min) return true;
|
|
61
|
+
if (curr > min) return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Main
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
const NO_UPDATE_RESULT: ForceUpdateResult = {
|
|
72
|
+
isUpdateRequired: false,
|
|
73
|
+
currentVersion: "",
|
|
74
|
+
minimumVersion: "",
|
|
75
|
+
storeUrl: "",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check whether the app needs a mandatory native update.
|
|
80
|
+
*
|
|
81
|
+
* Fetches the `checkUrl`, compares the server-provided `minimumVersion`
|
|
82
|
+
* against `currentVersion` using semver-style comparison, and returns the
|
|
83
|
+
* result.
|
|
84
|
+
*
|
|
85
|
+
* - If `FORCE_UPDATE.ENABLED` is false or `CHECK_URL` is empty the caller
|
|
86
|
+
* should skip the call entirely (handled in the hook).
|
|
87
|
+
* - Network / parsing errors are swallowed and return `isUpdateRequired: false`
|
|
88
|
+
* so the app is never blocked by a transient failure.
|
|
89
|
+
*
|
|
90
|
+
* @param config - The force update configuration
|
|
91
|
+
* @returns The result of the version check
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* const result = await checkForUpdate({
|
|
96
|
+
* checkUrl: "https://api.example.com/app/version",
|
|
97
|
+
* currentVersion: "1.0.0",
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* if (result.isUpdateRequired) {
|
|
101
|
+
* // Show force-update screen
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export async function checkForUpdate(
|
|
106
|
+
config: ForceUpdateConfig
|
|
107
|
+
): Promise<ForceUpdateResult> {
|
|
108
|
+
const { checkUrl, currentVersion } = config;
|
|
109
|
+
|
|
110
|
+
if (!checkUrl) {
|
|
111
|
+
return NO_UPDATE_RESULT;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(checkUrl);
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
return NO_UPDATE_RESULT;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const data: ForceUpdateResponse = await response.json();
|
|
122
|
+
const { minimumVersion, storeUrl } = data;
|
|
123
|
+
|
|
124
|
+
if (!minimumVersion || !storeUrl) {
|
|
125
|
+
return NO_UPDATE_RESULT;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const isUpdateRequired = isVersionLessThan(currentVersion, minimumVersion);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
isUpdateRequired,
|
|
132
|
+
currentVersion,
|
|
133
|
+
minimumVersion,
|
|
134
|
+
storeUrl,
|
|
135
|
+
};
|
|
136
|
+
} catch {
|
|
137
|
+
// Network errors, JSON parse errors, etc. — never block the app.
|
|
138
|
+
return NO_UPDATE_RESULT;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/services/index.ts
CHANGED
|
@@ -1,54 +1,116 @@
|
|
|
1
|
-
export { api, ApiClient } from "./api";
|
|
2
|
-
export { storage, secureStorage } from "./storage";
|
|
3
|
-
export {
|
|
4
|
-
initSentry,
|
|
5
|
-
captureException,
|
|
6
|
-
captureMessage,
|
|
7
|
-
setUser,
|
|
8
|
-
addBreadcrumb,
|
|
9
|
-
} from "./sentry";
|
|
10
|
-
export { authAdapter, mockAuthAdapter } from "./authAdapter";
|
|
11
|
-
export type { AuthAdapter, AuthResult, AuthError } from "./authAdapter";
|
|
12
|
-
export {
|
|
13
|
-
analytics,
|
|
14
|
-
track,
|
|
15
|
-
identify,
|
|
16
|
-
screen,
|
|
17
|
-
resetAnalytics,
|
|
18
|
-
setUserProperties,
|
|
19
|
-
trackRevenue,
|
|
20
|
-
AnalyticsEvents,
|
|
21
|
-
} from "./analytics";
|
|
22
|
-
export type { AnalyticsAdapter, AnalyticsEvent } from "./analytics";
|
|
23
|
-
export {
|
|
24
|
-
getCertificatePins,
|
|
25
|
-
isSslPinningEnabled,
|
|
26
|
-
getSecurityHeaders,
|
|
27
|
-
isUrlAllowed,
|
|
28
|
-
sanitizeInput,
|
|
29
|
-
checkSecurityEnvironment,
|
|
30
|
-
SSL_PINNING_CONFIG,
|
|
31
|
-
} from "./security";
|
|
32
|
-
export {
|
|
33
|
-
queueMutation,
|
|
34
|
-
getMutationQueue,
|
|
35
|
-
removeMutation,
|
|
36
|
-
clearMutationQueue,
|
|
37
|
-
getPendingMutationCount,
|
|
38
|
-
processQueue,
|
|
39
|
-
registerBackgroundSync,
|
|
40
|
-
unregisterBackgroundSync,
|
|
41
|
-
isBackgroundSyncRegistered,
|
|
42
|
-
getBackgroundSyncStatus,
|
|
43
|
-
isNetworkError,
|
|
44
|
-
registerConflictResolver,
|
|
45
|
-
unregisterConflictResolver,
|
|
46
|
-
getPendingConflicts,
|
|
47
|
-
clearPendingConflict,
|
|
48
|
-
} from "./backgroundSync";
|
|
49
|
-
export type {
|
|
50
|
-
QueuedMutation,
|
|
51
|
-
ConflictResolutionStrategy,
|
|
52
|
-
SyncConflict,
|
|
53
|
-
ConflictResolver,
|
|
54
|
-
} from "./backgroundSync";
|
|
1
|
+
export { api, ApiClient } from "./api";
|
|
2
|
+
export { storage, secureStorage } from "./storage";
|
|
3
|
+
export {
|
|
4
|
+
initSentry,
|
|
5
|
+
captureException,
|
|
6
|
+
captureMessage,
|
|
7
|
+
setUser,
|
|
8
|
+
addBreadcrumb,
|
|
9
|
+
} from "./sentry";
|
|
10
|
+
export { authAdapter, mockAuthAdapter } from "./authAdapter";
|
|
11
|
+
export type { AuthAdapter, AuthResult, AuthError } from "./authAdapter";
|
|
12
|
+
export {
|
|
13
|
+
analytics,
|
|
14
|
+
track,
|
|
15
|
+
identify,
|
|
16
|
+
screen,
|
|
17
|
+
resetAnalytics,
|
|
18
|
+
setUserProperties,
|
|
19
|
+
trackRevenue,
|
|
20
|
+
AnalyticsEvents,
|
|
21
|
+
} from "./analytics";
|
|
22
|
+
export type { AnalyticsAdapter, AnalyticsEvent } from "./analytics";
|
|
23
|
+
export {
|
|
24
|
+
getCertificatePins,
|
|
25
|
+
isSslPinningEnabled,
|
|
26
|
+
getSecurityHeaders,
|
|
27
|
+
isUrlAllowed,
|
|
28
|
+
sanitizeInput,
|
|
29
|
+
checkSecurityEnvironment,
|
|
30
|
+
SSL_PINNING_CONFIG,
|
|
31
|
+
} from "./security";
|
|
32
|
+
export {
|
|
33
|
+
queueMutation,
|
|
34
|
+
getMutationQueue,
|
|
35
|
+
removeMutation,
|
|
36
|
+
clearMutationQueue,
|
|
37
|
+
getPendingMutationCount,
|
|
38
|
+
processQueue,
|
|
39
|
+
registerBackgroundSync,
|
|
40
|
+
unregisterBackgroundSync,
|
|
41
|
+
isBackgroundSyncRegistered,
|
|
42
|
+
getBackgroundSyncStatus,
|
|
43
|
+
isNetworkError,
|
|
44
|
+
registerConflictResolver,
|
|
45
|
+
unregisterConflictResolver,
|
|
46
|
+
getPendingConflicts,
|
|
47
|
+
clearPendingConflict,
|
|
48
|
+
} from "./backgroundSync";
|
|
49
|
+
export type {
|
|
50
|
+
QueuedMutation,
|
|
51
|
+
ConflictResolutionStrategy,
|
|
52
|
+
SyncConflict,
|
|
53
|
+
ConflictResolver,
|
|
54
|
+
} from "./backgroundSync";
|
|
55
|
+
export {
|
|
56
|
+
PermissionManager,
|
|
57
|
+
normalizeStatus,
|
|
58
|
+
} from "./permissions/permission-manager";
|
|
59
|
+
export { DEFAULT_PERMISSION_CONFIGS } from "./permissions/types";
|
|
60
|
+
export type {
|
|
61
|
+
PermissionType,
|
|
62
|
+
PermissionStatus,
|
|
63
|
+
PermissionResult,
|
|
64
|
+
PermissionConfig,
|
|
65
|
+
} from "./permissions/types";
|
|
66
|
+
export { Analytics } from "./analytics/analytics-adapter";
|
|
67
|
+
export { ConsoleAnalyticsAdapter } from "./analytics/adapters/console";
|
|
68
|
+
export type {
|
|
69
|
+
AnalyticsAdapter as AnalyticsAdapterInterface,
|
|
70
|
+
AnalyticsConfig,
|
|
71
|
+
} from "./analytics/types";
|
|
72
|
+
export { Payments } from "./payments/payment-adapter";
|
|
73
|
+
export { MockPaymentAdapter, MOCK_PRODUCTS } from "./payments/adapters/mock";
|
|
74
|
+
export type {
|
|
75
|
+
PaymentAdapter,
|
|
76
|
+
Product,
|
|
77
|
+
Purchase,
|
|
78
|
+
ProductType,
|
|
79
|
+
SubscriptionPeriod,
|
|
80
|
+
SubscriptionStatus,
|
|
81
|
+
SubscriptionInfo,
|
|
82
|
+
} from "./payments/types";
|
|
83
|
+
export { pickFromLibrary, pickFromCamera } from "./media/media-picker";
|
|
84
|
+
export type { PickedMedia, PickOptions } from "./media/media-picker";
|
|
85
|
+
export { compressImage } from "./media/compression";
|
|
86
|
+
export type {
|
|
87
|
+
CompressionOptions,
|
|
88
|
+
CompressionResult,
|
|
89
|
+
} from "./media/compression";
|
|
90
|
+
export { uploadFile } from "./media/media-upload";
|
|
91
|
+
export type {
|
|
92
|
+
UploadProgress,
|
|
93
|
+
UploadOptions,
|
|
94
|
+
UploadResult,
|
|
95
|
+
} from "./media/media-upload";
|
|
96
|
+
export { WebSocketManager } from "./realtime/websocket-manager";
|
|
97
|
+
export type {
|
|
98
|
+
ConnectionStatus,
|
|
99
|
+
WebSocketConfig,
|
|
100
|
+
WebSocketMessage,
|
|
101
|
+
MessageHandler,
|
|
102
|
+
StatusHandler,
|
|
103
|
+
PresenceUser,
|
|
104
|
+
} from "./realtime/types";
|
|
105
|
+
export { checkForUpdate, isVersionLessThan } from "./force-update";
|
|
106
|
+
export type {
|
|
107
|
+
ForceUpdateConfig,
|
|
108
|
+
ForceUpdateResponse,
|
|
109
|
+
ForceUpdateResult,
|
|
110
|
+
} from "./force-update";
|
|
111
|
+
export { FeatureFlags } from "./feature-flags/feature-flag-adapter";
|
|
112
|
+
export { MockFeatureFlagAdapter } from "./feature-flags/adapters/mock";
|
|
113
|
+
export type {
|
|
114
|
+
FeatureFlagAdapter,
|
|
115
|
+
FeatureFlagConfig,
|
|
116
|
+
} from "./feature-flags/types";
|