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.
- package/.claude/settings.local.json +12 -1
- package/.github/dependabot.yml +7 -0
- package/.github/workflows/node.js.yml +24 -1
- package/.vscode/settings.json +2 -1
- package/MixpanelReactNative.podspec +1 -1
- package/Samples/MixpanelExample/ios/MixpanelExample.xcworkspace/contents.xcworkspacedata +10 -0
- package/Samples/MixpanelExample/ios/Podfile.lock +1996 -0
- package/Samples/MixpanelStarter/.bundle/config +2 -0
- package/Samples/MixpanelStarter/.env.example +4 -0
- package/Samples/MixpanelStarter/.eslintrc.js +4 -0
- package/Samples/MixpanelStarter/.prettierrc.js +5 -0
- package/Samples/MixpanelStarter/.watchmanconfig +1 -0
- package/Samples/MixpanelStarter/App.tsx +10 -0
- package/Samples/MixpanelStarter/CLAUDE.md +538 -0
- package/Samples/MixpanelStarter/Gemfile +16 -0
- package/Samples/MixpanelStarter/INTEGRATION_GUIDE.md +606 -0
- package/Samples/MixpanelStarter/README.md +406 -0
- package/Samples/MixpanelStarter/__tests__/MixpanelContext.test.tsx +63 -0
- package/Samples/MixpanelStarter/android/app/build.gradle +119 -0
- package/Samples/MixpanelStarter/android/app/debug.keystore +0 -0
- package/Samples/MixpanelStarter/android/app/proguard-rules.pro +10 -0
- package/Samples/MixpanelStarter/android/app/src/main/AndroidManifest.xml +27 -0
- package/Samples/MixpanelStarter/android/app/src/main/java/com/mixpanelstarter/MainActivity.kt +22 -0
- package/Samples/MixpanelStarter/android/app/src/main/java/com/mixpanelstarter/MainApplication.kt +27 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/values/strings.xml +3 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/values/styles.xml +9 -0
- package/Samples/MixpanelStarter/android/build.gradle +21 -0
- package/Samples/MixpanelStarter/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/Samples/MixpanelStarter/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/Samples/MixpanelStarter/android/gradle.properties +44 -0
- package/Samples/MixpanelStarter/android/gradlew +251 -0
- package/Samples/MixpanelStarter/android/gradlew.bat +99 -0
- package/Samples/MixpanelStarter/android/settings.gradle +6 -0
- package/Samples/MixpanelStarter/app.json +4 -0
- package/Samples/MixpanelStarter/babel.config.js +14 -0
- package/Samples/MixpanelStarter/index.js +9 -0
- package/Samples/MixpanelStarter/ios/.xcode.env +11 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/AppDelegate.swift +48 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/Images.xcassets/Contents.json +6 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/Info.plist +55 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/LaunchScreen.storyboard +47 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/PrivacyInfo.xcprivacy +38 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter.xcodeproj/project.pbxproj +482 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter.xcodeproj/xcshareddata/xcschemes/MixpanelStarter.xcscheme +88 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter.xcworkspace/contents.xcworkspacedata +10 -0
- package/Samples/MixpanelStarter/ios/Podfile +34 -0
- package/Samples/MixpanelStarter/ios/Podfile.lock +2839 -0
- package/Samples/MixpanelStarter/jest.config.js +3 -0
- package/Samples/MixpanelStarter/metro.config.js +42 -0
- package/Samples/MixpanelStarter/package-lock.json +12141 -0
- package/Samples/MixpanelStarter/package.json +51 -0
- package/Samples/MixpanelStarter/src/@types/env.d.ts +3 -0
- package/Samples/MixpanelStarter/src/App.tsx +83 -0
- package/Samples/MixpanelStarter/src/components/ActionButton.tsx +92 -0
- package/Samples/MixpanelStarter/src/components/ErrorBoundary.tsx +81 -0
- package/Samples/MixpanelStarter/src/components/EventTrackingLog.tsx +163 -0
- package/Samples/MixpanelStarter/src/components/FlagCard.tsx +199 -0
- package/Samples/MixpanelStarter/src/components/InfoCard.tsx +77 -0
- package/Samples/MixpanelStarter/src/components/TestResultDisplay.tsx +181 -0
- package/Samples/MixpanelStarter/src/constants/tracking.ts +77 -0
- package/Samples/MixpanelStarter/src/contexts/MixpanelContext.tsx +159 -0
- package/Samples/MixpanelStarter/src/screens/FeatureFlagsScreen.tsx +1011 -0
- package/Samples/MixpanelStarter/src/screens/HomeScreen.tsx +307 -0
- package/Samples/MixpanelStarter/src/screens/OnboardingScreen.tsx +253 -0
- package/Samples/MixpanelStarter/src/screens/SettingsScreen.tsx +316 -0
- package/Samples/MixpanelStarter/src/types/flags.types.ts +42 -0
- package/Samples/MixpanelStarter/src/types/mixpanel.types.ts +26 -0
- package/Samples/MixpanelStarter/tsconfig.json +13 -0
- package/__tests__/flags.test.js +730 -0
- package/__tests__/index.test.js +7 -3
- package/__tests__/jest_setup.js +18 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/mixpanel/reactnative/MixpanelReactNativeModule.java +272 -2
- package/index.d.ts +64 -1
- package/index.js +42 -3
- package/ios/MixpanelReactNative.m +19 -1
- package/ios/MixpanelReactNative.swift +183 -5
- package/javascript/mixpanel-flags-js.js +463 -0
- package/javascript/mixpanel-flags.js +290 -0
- package/javascript/mixpanel-main.js +13 -1
- package/package.json +2 -2
package/__tests__/index.test.js
CHANGED
|
@@ -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
|
|
package/__tests__/jest_setup.js
CHANGED
|
@@ -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
|
},
|
package/android/build.gradle
CHANGED
|
@@ -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
|
-
|
|
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
|