mixpanel-react-native 3.1.2 → 3.2.0-beta.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 (93) hide show
  1. package/.claude/settings.local.json +12 -1
  2. package/.github/dependabot.yml +7 -0
  3. package/.github/workflows/node.js.yml +24 -1
  4. package/.vscode/settings.json +2 -1
  5. package/MixpanelReactNative.podspec +1 -1
  6. package/Samples/MixpanelExample/ios/MixpanelExample.xcworkspace/contents.xcworkspacedata +10 -0
  7. package/Samples/MixpanelExample/ios/Podfile.lock +1996 -0
  8. package/Samples/MixpanelStarter/.bundle/config +2 -0
  9. package/Samples/MixpanelStarter/.env.example +4 -0
  10. package/Samples/MixpanelStarter/.eslintrc.js +4 -0
  11. package/Samples/MixpanelStarter/.prettierrc.js +5 -0
  12. package/Samples/MixpanelStarter/.watchmanconfig +1 -0
  13. package/Samples/MixpanelStarter/App.tsx +10 -0
  14. package/Samples/MixpanelStarter/CLAUDE.md +538 -0
  15. package/Samples/MixpanelStarter/Gemfile +16 -0
  16. package/Samples/MixpanelStarter/INTEGRATION_GUIDE.md +606 -0
  17. package/Samples/MixpanelStarter/README.md +406 -0
  18. package/Samples/MixpanelStarter/__tests__/MixpanelContext.test.tsx +63 -0
  19. package/Samples/MixpanelStarter/android/app/build.gradle +119 -0
  20. package/Samples/MixpanelStarter/android/app/debug.keystore +0 -0
  21. package/Samples/MixpanelStarter/android/app/proguard-rules.pro +10 -0
  22. package/Samples/MixpanelStarter/android/app/src/main/AndroidManifest.xml +27 -0
  23. package/Samples/MixpanelStarter/android/app/src/main/java/com/mixpanelstarter/MainActivity.kt +22 -0
  24. package/Samples/MixpanelStarter/android/app/src/main/java/com/mixpanelstarter/MainApplication.kt +27 -0
  25. package/Samples/MixpanelStarter/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  26. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  27. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  28. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  29. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  30. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  31. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  32. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  33. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  34. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  35. package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  36. package/Samples/MixpanelStarter/android/app/src/main/res/values/strings.xml +3 -0
  37. package/Samples/MixpanelStarter/android/app/src/main/res/values/styles.xml +9 -0
  38. package/Samples/MixpanelStarter/android/build.gradle +21 -0
  39. package/Samples/MixpanelStarter/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  40. package/Samples/MixpanelStarter/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  41. package/Samples/MixpanelStarter/android/gradle.properties +44 -0
  42. package/Samples/MixpanelStarter/android/gradlew +251 -0
  43. package/Samples/MixpanelStarter/android/gradlew.bat +99 -0
  44. package/Samples/MixpanelStarter/android/settings.gradle +6 -0
  45. package/Samples/MixpanelStarter/app.json +4 -0
  46. package/Samples/MixpanelStarter/babel.config.js +14 -0
  47. package/Samples/MixpanelStarter/index.js +9 -0
  48. package/Samples/MixpanelStarter/ios/.xcode.env +11 -0
  49. package/Samples/MixpanelStarter/ios/MixpanelStarter/AppDelegate.swift +48 -0
  50. package/Samples/MixpanelStarter/ios/MixpanelStarter/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
  51. package/Samples/MixpanelStarter/ios/MixpanelStarter/Images.xcassets/Contents.json +6 -0
  52. package/Samples/MixpanelStarter/ios/MixpanelStarter/Info.plist +55 -0
  53. package/Samples/MixpanelStarter/ios/MixpanelStarter/LaunchScreen.storyboard +47 -0
  54. package/Samples/MixpanelStarter/ios/MixpanelStarter/PrivacyInfo.xcprivacy +38 -0
  55. package/Samples/MixpanelStarter/ios/MixpanelStarter.xcodeproj/project.pbxproj +482 -0
  56. package/Samples/MixpanelStarter/ios/MixpanelStarter.xcodeproj/xcshareddata/xcschemes/MixpanelStarter.xcscheme +88 -0
  57. package/Samples/MixpanelStarter/ios/MixpanelStarter.xcworkspace/contents.xcworkspacedata +10 -0
  58. package/Samples/MixpanelStarter/ios/Podfile +34 -0
  59. package/Samples/MixpanelStarter/ios/Podfile.lock +2839 -0
  60. package/Samples/MixpanelStarter/jest.config.js +3 -0
  61. package/Samples/MixpanelStarter/metro.config.js +42 -0
  62. package/Samples/MixpanelStarter/package-lock.json +12141 -0
  63. package/Samples/MixpanelStarter/package.json +51 -0
  64. package/Samples/MixpanelStarter/src/@types/env.d.ts +3 -0
  65. package/Samples/MixpanelStarter/src/App.tsx +83 -0
  66. package/Samples/MixpanelStarter/src/components/ActionButton.tsx +92 -0
  67. package/Samples/MixpanelStarter/src/components/ErrorBoundary.tsx +81 -0
  68. package/Samples/MixpanelStarter/src/components/EventTrackingLog.tsx +163 -0
  69. package/Samples/MixpanelStarter/src/components/FlagCard.tsx +199 -0
  70. package/Samples/MixpanelStarter/src/components/InfoCard.tsx +77 -0
  71. package/Samples/MixpanelStarter/src/components/TestResultDisplay.tsx +181 -0
  72. package/Samples/MixpanelStarter/src/constants/tracking.ts +77 -0
  73. package/Samples/MixpanelStarter/src/contexts/MixpanelContext.tsx +159 -0
  74. package/Samples/MixpanelStarter/src/screens/FeatureFlagsScreen.tsx +1011 -0
  75. package/Samples/MixpanelStarter/src/screens/HomeScreen.tsx +307 -0
  76. package/Samples/MixpanelStarter/src/screens/OnboardingScreen.tsx +253 -0
  77. package/Samples/MixpanelStarter/src/screens/SettingsScreen.tsx +316 -0
  78. package/Samples/MixpanelStarter/src/types/flags.types.ts +42 -0
  79. package/Samples/MixpanelStarter/src/types/mixpanel.types.ts +26 -0
  80. package/Samples/MixpanelStarter/tsconfig.json +13 -0
  81. package/__tests__/flags.test.js +730 -0
  82. package/__tests__/index.test.js +7 -3
  83. package/__tests__/jest_setup.js +18 -0
  84. package/android/build.gradle +1 -1
  85. package/android/src/main/java/com/mixpanel/reactnative/MixpanelReactNativeModule.java +272 -2
  86. package/index.d.ts +64 -1
  87. package/index.js +42 -3
  88. package/ios/MixpanelReactNative.m +19 -1
  89. package/ios/MixpanelReactNative.swift +183 -5
  90. package/javascript/mixpanel-flags-js.js +463 -0
  91. package/javascript/mixpanel-flags.js +290 -0
  92. package/javascript/mixpanel-main.js +13 -1
  93. package/package.json +2 -2
