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
@@ -8,7 +8,9 @@ test(`it calls MixpanelReactNative initialize`, async () => {
8
8
  true,
9
9
  false,
10
10
  { $lib_version: expect.any(String), mp_lib: "react-native" },
11
- "https://api.mixpanel.com"
11
+ "https://api.mixpanel.com",
12
+ false,
13
+ {}
12
14
  );
13
15
  });
14
16
 
@@ -25,7 +27,8 @@ test(`it calls MixpanelReactNative initialize with optOut, superProperties and u
25
27
  super: "property",
26
28
  },
27
29
  "https://api.mixpanel.com",
28
- false
30
+ false,
31
+ {}
29
32
  );
30
33
  });
31
34
 
@@ -41,7 +44,8 @@ test(`it passes useGzipCompression parameter to native modules when enabled`, as
41
44
  mp_lib: "react-native",
42
45
  },
43
46
  "https://api.mixpanel.com",
44
- true
47
+ true,
48
+ {}
45
49
  );
46
50
  });
47
51
 
@@ -21,9 +21,16 @@ jest.mock("uuid", () => ({
21
21
  }));
22
22
 
23
23
  jest.mock("@react-native-async-storage/async-storage", () => ({
24
+ default: {
25
+ getItem: jest.fn().mockResolvedValue(null),
26
+ setItem: jest.fn().mockResolvedValue(undefined),
27
+ removeItem: jest.fn().mockResolvedValue(undefined),
28
+ clear: jest.fn().mockResolvedValue(undefined),
29
+ },
24
30
  getItem: jest.fn().mockResolvedValue(null),
25
31
  setItem: jest.fn().mockResolvedValue(undefined),
26
32
  removeItem: jest.fn().mockResolvedValue(undefined),
33
+ clear: jest.fn().mockResolvedValue(undefined),
27
34
  }));
28
35
 
29
36
  jest.doMock("react-native", () => {
@@ -85,6 +92,17 @@ jest.doMock("react-native", () => {
85
92
  groupUnsetProperty: jest.fn(),
86
93
  groupRemovePropertyValue: jest.fn(),
87
94
  groupUnionProperty: jest.fn(),
95
+ // Feature Flags native module mocks
96
+ loadFlags: jest.fn().mockResolvedValue(true),
97
+ areFlagsReadySync: jest.fn().mockReturnValue(false),
98
+ getVariantSync: jest.fn(),
99
+ getVariantValueSync: jest.fn(),
100
+ isEnabledSync: jest.fn(),
101
+ getVariant: jest.fn().mockResolvedValue({ key: 'control', value: 'default' }),
102
+ getVariantValue: jest.fn().mockResolvedValue('default'),
103
+ isEnabled: jest.fn().mockResolvedValue(false),
104
+ updateContext: jest.fn().mockResolvedValue(undefined), // Added for mixpanel-js alignment
105
+ updateFlagsContext: jest.fn().mockResolvedValue(true),
88
106
  },
89
107
  },
90
108
  },
@@ -41,5 +41,5 @@ repositories {
41
41
 
42
42
  dependencies {
43
43
  implementation 'com.facebook.react:react-native:+'
44
- implementation 'com.mixpanel.android:mixpanel-android:8.2.0'
44
+ implementation 'com.mixpanel.android:mixpanel-android:8.2.4'
45
45
  }
@@ -1,6 +1,9 @@
1
1
  package com.mixpanel.reactnative;
2
2
 
3
3
  import com.mixpanel.android.mpmetrics.MixpanelAPI;
4
+ import com.mixpanel.android.mpmetrics.MixpanelOptions;
5
+ import com.mixpanel.android.mpmetrics.MixpanelFlagVariant;
6
+ import com.mixpanel.android.mpmetrics.FlagCompletionCallback;
4
7
 
5
8
  import com.facebook.react.bridge.Promise;
6
9
  import com.facebook.react.bridge.ReactApplicationContext;
@@ -9,6 +12,10 @@ import com.facebook.react.bridge.ReactMethod;
9
12
  import com.facebook.react.bridge.ReadableArray;
10
13
  import com.facebook.react.bridge.ReadableMap;
11
14
  import com.facebook.react.bridge.Dynamic;
15
+ import com.facebook.react.bridge.WritableMap;
16
+ import com.facebook.react.bridge.WritableNativeMap;
17
+ import com.facebook.react.bridge.WritableArray;
18
+ import com.facebook.react.bridge.Callback;
12
19
 
13
20
  import org.json.JSONArray;
14
21
  import org.json.JSONException;
@@ -33,10 +40,35 @@ public class MixpanelReactNativeModule extends ReactContextBaseJavaModule {
33
40
 
34
41
 
35
42
  @ReactMethod
36
- public void initialize(String token, boolean trackAutomaticEvents, boolean optOutTrackingDefault, ReadableMap metadata, String serverURL, boolean useGzipCompression, Promise promise) throws JSONException {
43
+ public void initialize(String token, boolean trackAutomaticEvents, boolean optOutTrackingDefault, ReadableMap metadata, String serverURL, boolean useGzipCompression, ReadableMap featureFlagsOptions, Promise promise) throws JSONException {
37
44
  JSONObject mixpanelProperties = ReactNativeHelper.reactToJSON(metadata);
38
45
  AutomaticProperties.setAutomaticProperties(mixpanelProperties);
39
- MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, optOutTrackingDefault, mixpanelProperties, null, trackAutomaticEvents);
46
+
47
+ // Handle feature flags options
48
+ boolean featureFlagsEnabled = false;
49
+ JSONObject featureFlagsContext = null;
50
+
51
+ if (featureFlagsOptions != null && featureFlagsOptions.hasKey("enabled")) {
52
+ featureFlagsEnabled = featureFlagsOptions.getBoolean("enabled");
53
+
54
+ if (featureFlagsOptions.hasKey("context")) {
55
+ featureFlagsContext = ReactNativeHelper.reactToJSON(featureFlagsOptions.getMap("context"));
56
+ }
57
+ }
58
+
59
+ // Create Mixpanel instance with feature flags configuration
60
+ MixpanelOptions.Builder optionsBuilder = new MixpanelOptions.Builder()
61
+ .optOutTrackingDefault(optOutTrackingDefault)
62
+ .superProperties(mixpanelProperties)
63
+ .featureFlagsEnabled(featureFlagsEnabled);
64
+
65
+ if (featureFlagsContext != null) {
66
+ optionsBuilder.featureFlagsContext(featureFlagsContext);
67
+ }
68
+
69
+ MixpanelOptions options = optionsBuilder.build();
70
+
71
+ MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, trackAutomaticEvents, options);
40
72
  instance.setServerURL(serverURL);
41
73
  if (useGzipCompression) {
42
74
  instance.setShouldGzipRequestPayload(true);
@@ -602,4 +634,242 @@ public class MixpanelReactNativeModule extends ReactContextBaseJavaModule {
602
634
  promise.resolve(null);
603
635
  }
604
636
  }
637
+
638
+ // Feature Flags Methods
639
+
640
+ @ReactMethod
641
+ public void loadFlags(final String token, Promise promise) {
642
+ MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, true);
643
+ if (instance == null) {
644
+ promise.reject("Instance Error", "Failed to get Mixpanel instance");
645
+ return;
646
+ }
647
+ synchronized (instance) {
648
+ instance.getFlags().loadFlags();
649
+ promise.resolve(null);
650
+ }
651
+ }
652
+
653
+ @ReactMethod(isBlockingSynchronousMethod = true)
654
+ public boolean areFlagsReadySync(final String token) {
655
+ MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, true);
656
+ if (instance == null) {
657
+ return false;
658
+ }
659
+ synchronized (instance) {
660
+ return instance.getFlags().areFlagsReady();
661
+ }
662
+ }
663
+
664
+ @ReactMethod(isBlockingSynchronousMethod = true)
665
+ public WritableMap getVariantSync(final String token, String featureName, ReadableMap fallback) {
666
+ MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, true);
667
+ if (instance == null) {
668
+ return convertVariantToMap(fallback);
669
+ }
670
+
671
+ synchronized (instance) {
672
+ MixpanelFlagVariant fallbackVariant = convertMapToVariant(fallback);
673
+ MixpanelFlagVariant variant = instance.getFlags().getVariantSync(featureName, fallbackVariant);
674
+ return convertVariantToWritableMap(variant);
675
+ }
676
+ }
677
+
678
+ // Note: For getVariantValueSync, we'll return the full variant and extract value in JS
679
+ // React Native doesn't support returning Dynamic types from synchronous methods
680
+ @ReactMethod(isBlockingSynchronousMethod = true)
681
+ public WritableMap getVariantValueSync(final String token, String featureName, Dynamic fallbackValue) {
682
+ MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, true);
683
+
684
+ WritableMap result = new WritableNativeMap();
685
+ if (instance == null) {
686
+ result.putString("type", "fallback");
687
+ // We'll handle the conversion in JavaScript
688
+ return result;
689
+ }
690
+
691
+ synchronized (instance) {
692
+ Object value = instance.getFlags().getVariantValueSync(featureName, ReactNativeHelper.dynamicToObject(fallbackValue));
693
+ result.putString("type", "value");
694
+
695
+ // Convert value to appropriate type
696
+ if (value == null) {
697
+ result.putNull("value");
698
+ } else if (value instanceof String) {
699
+ result.putString("value", (String) value);
700
+ } else if (value instanceof Boolean) {
701
+ result.putBoolean("value", (Boolean) value);
702
+ } else if (value instanceof Integer) {
703
+ result.putInt("value", (Integer) value);
704
+ } else if (value instanceof Double) {
705
+ result.putDouble("value", (Double) value);
706
+ } else if (value instanceof Float) {
707
+ result.putDouble("value", ((Float) value).doubleValue());
708
+ } else if (value instanceof Long) {
709
+ result.putDouble("value", ((Long) value).doubleValue());
710
+ } else {
711
+ result.putString("value", value.toString());
712
+ }
713
+
714
+ return result;
715
+ }
716
+ }
717
+
718
+ @ReactMethod(isBlockingSynchronousMethod = true)
719
+ public boolean isEnabledSync(final String token, String featureName, boolean fallbackValue) {
720
+ MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, true);
721
+ if (instance == null) {
722
+ return fallbackValue;
723
+ }
724
+
725
+ synchronized (instance) {
726
+ return instance.getFlags().isEnabledSync(featureName, fallbackValue);
727
+ }
728
+ }
729
+
730
+ @ReactMethod
731
+ public void getVariant(final String token, String featureName, ReadableMap fallback, final Promise promise) {
732
+ MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, true);
733
+ if (instance == null) {
734
+ promise.resolve(convertVariantToMap(fallback));
735
+ return;
736
+ }
737
+
738
+ synchronized (instance) {
739
+ MixpanelFlagVariant fallbackVariant = convertMapToVariant(fallback);
740
+ instance.getFlags().getVariant(featureName, fallbackVariant, new FlagCompletionCallback<MixpanelFlagVariant>() {
741
+ @Override
742
+ public void onComplete(MixpanelFlagVariant variant) {
743
+ promise.resolve(convertVariantToWritableMap(variant));
744
+ }
745
+ });
746
+ }
747
+ }
748
+
749
+ @ReactMethod
750
+ public void getVariantValue(final String token, String featureName, Dynamic fallbackValue, final Promise promise) {
751
+ MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, true);
752
+ if (instance == null) {
753
+ promise.resolve(fallbackValue);
754
+ return;
755
+ }
756
+
757
+ synchronized (instance) {
758
+ Object fallbackObj = ReactNativeHelper.dynamicToObject(fallbackValue);
759
+ instance.getFlags().getVariantValue(featureName, fallbackObj, new FlagCompletionCallback<Object>() {
760
+ @Override
761
+ public void onComplete(Object value) {
762
+ // Convert the value back to a format React Native can handle
763
+ if (value == null) {
764
+ promise.resolve(null);
765
+ } else if (value instanceof String) {
766
+ promise.resolve((String) value);
767
+ } else if (value instanceof Boolean) {
768
+ promise.resolve((Boolean) value);
769
+ } else if (value instanceof Number) {
770
+ promise.resolve(((Number) value).doubleValue());
771
+ } else if (value instanceof JSONObject) {
772
+ try {
773
+ WritableMap map = ReactNativeHelper.convertJsonToMap((JSONObject) value);
774
+ promise.resolve(map);
775
+ } catch (Exception e) {
776
+ promise.resolve(value.toString());
777
+ }
778
+ } else if (value instanceof JSONArray) {
779
+ try {
780
+ WritableArray array = ReactNativeHelper.convertJsonToArray((JSONArray) value);
781
+ promise.resolve(array);
782
+ } catch (Exception e) {
783
+ promise.resolve(value.toString());
784
+ }
785
+ } else {
786
+ promise.resolve(value.toString());
787
+ }
788
+ }
789
+ });
790
+ }
791
+ }
792
+
793
+ @ReactMethod
794
+ public void isEnabled(final String token, String featureName, boolean fallbackValue, final Promise promise) {
795
+ MixpanelAPI instance = MixpanelAPI.getInstance(this.mReactContext, token, true);
796
+ if (instance == null) {
797
+ promise.resolve(fallbackValue);
798
+ return;
799
+ }
800
+
801
+ synchronized (instance) {
802
+ instance.getFlags().isEnabled(featureName, fallbackValue, new FlagCompletionCallback<Boolean>() {
803
+ @Override
804
+ public void onComplete(Boolean isEnabled) {
805
+ promise.resolve(isEnabled);
806
+ }
807
+ });
808
+ }
809
+ }
810
+
811
+ // Helper methods for variant conversion
812
+ private MixpanelFlagVariant convertMapToVariant(ReadableMap map) {
813
+ if (map == null) {
814
+ return new MixpanelFlagVariant("", null);
815
+ }
816
+
817
+ String key = map.hasKey("key") ? map.getString("key") : "";
818
+ Object value = map.hasKey("value") ? ReactNativeHelper.dynamicToObject(map.getDynamic("value")) : null;
819
+ String experimentID = map.hasKey("experimentID") ? map.getString("experimentID") : null;
820
+ Boolean isExperimentActive = map.hasKey("isExperimentActive") ? map.getBoolean("isExperimentActive") : null;
821
+ Boolean isQATester = map.hasKey("isQATester") ? map.getBoolean("isQATester") : null;
822
+
823
+ // Create variant with all properties using the full constructor
824
+ return new MixpanelFlagVariant(key, value, experimentID, isExperimentActive, isQATester);
825
+ }
826
+
827
+ private WritableMap convertVariantToMap(ReadableMap source) {
828
+ WritableMap map = new WritableNativeMap();
829
+ if (source != null) {
830
+ map.merge(source);
831
+ }
832
+ return map;
833
+ }
834
+
835
+ private WritableMap convertVariantToWritableMap(MixpanelFlagVariant variant) {
836
+ WritableMap map = new WritableNativeMap();
837
+
838
+ if (variant != null) {
839
+ map.putString("key", variant.key);
840
+
841
+ Object value = variant.value;
842
+ if (value == null) {
843
+ map.putNull("value");
844
+ } else if (value instanceof String) {
845
+ map.putString("value", (String) value);
846
+ } else if (value instanceof Boolean) {
847
+ map.putBoolean("value", (Boolean) value);
848
+ } else if (value instanceof Integer) {
849
+ map.putInt("value", (Integer) value);
850
+ } else if (value instanceof Double) {
851
+ map.putDouble("value", (Double) value);
852
+ } else if (value instanceof Float) {
853
+ map.putDouble("value", ((Float) value).doubleValue());
854
+ } else if (value instanceof Long) {
855
+ map.putDouble("value", ((Long) value).doubleValue());
856
+ } else {
857
+ // For complex objects, convert to string
858
+ map.putString("value", value.toString());
859
+ }
860
+
861
+ // Add optional fields if they exist
862
+ if (variant.experimentID != null) {
863
+ map.putString("experimentID", variant.experimentID);
864
+ }
865
+ if (variant.isExperimentActive != null) {
866
+ map.putBoolean("isExperimentActive", variant.isExperimentActive);
867
+ }
868
+ if (variant.isQATester != null) {
869
+ map.putBoolean("isQATester", variant.isQATester);
870
+ }
871
+ }
872
+
873
+ return map;
874
+ }
605
875
  }
package/index.d.ts CHANGED
@@ -7,7 +7,69 @@ export type MixpanelAsyncStorage = {
7
7
  removeItem(key: string): Promise<void>;
8
8
  };
9
9
 
10
+ export interface MixpanelFlagVariant {
11
+ key: string;
12
+ value: any;
13
+ experiment_id?: string | number; // Updated to match mixpanel-js format
14
+ is_experiment_active?: boolean; // Updated to match mixpanel-js format
15
+ is_qa_tester?: boolean; // Updated to match mixpanel-js format
16
+ }
17
+
18
+ export interface FeatureFlagsOptions {
19
+ enabled?: boolean;
20
+ context?: {
21
+ [key: string]: any;
22
+ custom_properties?: {
23
+ [key: string]: any;
24
+ };
25
+ };
26
+ }
27
+
28
+ export interface UpdateContextOptions {
29
+ replace?: boolean;
30
+ }
31
+
32
+ export interface Flags {
33
+ // Synchronous methods
34
+ loadFlags(): Promise<void>;
35
+ areFlagsReady(): boolean;
36
+ getVariantSync(featureName: string, fallback: MixpanelFlagVariant): MixpanelFlagVariant;
37
+ getVariantValueSync(featureName: string, fallbackValue: any): any;
38
+ isEnabledSync(featureName: string, fallbackValue?: boolean): boolean;
39
+
40
+ // Asynchronous methods with overloads for callback and Promise patterns
41
+ getVariant(featureName: string, fallback: MixpanelFlagVariant): Promise<MixpanelFlagVariant>;
42
+ getVariant(featureName: string, fallback: MixpanelFlagVariant, callback: (result: MixpanelFlagVariant) => void): void;
43
+
44
+ getVariantValue(featureName: string, fallbackValue: any): Promise<any>;
45
+ getVariantValue(featureName: string, fallbackValue: any, callback: (value: any) => void): void;
46
+
47
+ isEnabled(featureName: string, fallbackValue?: boolean): Promise<boolean>;
48
+ isEnabled(featureName: string, fallbackValue: boolean, callback: (isEnabled: boolean) => void): void;
49
+
50
+ // Context management (NEW - aligned with mixpanel-js)
51
+ // NOTE: Only available in JavaScript mode (Expo/React Native Web)
52
+ // In native mode, throws an error - context must be set during initialization
53
+ updateContext(newContext: MixpanelProperties, options?: UpdateContextOptions): Promise<void>;
54
+
55
+ // snake_case aliases (NEW - aligned with mixpanel-js)
56
+ are_flags_ready(): boolean;
57
+ get_variant(featureName: string, fallback: MixpanelFlagVariant): Promise<MixpanelFlagVariant>;
58
+ get_variant(featureName: string, fallback: MixpanelFlagVariant, callback: (result: MixpanelFlagVariant) => void): void;
59
+ get_variant_sync(featureName: string, fallback: MixpanelFlagVariant): MixpanelFlagVariant;
60
+ get_variant_value(featureName: string, fallbackValue: any): Promise<any>;
61
+ get_variant_value(featureName: string, fallbackValue: any, callback: (value: any) => void): void;
62
+ get_variant_value_sync(featureName: string, fallbackValue: any): any;
63
+ is_enabled(featureName: string, fallbackValue?: boolean): Promise<boolean>;
64
+ is_enabled(featureName: string, fallbackValue: boolean, callback: (isEnabled: boolean) => void): void;
65
+ is_enabled_sync(featureName: string, fallbackValue?: boolean): boolean;
66
+ // NOTE: Only available in JavaScript mode (Expo/React Native Web)
67
+ update_context(newContext: MixpanelProperties, options?: UpdateContextOptions): Promise<void>;
68
+ }
69
+
10
70
  export class Mixpanel {
71
+ readonly flags: Flags;
72
+
11
73
  constructor(token: string, trackAutoMaticEvents: boolean);
12
74
  constructor(token: string, trackAutoMaticEvents: boolean, useNative: true);
13
75
  constructor(
@@ -25,7 +87,8 @@ export class Mixpanel {
25
87
  optOutTrackingDefault?: boolean,
26
88
  superProperties?: MixpanelProperties,
27
89
  serverURL?: string,
28
- useGzipCompression?: boolean
90
+ useGzipCompression?: boolean,
91
+ featureFlagsOptions?: FeatureFlagsOptions
29
92
  ): Promise<void>;
30
93
  setServerURL(serverURL: string): void;
31
94
  setLoggingEnabled(loggingEnabled: boolean): void;
package/index.js CHANGED
@@ -46,6 +46,8 @@ export class Mixpanel {
46
46
  }
47
47
  this.token = token;
48
48
  this.trackAutomaticEvents = trackAutomaticEvents;
49
+ this._flags = null; // Lazy-loaded flags instance
50
+ this.storage = storage; // Store for JavaScript mode
49
51
 
50
52
  if (useNative && MixpanelReactNative) {
51
53
  this.mixpanelImpl = MixpanelReactNative;
@@ -59,6 +61,30 @@ export class Mixpanel {
59
61
  this.mixpanelImpl = new MixpanelMain(token, trackAutomaticEvents, storage);
60
62
  }
61
63
 
64
+ /**
65
+ * Returns the Flags instance for feature flags operations.
66
+ * This property is lazy-loaded to avoid unnecessary initialization.
67
+ *
68
+ * NOTE: Feature flags are only available in native mode.
69
+ * JavaScript mode is not yet supported.
70
+ */
71
+ get flags() {
72
+ // Short circuit for JavaScript mode - flags not ready for public use
73
+ if (this.mixpanelImpl !== MixpanelReactNative) {
74
+ throw new Error(
75
+ "Feature flags are only available in native mode. " +
76
+ "JavaScript mode support is coming in a future release."
77
+ );
78
+ }
79
+
80
+ if (!this._flags) {
81
+ // Lazy load the Flags instance with proper dependencies
82
+ const Flags = require("./javascript/mixpanel-flags").Flags;
83
+ this._flags = new Flags(this.token, this.mixpanelImpl, this.storage);
84
+ }
85
+ return this._flags;
86
+ }
87
+
62
88
  /**
63
89
  * Initializes Mixpanel
64
90
  *
@@ -66,21 +92,32 @@ export class Mixpanel {
66
92
  * @param {object} superProperties Optional A Map containing the key value pairs of the super properties to register
67
93
  * @param {string} serverURL Optional Set the base URL used for Mixpanel API requests. See setServerURL()
68
94
  * @param {boolean} useGzipCompression Optional Set whether to use gzip compression for network requests. Defaults to false.
95
+ * @param {object} featureFlagsOptions Optional Feature flags configuration including enabled flag and context
69
96
  */
70
97
  async init(
71
98
  optOutTrackingDefault = DEFAULT_OPT_OUT,
72
99
  superProperties = {},
73
100
  serverURL = "https://api.mixpanel.com",
74
- useGzipCompression = false
101
+ useGzipCompression = false,
102
+ featureFlagsOptions = {}
75
103
  ) {
104
+ // Store feature flags options for later use
105
+ this.featureFlagsOptions = featureFlagsOptions;
106
+
76
107
  await this.mixpanelImpl.initialize(
77
108
  this.token,
78
109
  this.trackAutomaticEvents,
79
110
  optOutTrackingDefault,
80
111
  {...Helper.getMetaData(), ...superProperties},
81
112
  serverURL,
82
- useGzipCompression
113
+ useGzipCompression,
114
+ featureFlagsOptions
83
115
  );
116
+
117
+ // If flags are enabled AND we're in native mode, initialize them
118
+ if (featureFlagsOptions.enabled && this.mixpanelImpl === MixpanelReactNative) {
119
+ await this.flags.loadFlags();
120
+ }
84
121
  }
85
122
 
86
123
  /**
@@ -109,7 +146,9 @@ export class Mixpanel {
109
146
  trackAutomaticEvents,
110
147
  optOutTrackingDefault,
111
148
  Helper.getMetaData(),
112
- "https://api.mixpanel.com"
149
+ "https://api.mixpanel.com",
150
+ false,
151
+ {}
113
152
  );
114
153
  return new Mixpanel(token, trackAutomaticEvents);
115
154
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  // MARK: - Mixpanel Instance
7
7
 
8
- RCT_EXTERN_METHOD(initialize:(NSString *)token trackAutomaticEvents:(BOOL)trackAutomaticEvents optOutTrackingByDefault:(BOOL)optOutTrackingByDefault properties:(NSDictionary *)properties serverURL:(NSString *)serverURL useGzipCompression:(BOOL)useGzipCompression resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
8
+ RCT_EXTERN_METHOD(initialize:(NSString *)token trackAutomaticEvents:(BOOL)trackAutomaticEvents optOutTrackingByDefault:(BOOL)optOutTrackingByDefault properties:(NSDictionary *)properties serverURL:(NSString *)serverURL useGzipCompression:(BOOL)useGzipCompression featureFlagsOptions:(NSDictionary *)featureFlagsOptions resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
9
9
 
10
10
  // Mark: - Settings
11
11
  RCT_EXTERN_METHOD(setServerURL:(NSString *)token serverURL:(NSString *)serverURL resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
@@ -105,4 +105,22 @@ RCT_EXTERN_METHOD(groupRemovePropertyValue:(NSString *)token groupKey:(NSString
105
105
 
106
106
  RCT_EXTERN_METHOD(groupUnionProperty:(NSString *)token groupKey:(NSString *)groupKey groupID:(id)groupID name:(NSString *)name values:(NSArray *)values resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
107
107
 
108
+ // MARK: - Feature Flags
109
+
110
+ RCT_EXTERN_METHOD(loadFlags:(NSString *)token resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
111
+
112
+ RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(areFlagsReadySync:(NSString *)token)
113
+
114
+ RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(getVariantSync:(NSString *)token featureName:(NSString *)featureName fallback:(NSDictionary *)fallback)
115
+
116
+ RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(getVariantValueSync:(NSString *)token featureName:(NSString *)featureName fallbackValue:(id)fallbackValue)
117
+
118
+ RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(isEnabledSync:(NSString *)token featureName:(NSString *)featureName fallbackValue:(BOOL)fallbackValue)
119
+
120
+ RCT_EXTERN_METHOD(getVariant:(NSString *)token featureName:(NSString *)featureName fallback:(NSDictionary *)fallback resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
121
+
122
+ RCT_EXTERN_METHOD(getVariantValue:(NSString *)token featureName:(NSString *)featureName fallbackValue:(id)fallbackValue resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
123
+
124
+ RCT_EXTERN_METHOD(isEnabled:(NSString *)token featureName:(NSString *)featureName fallbackValue:(BOOL)fallbackValue resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
125
+
108
126
  @end