diff --git a/CHANGELOG.md b/CHANGELOG.md index e77bfa912..099defc51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [Unreleased](https://github.com/Instabug/Instabug-Flutter/compare/v13.2.0...dev) + +### Added + +- Add support for variants in Feature Flags through the APIs `Instabug.addFeatureFlags`, `Instabug.removeFeatureFlags` and `Instabug.clearAllFeatureFlags` ([#471](https://github.com/Instabug/Instabug-Flutter/pull/471)). + +### Deprecated + +- Deprecate Experiments APIs `Instabug.addExperiments`, `Instabug.removeExperiments` and `Instabug.clearAllExperiments` in favor of the new Feature Flags APIs ([#471](https://github.com/Instabug/Instabug-Flutter/pull/471)). + ## [13.2.0](https://github.com/Instabug/Instabug-Flutter/compare/v13.1.1...v13.2.0) ### Added diff --git a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java index 12c0c72d6..74c4e4ba4 100644 --- a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java @@ -13,7 +13,14 @@ import com.instabug.flutter.util.ArgsRegistry; import com.instabug.flutter.util.Reflection; import com.instabug.flutter.util.ThreadManager; -import com.instabug.library.*; +import com.instabug.library.Feature; +import com.instabug.library.Instabug; +import com.instabug.library.InstabugColorTheme; +import com.instabug.library.InstabugCustomTextPlaceHolder; +import com.instabug.library.IssueType; +import com.instabug.library.Platform; +import com.instabug.library.ReproConfigurations; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.internal.module.InstabugLocale; import com.instabug.library.invocation.InstabugInvocationEvent; import com.instabug.library.model.NetworkLog; @@ -28,6 +35,7 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -225,6 +233,37 @@ public void clearAllExperiments() { Instabug.clearAllExperiments(); } + @Override + public void addFeatureFlags(@NonNull Map featureFlags) { + try { + List features = new ArrayList<>(); + for (Map.Entry entry : featureFlags.entrySet()) { + features.add(new IBGFeatureFlag(entry.getKey(), entry.getValue().isEmpty() ? null : entry.getValue())); + } + Instabug.addFeatureFlags(features); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void removeFeatureFlags(@NonNull List featureFlags) { + try { + Instabug.removeFeatureFlag(featureFlags); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void removeAllFeatureFlags() { + try { + Instabug.removeAllFeatureFlags(); + } catch (Exception e) { + e.printStackTrace(); + } + } + @Override public void setUserAttribute(@NonNull String value, @NonNull String key) { Instabug.setUserAttribute(key, value); diff --git a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index deb8d7b47..2abb8987e 100644 --- a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -35,6 +35,7 @@ import com.instabug.library.Platform; import com.instabug.library.ReproConfigurations; import com.instabug.library.ReproMode; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.invocation.InstabugInvocationEvent; import com.instabug.library.model.NetworkLog; import com.instabug.library.ui.onboarding.WelcomeMessage; @@ -48,6 +49,7 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -345,6 +347,32 @@ public void testClearAllExperiments() { mInstabug.verify(Instabug::clearAllExperiments); } + @Test + public void testAddFeatureFlags() { + Map featureFlags = new HashMap<>(); + featureFlags.put("key1","variant1"); + api.addFeatureFlags(featureFlags); + List flags=new ArrayList(); + flags.add(new IBGFeatureFlag("key1","variant1")); + mInstabug.verify(() -> Instabug.addFeatureFlags(flags)); + } + + @Test + public void testRemoveFeatureFlags() { + List featureFlags = Arrays.asList("premium", "star"); + + api.removeFeatureFlags(featureFlags); + + mInstabug.verify(() -> Instabug.removeFeatureFlag(featureFlags)); + } + + @Test + public void testClearAllFeatureFlags() { + api.removeAllFeatureFlags(); + + mInstabug.verify(Instabug::removeAllFeatureFlags); + } + @Test public void testSetUserAttribute() { String key = "is_premium"; diff --git a/example/ios/InstabugTests/InstabugApiTests.m b/example/ios/InstabugTests/InstabugApiTests.m index 9f9684120..886e22ece 100644 --- a/example/ios/InstabugTests/InstabugApiTests.m +++ b/example/ios/InstabugTests/InstabugApiTests.m @@ -224,6 +224,46 @@ - (void)testClearAllExperiments { OCMVerify([self.mInstabug clearAllExperiments]); } +- (void)testAddFeatureFlags { + NSDictionary *featureFlagsMap = @{ @"key13" : @"value1", @"key2" : @"value2"}; + FlutterError *error; + + [self.api addFeatureFlagsFeatureFlagsMap:featureFlagsMap error:&error]; + OCMVerify([self.mInstabug addFeatureFlags: [OCMArg checkWithBlock:^(id value) { + NSArray *featureFlags = value; + NSString* firstFeatureFlagName = [featureFlags objectAtIndex:0 ].name; + NSString* firstFeatureFlagKey = [[featureFlagsMap allKeys] objectAtIndex:0] ; + if([ firstFeatureFlagKey isEqualToString: firstFeatureFlagName]){ + return YES; + } + return NO; + }]]); +} + +- (void)testRemoveFeatureFlags { + NSArray *featureFlags = @[@"exp1"]; + FlutterError *error; + + [self.api removeFeatureFlagsFeatureFlags:featureFlags error:&error]; + OCMVerify([self.mInstabug removeFeatureFlags: [OCMArg checkWithBlock:^(id value) { + NSArray *featureFlagsObJ = value; + NSString* firstFeatureFlagName = [featureFlagsObJ objectAtIndex:0 ].name; + NSString* firstFeatureFlagKey = [featureFlags firstObject] ; + if([ firstFeatureFlagKey isEqualToString: firstFeatureFlagName]){ + return YES; + } + return NO; + }]]);} + +- (void)testRemoveAllFeatureFlags { + FlutterError *error; + + [self.api removeAllFeatureFlagsWithError:&error]; + OCMVerify([self.mInstabug removeAllFeatureFlags]); +} + + + - (void)testSetUserAttribute { NSString *key = @"is_premium"; NSString *value = @"true"; diff --git a/example/ios/Podfile b/example/ios/Podfile index 34e2dd968..cdffbc5db 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '13.4' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/lib/src/screens/my_home_page.dart b/example/lib/src/screens/my_home_page.dart index 283a09ecf..404d79cdd 100644 --- a/example/lib/src/screens/my_home_page.dart +++ b/example/lib/src/screens/my_home_page.dart @@ -19,6 +19,15 @@ class _MyHomePageState extends State { final primaryColorController = TextEditingController(); final screenNameController = TextEditingController(); + final featureFlagsController = TextEditingController(); + + @override + void dispose() { + featureFlagsController.dispose(); + screenNameController.dispose(); + primaryColorController.dispose(); + super.dispose(); + } void restartInstabug() { Instabug.setEnabled(false); @@ -325,7 +334,36 @@ class _MyHomePageState extends State { ), ], ), + SectionTitle('FeatureFlags'), + InstabugTextField( + controller: featureFlagsController, + label: 'Feature Flag name', + ), + InstabugButton( + onPressed: () => setFeatureFlag(), + text: 'SetFeatureFlag', + ), + InstabugButton( + onPressed: () => removeFeatureFlag(), + text: 'RemoveFeatureFlag', + ), + InstabugButton( + onPressed: () => removeAllFeatureFlags(), + text: 'RemoveAllFeatureFlags', + ), ], ); } + + setFeatureFlag() { + Instabug.addFeatureFlags([FeatureFlag(name: featureFlagsController.text)]); + } + + removeFeatureFlag() { + Instabug.removeFeatureFlags([featureFlagsController.text]); + } + + removeAllFeatureFlags() { + Instabug.clearAllFeatureFlags(); + } } diff --git a/ios/Classes/Modules/InstabugApi.m b/ios/Classes/Modules/InstabugApi.m index c6806c805..44f356685 100644 --- a/ios/Classes/Modules/InstabugApi.m +++ b/ios/Classes/Modules/InstabugApi.m @@ -318,5 +318,40 @@ - (void)willRedirectToStoreWithError:(FlutterError * _Nullable __autoreleasing * [Instabug willRedirectToAppStore]; } +- (void)addFeatureFlagsFeatureFlagsMap:(nonnull NSDictionary *)featureFlagsMap error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + NSMutableArray *featureFlags = [NSMutableArray array]; + for(id key in featureFlagsMap){ + NSString* variant =((NSString * )[featureFlagsMap objectForKey:key]); + if ([variant length]==0) { + [featureFlags addObject:[[IBGFeatureFlag alloc] initWithName:key]]; + } + else{ + [featureFlags addObject:[[IBGFeatureFlag alloc] initWithName:key variant:variant]]; + + } + } + [Instabug addFeatureFlags:featureFlags]; +} + + +- (void)removeAllFeatureFlagsWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + [Instabug removeAllFeatureFlags]; + +} + + +- (void)removeFeatureFlagsFeatureFlags:(nonnull NSArray *)featureFlags error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + + NSMutableArray *features = [NSMutableArray array]; + for(id item in featureFlags){ + [features addObject:[[IBGFeatureFlag alloc] initWithName:item]]; + } + @try { + [Instabug removeFeatureFlags:features]; + } @catch (NSException *exception) { + NSLog(@"%@", exception); + + } +} @end diff --git a/lib/instabug_flutter.dart b/lib/instabug_flutter.dart index cfd50ce30..783fb1350 100644 --- a/lib/instabug_flutter.dart +++ b/lib/instabug_flutter.dart @@ -1,6 +1,7 @@ // Models export 'src/models/crash_data.dart'; export 'src/models/exception_data.dart'; +export 'src/models/feature_flag.dart'; export 'src/models/network_data.dart'; export 'src/models/trace.dart'; // Modules diff --git a/lib/src/models/feature_flag.dart b/lib/src/models/feature_flag.dart new file mode 100644 index 000000000..2cd94fd67 --- /dev/null +++ b/lib/src/models/feature_flag.dart @@ -0,0 +1,9 @@ +class FeatureFlag { + /// the name of feature flag + String name; + + /// The variant of the feature flag. + String? variant; + + FeatureFlag({required this.name, this.variant}); +} diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index b0628f258..9d578e030 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -1,9 +1,11 @@ // ignore_for_file: avoid_classes_with_only_static_members import 'dart:async'; + // to maintain supported versions prior to Flutter 3.3 // ignore: unnecessary_import import 'dart:typed_data'; + // to maintain supported versions prior to Flutter 3.3 // ignore: unnecessary_import import 'dart:ui'; @@ -246,20 +248,50 @@ class Instabug { } /// Adds experiments to the next report. + @Deprecated( + 'Please migrate to the new feature flags APIs: Instabug.addFeatureFlags.', + ) static Future addExperiments(List experiments) async { return _host.addExperiments(experiments); } /// Removes certain experiments from the next report. + @Deprecated( + 'Please migrate to the new feature flags APIs: Instabug.removeFeatureFlags.', + ) static Future removeExperiments(List experiments) async { return _host.removeExperiments(experiments); } /// Clears all experiments from the next report. + + @Deprecated( + 'Please migrate to the new feature flags APIs: Instabug.clearAllFeatureFlags.', + ) static Future clearAllExperiments() async { return _host.clearAllExperiments(); } + /// Adds feature flags to the next report. + static Future addFeatureFlags(List featureFlags) async { + final map = {}; + for (final value in featureFlags) { + map[value.name] = value.variant ?? ''; + } + + return _host.addFeatureFlags(map); + } + + /// Removes certain feature flags from the next report. + static Future removeFeatureFlags(List featureFlags) async { + return _host.removeFeatureFlags(featureFlags); + } + + /// Clears all feature flags from the next report. + static Future clearAllFeatureFlags() async { + return _host.removeAllFeatureFlags(); + } + /// Add custom user attribute [value] with a [key] that is going to be sent with each feedback, bug or crash. static Future setUserAttribute(String value, String key) async { return _host.setUserAttribute(value, key); diff --git a/pigeons/instabug.api.dart b/pigeons/instabug.api.dart index 6da75bcaa..b839f8d1e 100644 --- a/pigeons/instabug.api.dart +++ b/pigeons/instabug.api.dart @@ -31,6 +31,9 @@ abstract class InstabugHostApi { void addExperiments(List experiments); void removeExperiments(List experiments); void clearAllExperiments(); + void addFeatureFlags(Map featureFlagsMap); + void removeFeatureFlags(List featureFlags); + void removeAllFeatureFlags(); void setUserAttribute(String value, String key); void removeUserAttribute(String key); diff --git a/test/instabug_test.dart b/test/instabug_test.dart index a6631859d..f525dc02c 100644 --- a/test/instabug_test.dart +++ b/test/instabug_test.dart @@ -239,6 +239,7 @@ void main() { test('[addExperiments] should call host method', () async { const experiments = ["exp-1", "exp-2"]; + // ignore: deprecated_member_use_from_same_package await Instabug.addExperiments(experiments); verify( @@ -249,6 +250,7 @@ void main() { test('[removeExperiments] should call host method', () async { const experiments = ["exp-1", "exp-2"]; + // ignore: deprecated_member_use_from_same_package await Instabug.removeExperiments(experiments); verify( @@ -257,6 +259,7 @@ void main() { }); test('[clearAllExperiments] should call host method', () async { + // ignore: deprecated_member_use_from_same_package await Instabug.clearAllExperiments(); verify( @@ -264,6 +267,38 @@ void main() { ).called(1); }); + test('[addFeatureFlags] should call host method', () async { + await Instabug.addFeatureFlags([ + FeatureFlag(name: 'name1', variant: 'variant1'), + FeatureFlag(name: 'name2', variant: 'variant2'), + ]); + + verify( + mHost.addFeatureFlags({ + "name1": "variant1", + "name2": "variant2", + }), + ).called(1); + }); + + test('[removeFeatureFlags] should call host method', () async { + const featureFlags = ["exp-1", "exp-2"]; + + await Instabug.removeFeatureFlags(featureFlags); + + verify( + mHost.removeFeatureFlags(featureFlags), + ).called(1); + }); + + test('[clearAllFeatureFlags] should call host method', () async { + await Instabug.clearAllFeatureFlags(); + + verify( + mHost.removeAllFeatureFlags(), + ).called(1); + }); + test('[setUserAttribute] should call host method', () async { const key = "attr-key"; const attribute = "User Attribute";