@@ -0,0 +1,1011 @@
1
+ import React, {useState, useEffect, useCallback} from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ ScrollView,
7
+ ActivityIndicator,
8
+ Alert,
9
+ TouchableOpacity,
10
+ } from 'react-native';
11
+ import {useMixpanel} from '../contexts/MixpanelContext';
12
+ import {ActionButton} from '../components/ActionButton';
13
+ import {InfoCard} from '../components/InfoCard';
14
+ import {FlagCard} from '../components/FlagCard';
15
+ import {TestResultDisplay} from '../components/TestResultDisplay';
16
+ import {EventTrackingLog} from '../components/EventTrackingLog';
17
+ import {Events, Properties} from '../constants/tracking';
18
+ import {
19
+ FlagInfo,
20
+ TestResult,
21
+ TrackedEvent,
22
+ TestMode,
23
+ ValueType,
24
+ } from '../types/flags.types';
25
+
26
+ // Existing flags from your Mixpanel project
27
+ // Selected to demonstrate different flag types and scenarios
28
+ const RECOMMENDED_FLAGS = {
29
+ // Boolean FeatureGate flags (value: true/false)
30
+ 'sample-bool-flag': 'boolean',
31
+ 'hash-slinging-slasher': 'boolean',
32
+ 'mike-test': 'boolean',
33
+
34
+ // String Experiment flags (custom variants)
35
+ 'sample-exp-testing': 'string-experiment',
36
+ 'new_feature_flag_v2': 'string-variant',
37
+ 'mojojojo': 'string-variant',
38
+ 'af_ff_music_finder_test': 'string-variant',
39
+
40
+ // Experiment with active tracking
41
+ 'general-replay-events-query-improvement': 'active-experiment',
42
+ 'test-active-flag': 'active-experiment',
43
+
44
+ // Dynamic Config (object values)
45
+ 'matthew-dynamic-7': 'object-config',
46
+ 'mike-dynamic-config-4': 'complex-object',
47
+ } as const;
48
+
49
+ const getValueType = (value: any): ValueType => {
50
+ if (value === null) return 'null';
51
+ if (value === undefined) return 'undefined';
52
+ if (Array.isArray(value)) return 'array';
53
+ return typeof value as ValueType;
54
+ };
55
+
56
+ export const FeatureFlagsScreen: React.FC = () => {
57
+ const {mixpanel, isInitialized, track} = useMixpanel();
58
+
59
+ // State
60
+ const [flagsReady, setFlagsReady] = useState(false);
61
+ const [isLoadingFlags, setIsLoadingFlags] = useState(false);
62
+ const [allFlags, setAllFlags] = useState<Record<string, FlagInfo>>({});
63
+ const [selectedFlag, setSelectedFlag] = useState<string>('react-native');
64
+ const [testMode, setTestMode] = useState<TestMode>('sync');
65
+ const [testResult, setTestResult] = useState<TestResult | null>(null);
66
+ const [trackedEvents, setTrackedEvents] = useState<TrackedEvent[]>([]);
67
+
68
+
69
+ // Track screen view
70
+ useEffect(() => {
71
+ if (isInitialized) {
72
+ track(Events.SCREEN_VIEWED, {
73
+ [Properties.SCREEN_NAME]: 'Feature Flags',
74
+ [Properties.TIMESTAMP]: new Date().toISOString(),
75
+ });
76
+ console.log('Tracked SCREEN_VIEWED for Feature Flags screen');
77
+ }
78
+ }, [isInitialized, track]);
79
+
80
+ // Check if flags are ready on mount and after load
81
+ useEffect(() => {
82
+ if (mixpanel && isInitialized) {
83
+ const ready = mixpanel.flags.areFlagsReady();
84
+ setFlagsReady(ready);
85
+
86
+ if (ready) {
87
+ refreshAllFlags();
88
+ }
89
+ }
90
+ }, [mixpanel, isInitialized]);
91
+
92
+ // Intercept track calls to log them
93
+ const trackWithLog = useCallback(
94
+ (eventName: string, properties?: Record<string, any>) => {
95
+ const event: TrackedEvent = {
96
+ id: `${Date.now()}-${Math.random()}`,
97
+ timestamp: new Date(),
98
+ eventName,
99
+ properties: properties || {},
100
+ };
101
+ setTrackedEvents(prev => [event, ...prev].slice(0, 20));
102
+ track(eventName, properties);
103
+ },
104
+ [track],
105
+ );
106
+
107
+ // Refresh all flags from Mixpanel
108
+ const refreshAllFlags = useCallback(() => {
109
+ if (!mixpanel || !flagsReady) return;
110
+
111
+ const flags: Record<string, FlagInfo> = {};
112
+
113
+ // Get all recommended flags
114
+ Object.keys(RECOMMENDED_FLAGS).forEach(flagKey => {
115
+ try {
116
+ const variant = mixpanel.flags.getVariantSync(flagKey, {
117
+ key: 'fallback',
118
+ value: null,
119
+ });
120
+
121
+ let displayValue = variant.value;
122
+
123
+ // Handle dynamic config flags that return JSON strings
124
+ if (variant.key === '$dynamic_config' && typeof variant.value === 'string') {
125
+ try {
126
+ displayValue = JSON.parse(variant.value);
127
+ } catch (e) {
128
+ // If parsing fails, keep as string
129
+ displayValue = variant.value;
130
+ }
131
+ }
132
+
133
+ flags[flagKey] = {
134
+ key: flagKey,
135
+ value: displayValue,
136
+ valueType: getValueType(displayValue),
137
+ variantKey: variant.key,
138
+ experimentID: variant.experimentID,
139
+ isExperimentActive: variant.isExperimentActive,
140
+ isQATester: variant.isQATester,
141
+ lastAccessed: new Date(),
142
+ };
143
+ } catch (error) {
144
+ console.error(`Failed to get flag ${flagKey}:`, error);
145
+ }
146
+ });
147
+
148
+ setAllFlags(flags);
149
+ }, [mixpanel, flagsReady]);
150
+
151
+ // Load flags from Mixpanel
152
+ const handleLoadFlags = async () => {
153
+ if (!mixpanel) {
154
+ Alert.alert('Error', 'Mixpanel not initialized');
155
+ return;
156
+ }
157
+
158
+ try {
159
+ setIsLoadingFlags(true);
160
+
161
+ await mixpanel.flags.loadFlags();
162
+
163
+ setFlagsReady(true);
164
+
165
+ trackWithLog(Events.FLAGS_LOADED, {
166
+ [Properties.TIMESTAMP]: new Date().toISOString(),
167
+ });
168
+
169
+ refreshAllFlags();
170
+
171
+ Alert.alert(
172
+ 'Flags Loaded',
173
+ `Successfully fetched ${Object.keys(allFlags).length} feature flags!`,
174
+ );
175
+ } catch (error) {
176
+ console.error('Failed to load flags:', error);
177
+ Alert.alert(
178
+ 'Load Failed',
179
+ `Failed to load feature flags: ${error instanceof Error ? error.message : String(error)}`,
180
+ );
181
+ } finally {
182
+ setIsLoadingFlags(false);
183
+ }
184
+ };
185
+
186
+ // Helper to create test result
187
+ const createTestResult = (
188
+ method: string,
189
+ flagName: string,
190
+ fallback: any,
191
+ result: any,
192
+ startTime: number,
193
+ error?: string,
194
+ ): TestResult => {
195
+ const executionTime = Date.now() - startTime;
196
+ const resultType = getValueType(result);
197
+ const usedFallback =
198
+ JSON.stringify(result) === JSON.stringify(fallback) || error !== undefined;
199
+
200
+ return {
201
+ id: `${Date.now()}-${Math.random()}`,
202
+ timestamp: new Date(),
203
+ method,
204
+ flagName,
205
+ fallback,
206
+ result,
207
+ resultType,
208
+ executionTime,
209
+ usedFallback,
210
+ error,
211
+ };
212
+ };
213
+
214
+ // Sync Method Tests
215
+ const testGetVariantSync = () => {
216
+ if (!mixpanel) return;
217
+
218
+ const fallback = {key: 'fallback', value: JSON.parse(customFallback)};
219
+ const startTime = Date.now();
220
+
221
+ try {
222
+ const result = mixpanel.flags.getVariantSync(selectedFlag, fallback);
223
+ const testResult = createTestResult(
224
+ `getVariantSync('${selectedFlag}', fallback)`,
225
+ selectedFlag,
226
+ fallback,
227
+ result,
228
+ startTime,
229
+ );
230
+
231
+ setTestResult(testResult);
232
+ trackWithLog(Events.FLAG_TEST_SYNC, {
233
+ [Properties.FLAG_METHOD]: 'getVariantSync',
234
+ [Properties.FLAG_KEY]: selectedFlag,
235
+ [Properties.FLAG_VALUE]: result.value,
236
+ [Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
237
+ });
238
+ } catch (error) {
239
+ const testResult = createTestResult(
240
+ `getVariantSync('${selectedFlag}', fallback)`,
241
+ selectedFlag,
242
+ fallback,
243
+ fallback,
244
+ startTime,
245
+ error instanceof Error ? error.message : String(error),
246
+ );
247
+ setTestResult(testResult);
248
+ }
249
+ };
250
+
251
+ const testGetVariantValueSync = () => {
252
+ if (!mixpanel) return;
253
+
254
+ const fallback = JSON.parse(customFallback);
255
+ const startTime = Date.now();
256
+
257
+ try {
258
+ const result = mixpanel.flags.getVariantValueSync(selectedFlag, fallback);
259
+ const testResult = createTestResult(
260
+ `getVariantValueSync('${selectedFlag}', ${customFallback})`,
261
+ selectedFlag,
262
+ fallback,
263
+ result,
264
+ startTime,
265
+ );
266
+
267
+ setTestResult(testResult);
268
+ trackWithLog(Events.FLAG_TEST_SYNC, {
269
+ [Properties.FLAG_METHOD]: 'getVariantValueSync',
270
+ [Properties.FLAG_KEY]: selectedFlag,
271
+ [Properties.FLAG_VALUE]: result,
272
+ [Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
273
+ });
274
+ } catch (error) {
275
+ const testResult = createTestResult(
276
+ `getVariantValueSync('${selectedFlag}', ${customFallback})`,
277
+ selectedFlag,
278
+ fallback,
279
+ fallback,
280
+ startTime,
281
+ error instanceof Error ? error.message : String(error),
282
+ );
283
+ setTestResult(testResult);
284
+ }
285
+ };
286
+
287
+ const testIsEnabledSync = () => {
288
+ if (!mixpanel) return;
289
+
290
+ const fallback = false;
291
+ const startTime = Date.now();
292
+
293
+ try {
294
+ const result = mixpanel.flags.isEnabledSync(selectedFlag, fallback);
295
+ const testResult = createTestResult(
296
+ `isEnabledSync('${selectedFlag}', false)`,
297
+ selectedFlag,
298
+ fallback,
299
+ result,
300
+ startTime,
301
+ );
302
+
303
+ setTestResult(testResult);
304
+ trackWithLog(Events.FLAG_TEST_SYNC, {
305
+ [Properties.FLAG_METHOD]: 'isEnabledSync',
306
+ [Properties.FLAG_KEY]: selectedFlag,
307
+ [Properties.FLAG_ENABLED]: result,
308
+ [Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
309
+ });
310
+ } catch (error) {
311
+ const testResult = createTestResult(
312
+ `isEnabledSync('${selectedFlag}', false)`,
313
+ selectedFlag,
314
+ fallback,
315
+ fallback,
316
+ startTime,
317
+ error instanceof Error ? error.message : String(error),
318
+ );
319
+ setTestResult(testResult);
320
+ }
321
+ };
322
+
323
+ // Async Method Tests (Promise)
324
+ const testGetVariantAsync = async () => {
325
+ if (!mixpanel) return;
326
+
327
+ const fallback = {key: 'fallback', value: JSON.parse(customFallback)};
328
+ const startTime = Date.now();
329
+
330
+ try {
331
+ const result = await mixpanel.flags.getVariant(selectedFlag, fallback);
332
+ const testResult = createTestResult(
333
+ `await getVariant('${selectedFlag}', fallback)`,
334
+ selectedFlag,
335
+ fallback,
336
+ result,
337
+ startTime,
338
+ );
339
+
340
+ setTestResult(testResult);
341
+ trackWithLog(Events.FLAG_TEST_ASYNC, {
342
+ [Properties.FLAG_METHOD]: 'getVariant',
343
+ [Properties.FLAG_KEY]: selectedFlag,
344
+ [Properties.FLAG_VALUE]: result.value,
345
+ [Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
346
+ });
347
+ } catch (error) {
348
+ const testResult = createTestResult(
349
+ `await getVariant('${selectedFlag}', fallback)`,
350
+ selectedFlag,
351
+ fallback,
352
+ fallback,
353
+ startTime,
354
+ error instanceof Error ? error.message : String(error),
355
+ );
356
+ setTestResult(testResult);
357
+ }
358
+ };
359
+
360
+ const testGetVariantValueAsync = async () => {
361
+ if (!mixpanel) return;
362
+
363
+ const fallback = JSON.parse(customFallback);
364
+ const startTime = Date.now();
365
+
366
+ try {
367
+ const result = await mixpanel.flags.getVariantValue(
368
+ selectedFlag,
369
+ fallback,
370
+ );
371
+ const testResult = createTestResult(
372
+ `await getVariantValue('${selectedFlag}', ${customFallback})`,
373
+ selectedFlag,
374
+ fallback,
375
+ result,
376
+ startTime,
377
+ );
378
+
379
+ setTestResult(testResult);
380
+ trackWithLog(Events.FLAG_TEST_ASYNC, {
381
+ [Properties.FLAG_METHOD]: 'getVariantValue',
382
+ [Properties.FLAG_KEY]: selectedFlag,
383
+ [Properties.FLAG_VALUE]: result,
384
+ [Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
385
+ });
386
+ } catch (error) {
387
+ const testResult = createTestResult(
388
+ `await getVariantValue('${selectedFlag}', ${customFallback})`,
389
+ selectedFlag,
390
+ fallback,
391
+ fallback,
392
+ startTime,
393
+ error instanceof Error ? error.message : String(error),
394
+ );
395
+ setTestResult(testResult);
396
+ }
397
+ };
398
+
399
+ const testIsEnabledAsync = async () => {
400
+ if (!mixpanel) return;
401
+
402
+ const fallback = false;
403
+ const startTime = Date.now();
404
+
405
+ try {
406
+ const result = await mixpanel.flags.isEnabled(selectedFlag, fallback);
407
+ const testResult = createTestResult(
408
+ `await isEnabled('${selectedFlag}', false)`,
409
+ selectedFlag,
410
+ fallback,
411
+ result,
412
+ startTime,
413
+ );
414
+
415
+ setTestResult(testResult);
416
+ trackWithLog(Events.FLAG_TEST_ASYNC, {
417
+ [Properties.FLAG_METHOD]: 'isEnabled',
418
+ [Properties.FLAG_KEY]: selectedFlag,
419
+ [Properties.FLAG_ENABLED]: result,
420
+ [Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
421
+ });
422
+ } catch (error) {
423
+ const testResult = createTestResult(
424
+ `await isEnabled('${selectedFlag}', false)`,
425
+ selectedFlag,
426
+ fallback,
427
+ fallback,
428
+ startTime,
429
+ error instanceof Error ? error.message : String(error),
430
+ );
431
+ setTestResult(testResult);
432
+ }
433
+ };
434
+
435
+ // Callback Pattern Test
436
+ const testGetVariantCallback = () => {
437
+ if (!mixpanel) return;
438
+
439
+ const fallback = {key: 'fallback', value: JSON.parse(customFallback)};
440
+ const startTime = Date.now();
441
+
442
+ mixpanel.flags.getVariant(selectedFlag, fallback, result => {
443
+ const testResult = createTestResult(
444
+ `getVariant('${selectedFlag}', fallback, callback)`,
445
+ selectedFlag,
446
+ fallback,
447
+ result,
448
+ startTime,
449
+ );
450
+
451
+ setTestResult(testResult);
452
+ trackWithLog(Events.FLAG_TEST_CALLBACK, {
453
+ [Properties.FLAG_METHOD]: 'getVariant (callback)',
454
+ [Properties.FLAG_KEY]: selectedFlag,
455
+ [Properties.FLAG_VALUE]: result.value,
456
+ [Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
457
+ });
458
+ });
459
+ };
460
+
461
+ // Edge Case Tests
462
+ const testNonExistentFlag = () => {
463
+ if (!mixpanel) return;
464
+
465
+ const fakeFlag = 'non-existent-flag-12345';
466
+ const fallback = {key: 'fallback', value: 'NOT_FOUND'};
467
+ const startTime = Date.now();
468
+
469
+ try {
470
+ const result = mixpanel.flags.getVariantSync(fakeFlag, fallback);
471
+ const testResult = createTestResult(
472
+ `getVariantSync('${fakeFlag}', fallback) [non-existent]`,
473
+ fakeFlag,
474
+ fallback,
475
+ result,
476
+ startTime,
477
+ );
478
+
479
+ setTestResult(testResult);
480
+ trackWithLog(Events.FLAG_TEST_EDGE_CASE, {
481
+ [Properties.FLAG_METHOD]: 'non-existent flag',
482
+ [Properties.FLAG_KEY]: fakeFlag,
483
+ [Properties.FLAG_USED_FALLBACK]: testResult.usedFallback,
484
+ });
485
+ } catch (error) {
486
+ const testResult = createTestResult(
487
+ `getVariantSync('${fakeFlag}', fallback) [non-existent]`,
488
+ fakeFlag,
489
+ fallback,
490
+ fallback,
491
+ startTime,
492
+ error instanceof Error ? error.message : String(error),
493
+ );
494
+ setTestResult(testResult);
495
+ }
496
+ };
497
+
498
+ const testTypeCoercion = (value: any, type: string) => {
499
+ if (!mixpanel) return;
500
+
501
+ // Create a mock flag scenario for type coercion testing
502
+ const fallback = value;
503
+ const startTime = Date.now();
504
+
505
+ try {
506
+ // For demo purposes, we'll use isEnabledSync with different values
507
+ const result = Boolean(value);
508
+ const testResult = createTestResult(
509
+ `Boolean(${JSON.stringify(value)}) [${type}]`,
510
+ 'type-coercion-test',
511
+ fallback,
512
+ result,
513
+ startTime,
514
+ );
515
+
516
+ setTestResult(testResult);
517
+ trackWithLog(Events.FLAG_TEST_COERCION, {
518
+ [Properties.FLAG_METHOD]: 'type coercion',
519
+ [Properties.FLAG_VALUE]: value,
520
+ [Properties.FLAG_RESULT_TYPE]: type,
521
+ [Properties.FLAG_ENABLED]: result,
522
+ });
523
+ } catch (error) {
524
+ const testResult = createTestResult(
525
+ `Boolean(${JSON.stringify(value)}) [${type}]`,
526
+ 'type-coercion-test',
527
+ fallback,
528
+ fallback,
529
+ startTime,
530
+ error instanceof Error ? error.message : String(error),
531
+ );
532
+ setTestResult(testResult);
533
+ }
534
+ };
535
+
536
+ return (
537
+ <ScrollView style={styles.container} contentContainerStyle={styles.content}>
538
+ <View style={styles.header}>
539
+ <Text style={styles.title}>Feature Flags Testing</Text>
540
+ <Text style={styles.subtitle}>
541
+ Integration test bed for comprehensive API coverage
542
+ </Text>
543
+ </View>
544
+
545
+ {/* Status Bar */}
546
+ <View style={styles.statusBar}>
547
+ <View style={styles.statusItem}>
548
+ <Text style={styles.statusLabel}>Status:</Text>
549
+ <Text style={[styles.statusValue, flagsReady && styles.statusReady]}>
550
+ {flagsReady ? '✅ Ready' : '⏳ Not Ready'}
551
+ </Text>
552
+ </View>
553
+ <View style={styles.statusItem}>
554
+ <Text style={styles.statusLabel}>Flags:</Text>
555
+ <Text style={styles.statusValue}>{Object.keys(allFlags).length}</Text>
556
+ </View>
557
+ </View>
558
+
559
+ {isLoadingFlags && (
560
+ <View style={styles.loadingContainer}>
561
+ <ActivityIndicator size="large" color="#007AFF" />
562
+ <Text style={styles.loadingText}>Loading feature flags...</Text>
563
+ </View>
564
+ )}
565
+
566
+ {/* Lifecycle Controls */}
567
+ <View style={styles.section}>
568
+ <Text style={styles.sectionTitle}>🔄 Lifecycle Controls</Text>
569
+ <View style={styles.buttonRow}>
570
+ <ActionButton
571
+ title={flagsReady ? 'Reload Flags' : 'Load Flags'}
572
+ onPress={handleLoadFlags}
573
+ disabled={!isInitialized || isLoadingFlags}
574
+ style={styles.halfButton}
575
+ />
576
+ <ActionButton
577
+ title="Refresh Display"
578
+ onPress={refreshAllFlags}
579
+ disabled={!flagsReady}
580
+ variant="secondary"
581
+ style={styles.halfButton}
582
+ />
583
+ </View>
584
+ </View>
585
+
586
+ {/* All Flags Display */}
587
+ {Object.keys(allFlags).length > 0 && (
588
+ <View style={styles.section}>
589
+ <Text style={styles.sectionTitle}>
590
+ 📋 All Flags ({Object.keys(allFlags).length})
591
+ </Text>
592
+ {Object.values(allFlags).map(flag => (
593
+ <FlagCard
594
+ key={flag.key}
595
+ flag={flag}
596
+ onTest={key => setSelectedFlag(key)}
597
+ />
598
+ ))}
599
+ </View>
600
+ )}
601
+
602
+ {/* Test Mode Tabs */}
603
+ {flagsReady && (
604
+ <View style={styles.section}>
605
+ <Text style={styles.sectionTitle}>🧪 Test Controls</Text>
606
+ <View style={styles.tabs}>
607
+ <TouchableOpacity
608
+ style={[styles.tab, testMode === 'sync' && styles.activeTab]}
609
+ onPress={() => setTestMode('sync')}>
610
+ <Text
611
+ style={[
612
+ styles.tabText,
613
+ testMode === 'sync' && styles.activeTabText,
614
+ ]}>
615
+ Sync
616
+ </Text>
617
+ </TouchableOpacity>
618
+ <TouchableOpacity
619
+ style={[styles.tab, testMode === 'async' && styles.activeTab]}
620
+ onPress={() => setTestMode('async')}>
621
+ <Text
622
+ style={[
623
+ styles.tabText,
624
+ testMode === 'async' && styles.activeTabText,
625
+ ]}>
626
+ Async
627
+ </Text>
628
+ </TouchableOpacity>
629
+ <TouchableOpacity
630
+ style={[styles.tab, testMode === 'edge' && styles.activeTab]}
631
+ onPress={() => setTestMode('edge')}>
632
+ <Text
633
+ style={[
634
+ styles.tabText,
635
+ testMode === 'edge' && styles.activeTabText,
636
+ ]}>
637
+ Edge Cases
638
+ </Text>
639
+ </TouchableOpacity>
640
+ <TouchableOpacity
641
+ style={[styles.tab, testMode === 'coercion' && styles.activeTab]}
642
+ onPress={() => setTestMode('coercion')}>
643
+ <Text
644
+ style={[
645
+ styles.tabText,
646
+ testMode === 'coercion' && styles.activeTabText,
647
+ ]}>
648
+ Coercion
649
+ </Text>
650
+ </TouchableOpacity>
651
+ </View>
652
+
653
+ {/* Flag Selector */}
654
+ <View style={styles.flagSelector}>
655
+ <Text style={styles.label}>Test Flag:</Text>
656
+ <View style={styles.flagButtons}>
657
+ {Object.keys(RECOMMENDED_FLAGS).map(flagKey => (
658
+ <TouchableOpacity
659
+ key={flagKey}
660
+ style={[
661
+ styles.flagButton,
662
+ selectedFlag === flagKey && styles.selectedFlagButton,
663
+ ]}
664
+ onPress={() => setSelectedFlag(flagKey)}>
665
+ <Text
666
+ style={[
667
+ styles.flagButtonText,
668
+ selectedFlag === flagKey && styles.selectedFlagButtonText,
669
+ ]}>
670
+ {flagKey}
671
+ </Text>
672
+ </TouchableOpacity>
673
+ ))}
674
+ </View>
675
+ </View>
676
+
677
+ {/* Sync Test Panel */}
678
+ {testMode === 'sync' && (
679
+ <View style={styles.testPanel}>
680
+ <Text style={styles.helper}>
681
+ Synchronous methods use cached values (fast, no network delay):
682
+ </Text>
683
+ <ActionButton
684
+ title="getVariantSync()"
685
+ onPress={testGetVariantSync}
686
+ style={styles.testButton}
687
+ />
688
+ <ActionButton
689
+ title="getVariantValueSync()"
690
+ onPress={testGetVariantValueSync}
691
+ variant="secondary"
692
+ style={styles.testButton}
693
+ />
694
+ <ActionButton
695
+ title="isEnabledSync()"
696
+ onPress={testIsEnabledSync}
697
+ variant="secondary"
698
+ style={styles.testButton}
699
+ />
700
+ <Text style={styles.helperSmall}>
701
+ 💡 Note: isEnabledSync() only returns true for boolean-valued flags (FeatureGates).
702
+ For string experiments, use getVariantValueSync() instead.
703
+ </Text>
704
+ </View>
705
+ )}
706
+
707
+ {/* Async Test Panel */}
708
+ {testMode === 'async' && (
709
+ <View style={styles.testPanel}>
710
+ <ActionButton
711
+ title="getVariant() - Promise"
712
+ onPress={testGetVariantAsync}
713
+ style={styles.testButton}
714
+ />
715
+ <ActionButton
716
+ title="getVariantValue() - Promise"
717
+ onPress={testGetVariantValueAsync}
718
+ variant="secondary"
719
+ style={styles.testButton}
720
+ />
721
+ <ActionButton
722
+ title="isEnabled() - Promise"
723
+ onPress={testIsEnabledAsync}
724
+ variant="secondary"
725
+ style={styles.testButton}
726
+ />
727
+ <ActionButton
728
+ title="getVariant() - Callback"
729
+ onPress={testGetVariantCallback}
730
+ variant="secondary"
731
+ style={styles.testButton}
732
+ />
733
+ </View>
734
+ )}
735
+
736
+ {/* Edge Case Test Panel */}
737
+ {testMode === 'edge' && (
738
+ <View style={styles.testPanel}>
739
+ <ActionButton
740
+ title="Test Non-Existent Flag"
741
+ onPress={testNonExistentFlag}
742
+ style={styles.testButton}
743
+ />
744
+ <Text style={styles.helper}>
745
+ Tests fallback behavior when flag doesn't exist
746
+ </Text>
747
+ </View>
748
+ )}
749
+
750
+ {/* Type Coercion Test Panel */}
751
+ {testMode === 'coercion' && (
752
+ <View style={styles.testPanel}>
753
+ <Text style={styles.helper}>
754
+ Tests Boolean() coercion for isEnabled():
755
+ </Text>
756
+ <View style={styles.coercionGrid}>
757
+ <ActionButton
758
+ title="0 → false"
759
+ onPress={() => testTypeCoercion(0, 'zero')}
760
+ variant="secondary"
761
+ style={styles.coercionButton}
762
+ />
763
+ <ActionButton
764
+ title="1 → true"
765
+ onPress={() => testTypeCoercion(1, 'one')}
766
+ variant="secondary"
767
+ style={styles.coercionButton}
768
+ />
769
+ <ActionButton
770
+ title='"" → false'
771
+ onPress={() => testTypeCoercion('', 'empty string')}
772
+ variant="secondary"
773
+ style={styles.coercionButton}
774
+ />
775
+ <ActionButton
776
+ title='"text" → true'
777
+ onPress={() => testTypeCoercion('text', 'string')}
778
+ variant="secondary"
779
+ style={styles.coercionButton}
780
+ />
781
+ <ActionButton
782
+ title="null → false"
783
+ onPress={() => testTypeCoercion(null, 'null')}
784
+ variant="secondary"
785
+ style={styles.coercionButton}
786
+ />
787
+ <ActionButton
788
+ title="{} → true"
789
+ onPress={() => testTypeCoercion({}, 'object')}
790
+ variant="secondary"
791
+ style={styles.coercionButton}
792
+ />
793
+ </View>
794
+ </View>
795
+ )}
796
+ </View>
797
+ )}
798
+
799
+ {/* Test Results */}
800
+ {testResult && (
801
+ <View style={styles.section}>
802
+ <Text style={styles.sectionTitle}>📊 Test Results</Text>
803
+ <TestResultDisplay result={testResult} />
804
+ </View>
805
+ )}
806
+
807
+ {/* Event Tracking Log */}
808
+ {trackedEvents.length > 0 && (
809
+ <View style={styles.section}>
810
+ <Text style={styles.sectionTitle}>
811
+ 📈 Recent Events ({trackedEvents.length})
812
+ </Text>
813
+ <EventTrackingLog events={trackedEvents} maxEvents={5} />
814
+ </View>
815
+ )}
816
+
817
+ {/* Info Card */}
818
+ <InfoCard
819
+ title="Test Flag Categories"
820
+ content={`This screen uses existing flags from your project:
821
+
822
+ BOOLEAN FLAGS (FeatureGate):
823
+ • sample-bool-flag, hash-slinging-slasher, mike-test
824
+ Variants: "On" (true) / "Off" (false)
825
+ Test with: isEnabled() or getVariantValue()
826
+
827
+ STRING EXPERIMENT FLAGS:
828
+ • sample-exp-testing, new_feature_flag_v2, mojojojo
829
+ Custom string variants (e.g., "control", "treatment")
830
+ Test with: getVariant() to see variant key
831
+
832
+ ACTIVE EXPERIMENTS (with experimentID):
833
+ • general-replay-events-query-improvement
834
+ • test-active-flag
835
+ Watch for $experiment_started events!
836
+
837
+ DYNAMIC CONFIG (Object values):
838
+ • matthew-dynamic-7 - Simple object
839
+ • mike-dynamic-config-4 - Complex nested object
840
+ Test with: getVariantValue() for JSON objects
841
+
842
+ Use the tabs to test different API methods (Sync/Async/Edge Cases/Coercion) with these flags.`}
843
+ style={styles.infoCard}
844
+ />
845
+ </ScrollView>
846
+ );
847
+ };
848
+
849
+ const styles = StyleSheet.create({
850
+ container: {
851
+ flex: 1,
852
+ backgroundColor: '#fff',
853
+ },
854
+ content: {
855
+ padding: 20,
856
+ },
857
+ header: {
858
+ marginBottom: 20,
859
+ },
860
+ title: {
861
+ fontSize: 24,
862
+ fontWeight: 'bold',
863
+ color: '#333',
864
+ marginBottom: 8,
865
+ },
866
+ subtitle: {
867
+ fontSize: 14,
868
+ color: '#666',
869
+ },
870
+ statusBar: {
871
+ flexDirection: 'row',
872
+ backgroundColor: '#f8f9fa',
873
+ padding: 12,
874
+ borderRadius: 8,
875
+ marginBottom: 20,
876
+ },
877
+ statusItem: {
878
+ flexDirection: 'row',
879
+ alignItems: 'center',
880
+ marginRight: 20,
881
+ },
882
+ statusLabel: {
883
+ fontSize: 13,
884
+ color: '#666',
885
+ marginRight: 6,
886
+ },
887
+ statusValue: {
888
+ fontSize: 13,
889
+ fontWeight: '600',
890
+ color: '#333',
891
+ },
892
+ statusReady: {
893
+ color: '#4caf50',
894
+ },
895
+ section: {
896
+ marginBottom: 24,
897
+ },
898
+ sectionTitle: {
899
+ fontSize: 18,
900
+ fontWeight: '600',
901
+ color: '#333',
902
+ marginBottom: 12,
903
+ },
904
+ buttonRow: {
905
+ flexDirection: 'row',
906
+ gap: 12,
907
+ },
908
+ halfButton: {
909
+ flex: 1,
910
+ },
911
+ loadingContainer: {
912
+ alignItems: 'center',
913
+ paddingVertical: 20,
914
+ marginBottom: 20,
915
+ },
916
+ loadingText: {
917
+ marginTop: 10,
918
+ fontSize: 14,
919
+ color: '#666',
920
+ },
921
+ tabs: {
922
+ flexDirection: 'row',
923
+ backgroundColor: '#f0f0f0',
924
+ borderRadius: 8,
925
+ padding: 4,
926
+ marginBottom: 16,
927
+ },
928
+ tab: {
929
+ flex: 1,
930
+ paddingVertical: 8,
931
+ alignItems: 'center',
932
+ borderRadius: 6,
933
+ },
934
+ activeTab: {
935
+ backgroundColor: '#007AFF',
936
+ },
937
+ tabText: {
938
+ fontSize: 14,
939
+ fontWeight: '500',
940
+ color: '#666',
941
+ },
942
+ activeTabText: {
943
+ color: '#fff',
944
+ },
945
+ flagSelector: {
946
+ marginBottom: 16,
947
+ },
948
+ label: {
949
+ fontSize: 14,
950
+ fontWeight: '600',
951
+ color: '#666',
952
+ marginBottom: 8,
953
+ },
954
+ flagButtons: {
955
+ flexDirection: 'row',
956
+ flexWrap: 'wrap',
957
+ gap: 8,
958
+ },
959
+ flagButton: {
960
+ paddingHorizontal: 12,
961
+ paddingVertical: 8,
962
+ backgroundColor: '#f0f0f0',
963
+ borderRadius: 6,
964
+ borderWidth: 1,
965
+ borderColor: '#e0e0e0',
966
+ },
967
+ selectedFlagButton: {
968
+ backgroundColor: '#007AFF',
969
+ borderColor: '#007AFF',
970
+ },
971
+ flagButtonText: {
972
+ fontSize: 12,
973
+ color: '#666',
974
+ },
975
+ selectedFlagButtonText: {
976
+ color: '#fff',
977
+ fontWeight: '600',
978
+ },
979
+ testPanel: {
980
+ marginTop: 12,
981
+ },
982
+ testButton: {
983
+ marginBottom: 10,
984
+ },
985
+ helper: {
986
+ fontSize: 13,
987
+ color: '#666',
988
+ marginBottom: 12,
989
+ lineHeight: 18,
990
+ },
991
+ helperSmall: {
992
+ fontSize: 12,
993
+ color: '#999',
994
+ marginTop: 12,
995
+ lineHeight: 16,
996
+ fontStyle: 'italic',
997
+ },
998
+ coercionGrid: {
999
+ flexDirection: 'row',
1000
+ flexWrap: 'wrap',
1001
+ gap: 8,
1002
+ marginTop: 8,
1003
+ },
1004
+ coercionButton: {
1005
+ flex: 0,
1006
+ minWidth: '47%',
1007
+ },
1008
+ infoCard: {
1009
+ marginTop: 10,
1010
+ },
1011
+ });