diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..9e31981 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @splitio/sdk diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08b883c..fa2869b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,53 +4,66 @@ on: branches: - main - development + - '**_baseline' jobs: build-android: name: Build Android runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3.5.2 + - uses: actions/setup-java@v3.11.0 with: distribution: 'temurin' java-version: '11' - - uses: subosito/flutter-action@v2 + - uses: subosito/flutter-action@v2.10.0 with: channel: 'stable' - name: Build example app APK - run: cd example; flutter build apk + run: cd splitio/example; flutter build apk test-android: name: Test Android runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3.5.2 + - uses: actions/setup-java@v3.11.0 with: distribution: 'temurin' java-version: '11' - - uses: subosito/flutter-action@v2 + - uses: subosito/flutter-action@v2.10.0 with: channel: 'stable' - name: Run Pub Get - run: flutter pub get + run: cd splitio/; flutter pub get - name: Build Android - run: flutter build + run: cd splitio/example/android/; flutter build apk - name: Run Android test - run: cd example/android/; gradle :splitio:testReleaseUnitTest; + run: cd splitio/example/android/; ./gradlew :splitio:testReleaseUnitTest; + + build-ios: + name: Build iOS + runs-on: [ macos-latest ] + + steps: + - uses: actions/checkout@v3.5.2 + - uses: subosito/flutter-action@v2.10.0 + with: + channel: 'stable' + - name: Run Build iOS + run: cd splitio/example/ios; flutter build ios --no-codesign; test-ios: name: Test iOS runs-on: [ macos-latest ] steps: - - uses: actions/checkout@v3 - - uses: subosito/flutter-action@v2 + - uses: actions/checkout@v3.5.2 + - uses: subosito/flutter-action@v2.10.0 with: channel: 'stable' - name: Run Build iOS - run: cd example/ios/; flutter build ios --no-codesign; + run: cd splitio_ios/example/; flutter build ios --no-codesign; - name: Select Xcode uses: maxim-lobanov/setup-xcode@v1 @@ -58,4 +71,4 @@ jobs: xcode-version: 13.2.1 - name: ios - run: cd example/ios/; xcodebuild test -workspace "Runner.xcworkspace" -scheme "Runner" -destination "platform=iOS Simulator,name=iPhone 12,OS=latest" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO; + run: cd splitio_ios/example/ios; xcodebuild test -workspace "Runner.xcworkspace" -scheme "Runner" -destination "platform=iOS Simulator,name=iPhone 12,OS=latest" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d7f7198..a503a2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,6 @@ jobs: with: channel: 'stable' - name: Run Pub Get - run: flutter pub get + run: cd splitio/; flutter pub get - name: Run flutter test - run: flutter test + run: cd splitio/; flutter test diff --git a/.gitignore b/.gitignore index 9be145f..c95f7e3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,16 +14,3 @@ *.ipr *.iws .idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.packages -build/ diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 80fba2b..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,12 +0,0 @@ -# 0.1.1 - -Minor fixes. - -## 0.1.0 - -Initial release. - -* Added support for SDK instantiation. -* Added support for multiple clients. -* Added support for evaluation. -* Added support for tracking events. diff --git a/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java b/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java deleted file mode 100644 index 7add0b9..0000000 --- a/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.split.splitio; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - -import org.junit.Test; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import io.split.android.client.SplitClientConfig; -import io.split.android.client.SplitFilter; -import io.split.android.client.impressions.ImpressionListener; - -public class SplitClientConfigHelperTest { - - @Test - public void configValuesAreMappedCorrectly() { - Map configValues = new HashMap<>(); - - configValues.put("featuresRefreshRate", 80000); - configValues.put("segmentsRefreshRate", 70000); - configValues.put("impressionsRefreshRate", 60000); - configValues.put("telemetryRefreshRate", 2000); - configValues.put("eventsQueueSize", 3999); - configValues.put("impressionsQueueSize", 2999); - configValues.put("eventFlushInterval", 40000); - configValues.put("eventsPerPush", 5000); - configValues.put("trafficType", "none"); - configValues.put("enableDebug", true); - configValues.put("streamingEnabled", true); - configValues.put("persistentAttributesEnabled", true); - configValues.put("sdkEndpoint", "apiEndpoint.split.io"); - configValues.put("eventsEndpoint", "eventsEndpoint.split.io"); - configValues.put("authServiceEndpoint", "sseAuthServiceEndpoint.split.io"); - configValues.put("streamingServiceEndpoint", "streamingServiceEndpoint.split.io"); - configValues.put("telemetryServiceEndpoint", "telemetryServiceEndpoint.split.io"); - Map> syncConfigMap = new HashMap<>(); - syncConfigMap.put("syncConfigNames", Arrays.asList("split1", "split2")); - syncConfigMap.put("syncConfigPrefixes", Arrays.asList("split_", "my_split_")); - configValues.put("syncConfig", syncConfigMap); - - SplitClientConfig splitClientConfig = SplitClientConfigHelper.fromMap(configValues, mock(ImpressionListener.class)); - - assertEquals(80000, splitClientConfig.featuresRefreshRate()); - assertEquals(70000, splitClientConfig.segmentsRefreshRate()); - assertEquals(60000, splitClientConfig.impressionsRefreshRate()); - assertEquals(2000, splitClientConfig.telemetryRefreshRate()); - assertEquals(3999, splitClientConfig.eventsQueueSize()); - assertEquals(2999, splitClientConfig.impressionsQueueSize()); - assertEquals(40000, splitClientConfig.eventFlushInterval()); - assertEquals(5000, splitClientConfig.eventsPerPush()); - assertEquals("none", splitClientConfig.trafficType()); - assertTrue(splitClientConfig.debugEnabled()); - assertTrue(splitClientConfig.streamingEnabled()); - assertTrue(splitClientConfig.persistentAttributesEnabled()); - assertEquals("apiEndpoint.split.io", splitClientConfig.endpoint()); - assertEquals("eventsEndpoint.split.io", splitClientConfig.eventsEndpoint()); - assertEquals("sseAuthServiceEndpoint.split.io", splitClientConfig.authServiceUrl()); - assertEquals("streamingServiceEndpoint.split.io", splitClientConfig.streamingServiceUrl()); - assertEquals("telemetryServiceEndpoint.split.io", splitClientConfig.telemetryEndpoint()); - assertEquals(Arrays.asList("split1", "split2"), splitClientConfig.syncConfig().getFilters().get(0).getValues()); - assertEquals(SplitFilter.Type.BY_NAME, splitClientConfig.syncConfig().getFilters().get(0).getType()); - assertEquals(Arrays.asList("split_", "my_split_"), splitClientConfig.syncConfig().getFilters().get(1).getValues()); - assertEquals(SplitFilter.Type.BY_PREFIX, splitClientConfig.syncConfig().getFilters().get(1).getType()); - } -} diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock deleted file mode 100644 index bd44637..0000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,29 +0,0 @@ -PODS: - - Flutter (1.0.0) - - Split (2.15.0) - - splitio (0.1.0): - - Flutter - - Split (~> 2.15.0) - -DEPENDENCIES: - - Flutter (from `Flutter`) - - splitio (from `.symlinks/plugins/splitio/ios`) - -SPEC REPOS: - trunk: - - Split - -EXTERNAL SOURCES: - Flutter: - :path: Flutter - splitio: - :path: ".symlinks/plugins/splitio/ios" - -SPEC CHECKSUMS: - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - Split: d103c6afa47b5d1eac21e066c6bc09c879b21798 - splitio: 6ef65e65e042489e9d30fe1f56abe9340bf69039 - -PODFILE CHECKSUM: 6ab177d3659abbf5f15e864674366127c98cb8c0 - -COCOAPODS: 1.11.3 diff --git a/example/ios/SplitTests/SplitClientConfigHelperTests.swift b/example/ios/SplitTests/SplitClientConfigHelperTests.swift deleted file mode 100644 index 6534c9f..0000000 --- a/example/ios/SplitTests/SplitClientConfigHelperTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -import XCTest -@testable import splitio - -class SplitClientConfigHelperTests: XCTestCase { - - func testConfigValuesAreMappedCorrectly() throws { - let configValues: [String: Any?] = ["featuresRefreshRate": 80000, - "segmentsRefreshRate": 70000, - "impressionsRefreshRate": 60000, - "telemetryRefreshRate": 2000, - "eventsQueueSize": 3999, - "impressionsQueueSize": 2999, - "eventFlushInterval": 40000, - "eventsPerPush": 5000, - "trafficType": "none", - "connectionTimeOut": 10, - "readTimeout": 25, - "disableLabels": true, - "enableDebug": true, - "proxyHost": "https://proxy", - "ready": 25, - "streamingEnabled": true, - "persistentAttributesEnabled": true, - "syncConfig": ["syncConfigNames": ["split1", "split2"], "syncConfigPrefixes": ["split_", "my_split_"]]] - - let splitClientConfig = SplitClientConfigHelper.fromMap(configurationMap: configValues, impressionListener: nil) - - XCTAssert(80000 == splitClientConfig.featuresRefreshRate) - XCTAssert(70000 == splitClientConfig.segmentsRefreshRate) - XCTAssert(60000 == splitClientConfig.impressionRefreshRate) - XCTAssert(2000 == splitClientConfig.telemetryRefreshRate) - XCTAssert(3999 == splitClientConfig.eventsQueueSize) - XCTAssert(2999 == splitClientConfig.impressionsQueueSize) - XCTAssert(40000 == splitClientConfig.eventsPushRate) - XCTAssert(5000 == splitClientConfig.eventsPerPush) - XCTAssert("none" == splitClientConfig.trafficType) - XCTAssert(splitClientConfig.isDebugModeEnabled) - XCTAssert(splitClientConfig.streamingEnabled) - XCTAssert(splitClientConfig.persistentAttributesEnabled) - } -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml deleted file mode 100644 index 35cec97..0000000 --- a/example/pubspec.yaml +++ /dev/null @@ -1,84 +0,0 @@ -name: splitio_example -description: Demonstrates how to use the splitio plugin. - -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -environment: - sdk: ">=2.16.2 <3.0.0" - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - splitio: - # When depending on this package from a real application you should use: - # splitio: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^1.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/lib/channel/method_channel_manager.dart b/lib/channel/method_channel_manager.dart deleted file mode 100644 index 89ec677..0000000 --- a/lib/channel/method_channel_manager.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:splitio/method_call_handler.dart'; - -class MethodChannelManager { - final MethodChannel _channel; - - final Set _handlers = {}; - - MethodChannelManager(this._channel) { - _channel.setMethodCallHandler((call) => handle(call)); - } - - void addHandler(MethodCallHandler handler) { - _handlers.add(handler); - } - - void removeHandler(MethodCallHandler handler) { - _handlers.remove(handler); - } - - Future handle(MethodCall call) async { - for (MethodCallHandler handler in _handlers) { - handler.handle(call); - } - } - - Future invokeMethod(String method, [dynamic arguments]) { - return _channel.invokeMethod(method, arguments); - } - - Future?> invokeMapMethod(String method, [dynamic arguments]) { - return _channel.invokeMapMethod(method, arguments); - } - - Future?> invokeListMethod(String method, [dynamic arguments]) { - return _channel.invokeListMethod(method, arguments); - } -} diff --git a/lib/events/split_events_listener.dart b/lib/events/split_events_listener.dart deleted file mode 100644 index be9c9f6..0000000 --- a/lib/events/split_events_listener.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:async'; - -import 'package:splitio/channel/method_channel_manager.dart'; -import 'package:splitio/events/split_method_call_handler.dart'; -import 'package:splitio/split_client.dart'; - -abstract class SplitEventsListener { - Future onReady(); - - Future onReadyFromCache(); - - Stream onUpdated(); - - Future onTimeout(); - - void destroy(); -} - -class DefaultEventsListener implements SplitEventsListener { - final MethodChannelManager _methodChannelManager; - - final SplitEventMethodCallHandler _methodCallHandler; - - DefaultEventsListener(this._methodChannelManager, this._methodCallHandler) { - _methodChannelManager.addHandler(_methodCallHandler); - } - - @override - Future onReady() { - return _methodCallHandler.onReady(); - } - - @override - Future onReadyFromCache() { - return _methodCallHandler.onReadyFromCache(); - } - - @override - Stream onUpdated() { - return _methodCallHandler.onUpdated(); - } - - @override - Future onTimeout() { - return _methodCallHandler.onTimeout(); - } - - @override - void destroy() { - _methodCallHandler.destroy(); - _methodChannelManager.removeHandler(_methodCallHandler); - } -} diff --git a/lib/events/split_method_call_handler.dart b/lib/events/split_method_call_handler.dart deleted file mode 100644 index 862017a..0000000 --- a/lib/events/split_method_call_handler.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:splitio/events/split_events_listener.dart'; -import 'package:splitio/method_call_handler.dart'; -import 'package:splitio/split_client.dart'; - -class SplitEventMethodCallHandler - implements MethodCallHandler, SplitEventsListener { - static const String _eventClientReady = 'clientReady'; - static const String _eventClientReadyFromCache = 'clientReadyFromCache'; - static const String _eventClientTimeout = 'clientTimeout'; - static const String _eventClientUpdated = 'clientUpdated'; - - final String _matchingKey; - final String? _bucketingKey; - final SplitClient _splitClient; - final StreamController _updateStreamCompleter = - StreamController(); - - final Map> _clientEventCallbacks = { - _eventClientReady: Completer(), - _eventClientReadyFromCache: Completer(), - _eventClientTimeout: Completer(), - }; - - final Map _triggeredClientEvents = { - _eventClientReady: false, - _eventClientReadyFromCache: false, - _eventClientTimeout: false, - }; - - SplitEventMethodCallHandler( - this._matchingKey, this._bucketingKey, this._splitClient); - - @override - Future handle(MethodCall call) async { - if (_clientEventCallbacks.containsKey(call.method)) { - if (_matchingKey == call.arguments['matchingKey'] && - _bucketingKey == call.arguments['bucketingKey']) { - var clientEventCallback = _clientEventCallbacks[call.method]; - if (clientEventCallback != null && !clientEventCallback.isCompleted) { - clientEventCallback.complete(_splitClient); - } - - if (_triggeredClientEvents.containsKey(call.method)) { - _triggeredClientEvents[call.method] = true; - } - } - } else if (call.method == _eventClientUpdated && - _updateStreamCompleter.hasListener && - !_updateStreamCompleter.isPaused && - !_updateStreamCompleter.isClosed) { - _updateStreamCompleter.add(_splitClient); - } - } - - @override - Future onReady() { - return _onEvent(_eventClientReady); - } - - @override - Future onReadyFromCache() { - return _onEvent(_eventClientReadyFromCache); - } - - @override - Stream onUpdated() { - return _updateStreamCompleter.stream; - } - - @override - Future onTimeout() { - return _onEvent(_eventClientTimeout); - } - - @override - void destroy() { - _updateStreamCompleter.close(); - } - - Future _onEvent(String sdkEvent) { - if (_triggeredClientEvents.containsKey(sdkEvent) && - _triggeredClientEvents[sdkEvent] == true) { - return Future.value(_splitClient); - } - - return (_clientEventCallbacks[sdkEvent])?.future ?? Future.error(Object()); - } -} diff --git a/lib/split_client.dart b/lib/split_client.dart deleted file mode 100644 index fb72ce0..0000000 --- a/lib/split_client.dart +++ /dev/null @@ -1,342 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:splitio/channel/method_channel_manager.dart'; -import 'package:splitio/events/split_events_listener.dart'; -import 'package:splitio/events/split_method_call_handler.dart'; -import 'package:splitio/split_result.dart'; - -abstract class SplitClient { - /// Performs an evaluation for the [splitName] feature. - /// - /// This method returns the string 'control' if: there was an exception in - /// evaluating the feature, the SDK does not know of the existence of this - /// feature, and/or the feature was deleted through the web console. - /// - /// The sdk returns the default treatment of this feature if: The feature was - /// killed, or the key did not match any of the conditions in the feature - /// roll-out plan. - /// - /// [splitName] is the feature we want to evaluate. - /// - /// Optionally, a [Map] can be specified with the [attributes] parameter to - /// take into account when evaluating. - /// - /// Returns the evaluated treatment, the default treatment of this feature, or 'control'. - Future getTreatment(String splitName, - [Map attributes = const {}]); - - /// Performs and evaluation and returns a [SplitResult] object for the - /// [splitName] feature. This object contains the treatment alongside the - /// split's configuration, if any. - /// - /// This method returns 'control' if: there was an exception in - /// evaluating the treatment, the SDK does not know of the existence of this - /// feature, and/or the feature was deleted through the web console. - /// - /// The sdk returns the default treatment of this feature if: The feature was - /// killed, or the key did not match any of the conditions in the feature - /// roll-out plan. - /// - /// [splitName] is the feature we want to evaluate. - /// - /// Optionally, a [Map] can be specified with the [attributes] parameter to - /// take into account when evaluating. - Future getTreatmentWithConfig(String splitName, - [Map attributes = const {}]); - - /// Convenience method to perform multiple evaluations. Returns a [Map] in - /// which the keys are split names and the values are treatments. - /// - /// A list of splits need to be specified in [splitNames]. - /// - /// Optionally, a [Map] can be specified with the [attributes] parameter to - /// take into account when evaluating. - Future> getTreatments(List splitNames, - [Map attributes = const {}]); - - /// Convenience method to perform multiple evaluations. Returns a [Map] in - /// which the keys are split names and the values are [SplitResult] objects. - /// - /// A list of splits need to be specified in [splitNames]. - /// - /// Optionally, a [Map] can be specified with the [attributes] parameter to - /// take into account when evaluating. - Future> getTreatmentsWithConfig( - List splitNames, - [Map attributes = const {}]); - - /// Enqueue a new event to be sent to split data collection services. - /// - /// [eventType] is a [String] representing the event type. - /// - /// [trafficType] optionally specifies which traffic type this event belongs - /// to. - /// - /// A [value] can be specified if desired. - /// - /// A [Map] of custom properties can be specified in [properties]. - Future track(String eventType, - {String? trafficType, - double? value, - Map properties = const {}}); - - /// Stores a custom attribute value to be used in all evaluations. - /// - /// Specify the attribute's name with [attributeName], and its value in - /// [value]. - /// - /// Returns [true] if the operation was successful; false otherwise. - Future setAttribute(String attributeName, dynamic value); - - /// Retrieves an attribute previously saved. - /// - /// [attributeName] is the name of the attribute. - Future getAttribute(String attributeName); - - /// Stores a set of custom attributes to be used in all evaluations. - /// - /// Specify a [Map] of attributes in [attributes]. - /// - /// Returns [true] if the operation was successful; false otherwise. - Future setAttributes(Map attributes); - - /// Retrieves a [Map] of every attribute currently bound. Keys are attribute - /// names, and values their respective values. - Future> getAttributes(); - - /// Removes a specific attribute from storage. - /// - /// Specify the attribute to be removed using [attributeName]. - /// - /// Returns [true] if the operation was successful; false otherwise. - Future removeAttribute(String attributeName); - - /// Removes all bound attributes. - /// - /// Returns [true] if the operation was successful; false otherwise. - Future clearAttributes(); - - /// Forces the client to upload all queued events and impressions. - /// - /// Use only if there's a need to do this on demand. Otherwise, the SDK - /// performs this automatically. - Future flush(); - - /// Removes the client from memory and stops its synchronization tasks. - Future destroy(); - - /// Returns Future that is completed when the most up-to-date information has been - /// retrieved from the Split cloud. - Future whenReady(); - - /// Returns Future that is completed once the SDK has been able to load - /// definitions from cache. This information is not guaranteed to be the most - /// up-to-date, but all the functionality will be available. - Future whenReadyFromCache(); - - /// Returns Future that is completed when changes have been made, such as creating - /// new splits or modifying segments. - Stream whenUpdated(); - - /// Returns Future that is completed if the SDK has not been able to get ready in time. - Future whenTimeout(); -} - -class DefaultSplitClient implements SplitClient { - static const String _controlTreatment = 'control'; - static const SplitResult _controlResult = - SplitResult(_controlTreatment, null); - - final MethodChannelManager _methodChannelManager; - late final SplitEventMethodCallHandler _methodCallHandler; - final String _matchingKey; - final String? _bucketingKey; - - late final SplitEventsListener _splitEventsListener; - - DefaultSplitClient( - this._methodChannelManager, this._matchingKey, this._bucketingKey) { - _methodCallHandler = - SplitEventMethodCallHandler(_matchingKey, _bucketingKey, this); - _splitEventsListener = - DefaultEventsListener(_methodChannelManager, _methodCallHandler); - } - - @visibleForTesting - DefaultSplitClient.withEventListener(this._methodChannelManager, - this._matchingKey, this._bucketingKey, this._splitEventsListener); - - @override - Future getTreatment(String splitName, - [Map attributes = const {}]) async { - return await _methodChannelManager.invokeMethod( - 'getTreatment', - _buildParameters( - {'splitName': splitName, 'attributes': attributes})) ?? - _controlTreatment; - } - - @override - Future getTreatmentWithConfig(String splitName, - [Map attributes = const {}]) async { - Map? treatment = (await _methodChannelManager.invokeMapMethod( - 'getTreatmentWithConfig', - _buildParameters( - {'splitName': splitName, 'attributes': attributes}))) - ?.entries - .first - .value; - if (treatment == null) { - return _controlResult; - } - - return SplitResult(treatment['treatment'], treatment['config']); - } - - @override - Future> getTreatments(List splitNames, - [Map attributes = const {}]) async { - Map? treatments = await _methodChannelManager.invokeMapMethod( - 'getTreatments', - _buildParameters({'splitName': splitNames, 'attributes': attributes})); - - return treatments - ?.map((key, value) => MapEntry(key, value)) ?? - {for (var item in splitNames) item: _controlTreatment}; - } - - @override - Future> getTreatmentsWithConfig( - List splitNames, - [Map attributes = const {}]) async { - Map? treatments = await _methodChannelManager.invokeMapMethod( - 'getTreatmentsWithConfig', - _buildParameters({'splitName': splitNames, 'attributes': attributes})); - - return treatments?.map((key, value) => - MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? - {for (var item in splitNames) item: _controlResult}; - } - - @override - Future track(String eventType, - {String? trafficType, - double? value, - Map properties = const {}}) async { - var parameters = _buildParameters({'eventType': eventType}); - - if (trafficType != null) { - parameters['trafficType'] = trafficType; - } - - if (value != null) { - parameters['value'] = value; - } - - try { - return await _methodChannelManager.invokeMethod("track", parameters) - as bool; - } on Exception catch (_) { - return false; - } - } - - @override - Future setAttribute(String attributeName, dynamic value) async { - var result = await _methodChannelManager.invokeMethod('setAttribute', - _buildParameters({'attributeName': attributeName, 'value': value})); - - if (result is bool) { - return result; - } - - return false; - } - - @override - Future getAttribute(String attributeName) async { - return _methodChannelManager.invokeMethod( - 'getAttribute', _buildParameters({'attributeName': attributeName})); - } - - @override - Future setAttributes(Map attributes) async { - var result = await _methodChannelManager.invokeMethod( - 'setAttributes', _buildParameters({'attributes': attributes})); - - if (result is bool) { - return result; - } - - return false; - } - - @override - Future> getAttributes() async { - return (await _methodChannelManager.invokeMapMethod( - 'getAllAttributes', _buildParameters())) - ?.map((key, value) => MapEntry(key, value)) ?? - {}; - } - - @override - Future removeAttribute(String attributeName) async { - return await _methodChannelManager.invokeMethod( - 'removeAttribute', _buildParameters({'attributeName': attributeName})); - } - - @override - Future clearAttributes() async { - return await _methodChannelManager.invokeMethod( - 'clearAttributes', _buildParameters()); - } - - @override - Future flush() async { - return _methodChannelManager.invokeMethod('flush', _buildParameters()); - } - - @override - Future destroy() async { - _splitEventsListener.destroy(); - return _methodChannelManager.invokeMethod('destroy', _buildParameters()); - } - - @override - Future whenReady() { - return _splitEventsListener.onReady(); - } - - @override - Future whenReadyFromCache() { - return _splitEventsListener.onReadyFromCache(); - } - - @override - Stream whenUpdated() { - return _splitEventsListener.onUpdated(); - } - - @override - Future whenTimeout() { - return _splitEventsListener.onTimeout(); - } - - Map _getKeysMap() { - Map result = {'matchingKey': _matchingKey}; - - if (_bucketingKey != null) { - result.addAll({'bucketingKey': _bucketingKey!}); - } - - return result; - } - - Map _buildParameters( - [Map parameters = const {}]) { - Map result = {}; - result.addAll(parameters); - result.addAll(_getKeysMap()); - - return result; - } -} diff --git a/lib/split_sync_config.dart b/lib/split_sync_config.dart deleted file mode 100644 index 8e59b20..0000000 --- a/lib/split_sync_config.dart +++ /dev/null @@ -1,13 +0,0 @@ -class SyncConfig { - late final Set _names; - late final Set _prefixes; - - Set get names => _names; - - Set get prefixes => _prefixes; - - SyncConfig({Set names = const {}, Set prefixes = const {}}) { - _names = names; - _prefixes = prefixes; - } -} diff --git a/lib/splitio.dart b/lib/splitio.dart deleted file mode 100644 index 254a486..0000000 --- a/lib/splitio.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:splitio/channel/method_channel_manager.dart'; -import 'package:splitio/impressions/impressions_method_call_handler.dart'; -import 'package:splitio/impressions/split_impression.dart'; -import 'package:splitio/method_call_handler.dart'; -import 'package:splitio/split_client.dart'; -import 'package:splitio/split_configuration.dart'; -import 'package:splitio/split_view.dart'; - -export 'package:splitio/impressions/split_impression.dart'; -export 'package:splitio/split_client.dart'; -export 'package:splitio/split_configuration.dart'; -export 'package:splitio/split_result.dart'; -export 'package:splitio/split_sync_config.dart'; -export 'package:splitio/split_view.dart'; - -typedef ClientReadinessCallback = void Function(SplitClient splitClient); - -class Splitio { - final String _apiKey; - final String _defaultMatchingKey; - late final String? _defaultBucketingKey; - late final SplitConfiguration? _splitConfiguration; - late final StreamMethodCallHandler _impressionsMethodCallHandler; - final MethodChannelManager _methodChannelManager = - MethodChannelManager(const MethodChannel('splitio')); - - /// SDK instance constructor. - /// - /// Use [_apiKey] to specify your Split API key. - /// - /// Use [_defaultMatchingKey] to specify a default matching key. The default - /// client will be associated with it. - /// - /// An optional [bucketingKey] can also be specified. - /// - /// Use the optional [configuration] parameter to fine tune configuration options. - Splitio(this._apiKey, this._defaultMatchingKey, - {String? bucketingKey, SplitConfiguration? configuration}) { - _defaultBucketingKey = bucketingKey; - _splitConfiguration = configuration; - _impressionsMethodCallHandler = ImpressionsMethodCallHandler(); - _methodChannelManager.addHandler(_impressionsMethodCallHandler); - - _init(); - } - - /// Gets a [SplitClient] to interact with the SDK. - /// - /// Optionally provide a [matchingKey], otherwise the key used when creating - /// the SDK instance will be used. - /// - /// An optional [bucketingKey] can also be specified. - /// - /// While this method returns a valid [SplitClient] object, it may not be - /// initialized right away. To listen for specific client readiness events, - /// the optional callback parameters can be used: - /// - /// [onReady] is executed when the most up-to-date information has been - /// retrieved from the Split cloud. - /// - /// [onReadyFromCache] is executed once the SDK has been able to load - /// definitions from cache. This information is not guaranteed to be the most - /// up-to-date, but all the functionality will be available. - /// - /// [onUpdated] is executed when changes have been made, such as creating - /// new splits or modifying segments. - /// - /// [onTimeout] is executed if the SDK has not been able to get ready in time. - SplitClient client( - {String? matchingKey, - String? bucketingKey, - ClientReadinessCallback? onReady, - ClientReadinessCallback? onReadyFromCache, - ClientReadinessCallback? onUpdated, - ClientReadinessCallback? onTimeout}) { - String? key = matchingKey ?? _defaultMatchingKey; - - var client = DefaultSplitClient(_methodChannelManager, key, bucketingKey); - if (onReady != null) { - client.whenReady().then((client) => onReady.call(client)); - } - - if (onReadyFromCache != null) { - client - .whenReadyFromCache() - .then((client) => onReadyFromCache.call(client)); - } - - if (onTimeout != null) { - client.whenTimeout().then((client) => onTimeout.call(client)); - } - - if (onUpdated != null) { - client.whenUpdated().listen((client) => onUpdated.call(client)); - } - - _methodChannelManager.invokeMethod( - 'getClient', _buildGetClientArguments(key, bucketingKey)); - - return client; - } - - Future> splitNames() async { - List splitNames = - await _methodChannelManager.invokeListMethod('splitNames') ?? - []; - - return splitNames; - } - - Future> splits() async { - List callResult = (await _methodChannelManager - .invokeListMethod>('splits') ?? - []); - - List splits = []; - for (var element in callResult) { - SplitView? splitView = SplitView.fromEntry(element); - if (splitView != null) { - splits.add(splitView); - } - } - - return Future.value(splits); - } - - /// If the impressionListener configuration has been enabled, - /// generated impressions will be streamed here. - Stream impressionsStream() { - return _impressionsMethodCallHandler.stream(); - } - - Future split(String splitName) async { - Map? mapResult = await _methodChannelManager - .invokeMapMethod('split', {'splitName': splitName}); - - if (mapResult == null) { - return null; - } - - return SplitView.fromEntry(mapResult); - } - - Future _init() { - Map arguments = { - 'apiKey': _apiKey, - 'matchingKey': _defaultMatchingKey, - 'sdkConfiguration': _splitConfiguration?.configurationMap ?? {}, - }; - - if (_defaultBucketingKey != null) { - arguments.addAll({'bucketingKey': _defaultBucketingKey}); - } - - return _methodChannelManager.invokeMethod('init', arguments); - } - - Map _buildGetClientArguments( - String key, String? bucketingKey) { - var arguments = { - 'matchingKey': key, - }; - - if (bucketingKey != null) { - arguments.addAll({'bucketingKey': bucketingKey}); - } - - return arguments; - } -} diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100644 index 1069ef9..0000000 --- a/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: splitio -description: Official plugin for split.io, the platform for controlled rollouts, which serves features to your users via a Split feature flag to manage your complete customer experience. -version: 0.1.1 -homepage: https://split.io/ -repository: https://github.com/splitio/flutter-sdk-plugin - -environment: - sdk: ">=2.16.2 <3.0.0" - flutter: ">=2.5.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^1.0.0 - -flutter: - plugin: - platforms: - android: - package: io.split.splitio - pluginClass: SplitioPlugin - ios: - pluginClass: SplitioPlugin diff --git a/splitio/.gitignore b/splitio/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/splitio/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/.metadata b/splitio/.metadata similarity index 100% rename from .metadata rename to splitio/.metadata diff --git a/splitio/CHANGELOG.md b/splitio/CHANGELOG.md new file mode 100644 index 0000000..9d0be90 --- /dev/null +++ b/splitio/CHANGELOG.md @@ -0,0 +1,61 @@ +# 0.1.6 (Aug 15, 2023) + +* Added `readyTimeout` configuration option. If the SDK is not ready after the amount of time (in seconds) specified by this option, the `whenTimeout` future of the client will be completed. Defaults to 10 seconds. A negative value means no timeout. + +# 0.1.6-rc.1 (Aug 15, 2023) + +# 0.1.5 (Jul 19, 2023) + +* Updated iOS SDK to `2.21.0` & Android SDK to `3.3.0` + +# 0.1.5-rc.1 (Jul 19, 2023) + +# 0.1.4 (May 23, 2023) + +* Added `setUserConsent` method. +* Added `getUserConsent` method. + +# 0.1.4-rc.1 (May 23, 2023) + +# 0.1.3 (May 18, 2023) + +* Updated iOS SDK to `2.20.1` & Android SDK to `3.2.1` +* Added support for new configuration options: + * `impressionsMode` + * `syncEnabled` + * `logLevel` + * `userConsent` + * `encryptionEnabled` +* Deprecated `enableDebug` configuration in favor of `logLevel`. + +# 0.1.3-rc.1 (May 18, 2023) + +# 0.1.2+2 (Dec 7, 2022) + +Added consumer ProGuard rules for Android. + +# 0.1.2+1 (Sep 14, 2022) + +Added exports for models. + +# 0.1.2 (Sep 13, 2022) + +* Migrated to federated structure. +* Added support for Impression Listener. +* Added support for Sync Configuration. +* Added support for SDK event listeners. +* Added support for manager methods. +* Added support for linking native factory. + +# 0.1.1 (Aug 19, 2022) + +Minor fixes. + +# 0.1.0 (Aug 3, 2022) + +Initial release. + +* Added support for SDK instantiation. +* Added support for multiple clients. +* Added support for evaluation. +* Added support for tracking events. diff --git a/LICENSE b/splitio/LICENSE similarity index 93% rename from LICENSE rename to splitio/LICENSE index 20e4bd8..54326b0 100644 --- a/LICENSE +++ b/splitio/LICENSE @@ -1,6 +1,7 @@ + Apache License Version 2.0, January 2004 - https://www.apache.org/licenses/ + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -175,24 +176,13 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright © 2023 Split Software, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/README.md b/splitio/README.md similarity index 92% rename from README.md rename to splitio/README.md index fe2b2e1..7d2b918 100644 --- a/README.md +++ b/splitio/README.md @@ -1,7 +1,7 @@ # Split Flutter plugin ## Overview -This is the official Flutter plugin designed to work with Split, the platform for controlled rollouts, which serves features to your users via a Split feature flag to manage your complete customer experience. +This is the official Flutter plugin designed to work with Split, the platform for controlled rollouts, which serves features to your users via feature flags to manage your complete customer experience. [![Twitter Follow](https://img.shields.io/twitter/follow/splitsoftware.svg?style=social&label=Follow&maxAge=1529000)](https://twitter.com/intent/follow?screen_name=splitsoftware) @@ -19,11 +19,11 @@ Here is a small example of how to perform an evaluation. ```dart import 'package:splitio/splitio.dart'; -final Splitio _split = Splitio('YOUR_API_KEY', 'KEY'); +final Splitio _split = Splitio('YOUR_SDK_KEY', 'USER_KEY'); /// Get treatment _split.client(onReady: (client) async { - final String treatment = await client.getTreatment('SPLIT_NAME'); + final String treatment = await client.getTreatment('FEATURE_FLAG_NAME'); if (treatment == 'on') { /// Insert code here to show on treatment @@ -35,7 +35,7 @@ _split.client(onReady: (client) async { } ``` -For a more elaborate usage example see [here](https://github.com/splitio/flutter-sdk-plugin/blob/main/example/lib/main.dart). +For a more elaborate usage example see [here](https://github.com/splitio/flutter-sdk-plugin/blob/splitio/main/example/lib/main.dart). For additional information, refer to our docs page. @@ -44,7 +44,7 @@ For additional information, refer to our docs page. The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/flutter-sdk-plugin/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner. ## Contributing -Please see [contributors guide](https://github.com/splitio/flutter-sdk-plugin/blob/main/CONTRIBUTORS-GUIDE.md) to find all you need to submit a Pull Request (PR). +Please see [contributors guide](https://github.com/splitio/flutter-sdk-plugin/blob/splitio/main/CONTRIBUTORS-GUIDE.md) to find all you need to submit a Pull Request (PR). ## License Licensed under the Apache License, Version 2.0. See: [Apache License](https://www.apache.org/licenses/). diff --git a/analysis_options.yaml b/splitio/analysis_options.yaml similarity index 100% rename from analysis_options.yaml rename to splitio/analysis_options.yaml diff --git a/example/.gitignore b/splitio/example/.gitignore similarity index 100% rename from example/.gitignore rename to splitio/example/.gitignore diff --git a/example/.metadata b/splitio/example/.metadata similarity index 100% rename from example/.metadata rename to splitio/example/.metadata diff --git a/example/README.md b/splitio/example/README.md similarity index 100% rename from example/README.md rename to splitio/example/README.md diff --git a/example/analysis_options.yaml b/splitio/example/analysis_options.yaml similarity index 100% rename from example/analysis_options.yaml rename to splitio/example/analysis_options.yaml diff --git a/example/android/.gitignore b/splitio/example/android/.gitignore similarity index 85% rename from example/android/.gitignore rename to splitio/example/android/.gitignore index 6f56801..7760dbb 100644 --- a/example/android/.gitignore +++ b/splitio/example/android/.gitignore @@ -1,8 +1,5 @@ -gradle-wrapper.jar /.gradle /captures/ -/gradlew -/gradlew.bat /local.properties GeneratedPluginRegistrant.java diff --git a/example/android/app/build.gradle b/splitio/example/android/app/build.gradle similarity index 100% rename from example/android/app/build.gradle rename to splitio/example/android/app/build.gradle diff --git a/example/android/app/src/debug/AndroidManifest.xml b/splitio/example/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from example/android/app/src/debug/AndroidManifest.xml rename to splitio/example/android/app/src/debug/AndroidManifest.xml diff --git a/example/android/app/src/main/AndroidManifest.xml b/splitio/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from example/android/app/src/main/AndroidManifest.xml rename to splitio/example/android/app/src/main/AndroidManifest.xml diff --git a/example/android/app/src/main/java/io/split/splitio_example/MainActivity.java b/splitio/example/android/app/src/main/java/io/split/splitio_example/MainActivity.java similarity index 100% rename from example/android/app/src/main/java/io/split/splitio_example/MainActivity.java rename to splitio/example/android/app/src/main/java/io/split/splitio_example/MainActivity.java diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/splitio/example/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable-v21/launch_background.xml rename to splitio/example/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/splitio/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable/launch_background.xml rename to splitio/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/splitio/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to splitio/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/splitio/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to splitio/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/splitio/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to splitio/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/splitio/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to splitio/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/splitio/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to splitio/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/values-night/styles.xml b/splitio/example/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from example/android/app/src/main/res/values-night/styles.xml rename to splitio/example/android/app/src/main/res/values-night/styles.xml diff --git a/example/android/app/src/main/res/values/styles.xml b/splitio/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from example/android/app/src/main/res/values/styles.xml rename to splitio/example/android/app/src/main/res/values/styles.xml diff --git a/example/android/app/src/profile/AndroidManifest.xml b/splitio/example/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from example/android/app/src/profile/AndroidManifest.xml rename to splitio/example/android/app/src/profile/AndroidManifest.xml diff --git a/example/android/build.gradle b/splitio/example/android/build.gradle similarity index 81% rename from example/android/build.gradle rename to splitio/example/android/build.gradle index 5160cdd..0822484 100644 --- a/example/android/build.gradle +++ b/splitio/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/android/gradle.properties b/splitio/example/android/gradle.properties similarity index 100% rename from example/android/gradle.properties rename to splitio/example/android/gradle.properties diff --git a/splitio/example/android/gradle/wrapper/gradle-wrapper.jar b/splitio/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000..13372ae Binary files /dev/null and b/splitio/example/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/splitio/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 93% rename from example/android/gradle/wrapper/gradle-wrapper.properties rename to splitio/example/android/gradle/wrapper/gradle-wrapper.properties index 562c5e4..bfd9890 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/splitio/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip diff --git a/splitio/example/android/gradlew b/splitio/example/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/splitio/example/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/splitio/example/android/gradlew.bat b/splitio/example/android/gradlew.bat new file mode 100755 index 0000000..aec9973 --- /dev/null +++ b/splitio/example/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/example/android/settings.gradle b/splitio/example/android/settings.gradle similarity index 100% rename from example/android/settings.gradle rename to splitio/example/android/settings.gradle diff --git a/example/ios/.gitignore b/splitio/example/ios/.gitignore similarity index 100% rename from example/ios/.gitignore rename to splitio/example/ios/.gitignore diff --git a/splitio/example/ios/Flutter/AppFrameworkInfo.plist b/splitio/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/splitio/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/splitio/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from example/ios/Flutter/Debug.xcconfig rename to splitio/example/ios/Flutter/Debug.xcconfig diff --git a/example/ios/Flutter/Release.xcconfig b/splitio/example/ios/Flutter/Release.xcconfig similarity index 100% rename from example/ios/Flutter/Release.xcconfig rename to splitio/example/ios/Flutter/Release.xcconfig diff --git a/splitio/example/ios/Podfile b/splitio/example/ios/Podfile new file mode 100644 index 0000000..313ea4a --- /dev/null +++ b/splitio/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/splitio/example/ios/Podfile.lock b/splitio/example/ios/Podfile.lock new file mode 100644 index 0000000..51ab010 --- /dev/null +++ b/splitio/example/ios/Podfile.lock @@ -0,0 +1,29 @@ +PODS: + - Flutter (1.0.0) + - Split (2.21.0) + - splitio_ios (0.3.0): + - Flutter + - Split (~> 2.21.0) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - splitio_ios (from `.symlinks/plugins/splitio_ios/ios`) + +SPEC REPOS: + trunk: + - Split + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + splitio_ios: + :path: ".symlinks/plugins/splitio_ios/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Split: e4324a8fe3fcb19c36a9e4fd84a893fc6d4bbeb0 + splitio_ios: 8293cb4e46661f9403f92a0d143c5bf6ad5053d4 + +PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d + +COCOAPODS: 1.11.3 diff --git a/splitio/example/ios/Runner.xcodeproj/project.pbxproj b/splitio/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a69a7aa --- /dev/null +++ b/splitio/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,560 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 64E9F6FE44F7FCB12EB87449 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8C36271CBA13C112842B4DCF /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1368C3F6AADFE7B866247EA7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 383F16084D3941F6D2383A90 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5F80958AAA59EFFD9A29135E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8C1CA74A3F7715873887E477 /* Pods-SplitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SplitTests.release.xcconfig"; path = "Target Support Files/Pods-SplitTests/Pods-SplitTests.release.xcconfig"; sourceTree = ""; }; + 8C36271CBA13C112842B4DCF /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 96E6F25AD1A4E2E5BD01F74C /* Pods-SplitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SplitTests.profile.xcconfig"; path = "Target Support Files/Pods-SplitTests/Pods-SplitTests.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CDE81357140D4D18CD672621 /* Pods_SplitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SplitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F5DF347A782571EC3639305B /* Pods-SplitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SplitTests.debug.xcconfig"; path = "Target Support Files/Pods-SplitTests/Pods-SplitTests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 64E9F6FE44F7FCB12EB87449 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 28069006813B6DC2FE9CB1B1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8C36271CBA13C112842B4DCF /* Pods_Runner.framework */, + CDE81357140D4D18CD672621 /* Pods_SplitTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7153CEFEC2FBAAA4068E994C /* Pods */ = { + isa = PBXGroup; + children = ( + 1368C3F6AADFE7B866247EA7 /* Pods-Runner.debug.xcconfig */, + 5F80958AAA59EFFD9A29135E /* Pods-Runner.release.xcconfig */, + 383F16084D3941F6D2383A90 /* Pods-Runner.profile.xcconfig */, + F5DF347A782571EC3639305B /* Pods-SplitTests.debug.xcconfig */, + 8C1CA74A3F7715873887E477 /* Pods-SplitTests.release.xcconfig */, + 96E6F25AD1A4E2E5BD01F74C /* Pods-SplitTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 7153CEFEC2FBAAA4068E994C /* Pods */, + 28069006813B6DC2FE9CB1B1 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 06FEC5AC68198B8F6612EC6F /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 83B902C28BA5AB1DD426315E /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1300; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 06FEC5AC68198B8F6612EC6F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 83B902C28BA5AB1DD426315E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.split.splitioExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.split.splitioExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.split.splitioExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/splitio/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to splitio/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/splitio/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to splitio/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/splitio/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to splitio/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/splitio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to splitio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/splitio/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to splitio/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/splitio/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to splitio/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/splitio/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to splitio/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner/AppDelegate.swift b/splitio/example/ios/Runner/AppDelegate.swift similarity index 100% rename from example/ios/Runner/AppDelegate.swift rename to splitio/example/ios/Runner/AppDelegate.swift diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to splitio/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to splitio/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/splitio/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to splitio/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/splitio/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/Main.storyboard rename to splitio/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/splitio/example/ios/Runner/Info.plist b/splitio/example/ios/Runner/Info.plist new file mode 100644 index 0000000..8fe8f6a --- /dev/null +++ b/splitio/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Splitio + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + splitio_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/splitio/example/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from example/ios/Runner/Runner-Bridging-Header.h rename to splitio/example/ios/Runner/Runner-Bridging-Header.h diff --git a/example/lib/main.dart b/splitio/example/lib/main.dart similarity index 89% rename from example/lib/main.dart rename to splitio/example/lib/main.dart index 4e7fe9a..998a90b 100644 --- a/example/lib/main.dart +++ b/splitio/example/lib/main.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:splitio/splitio.dart'; /// Replace these with valid values -const String _apiKey = 'api-key'; +const String _sdkKey = 'sdk-key'; const String _matchingKey = 'user-id'; void main() { @@ -21,12 +21,12 @@ class SplitioExampleApp extends StatefulWidget { } class _SplitioExampleAppState extends State { - String _splitName = ''; + String _featureFlagName = ''; bool _sdkReady = false; bool _sdkReadyFromCache = false; late SplitClient _client; - final Splitio _split = Splitio(_apiKey, _matchingKey, + final Splitio _split = Splitio(_sdkKey, _matchingKey, configuration: SplitConfiguration( trafficType: "user", )); @@ -87,20 +87,20 @@ class _SplitioExampleAppState extends State { Padding( padding: const EdgeInsets.fromLTRB(32, 8, 32, 8), child: TextField( - decoration: - const InputDecoration(hintText: 'Enter split name'), + decoration: const InputDecoration( + hintText: 'Enter feature flag name'), onChanged: (text) { setState(() { - _splitName = text; + _featureFlagName = text; }); }, ), ), Visibility( - visible: _splitName != '', + visible: _featureFlagName != '', child: ElevatedButton( onPressed: performEvaluation, - child: Text('Evaluate: $_splitName')), + child: Text('Evaluate: $_featureFlagName')), ), Visibility( visible: _sdkReady || _sdkReadyFromCache, @@ -131,9 +131,8 @@ class _SplitioExampleAppState extends State { } void performEvaluation() async { - _client - .getTreatment(_splitName) - .then((value) => {print('Evaluation value for $_splitName is $value')}); + _client.getTreatment(_featureFlagName).then( + (value) => {print('Evaluation value for $_featureFlagName is $value')}); } void track() { diff --git a/splitio/example/pubspec.lock b/splitio/example/pubspec.lock new file mode 100644 index 0000000..cad1492 --- /dev/null +++ b/splitio/example/pubspec.lock @@ -0,0 +1,225 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "1989d917fbe8e6b39806207df5a3fdd3d816cbd090fac2ce26fb45e9a71476e5" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + lints: + dependency: transitive + description: + name: lints + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" + source: hosted + version: "0.12.15" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + splitio: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.1.6-rc.1" + splitio_android: + dependency: transitive + description: + path: "../../splitio_android" + relative: true + source: path + version: "0.1.6-rc.1" + splitio_ios: + dependency: transitive + description: + path: "../../splitio_ios" + relative: true + source: path + version: "0.1.6-rc.1" + splitio_platform_interface: + dependency: transitive + description: + path: "../../splitio_platform_interface" + relative: true + source: path + version: "1.3.0-rc.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" + source: hosted + version: "0.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" +sdks: + dart: ">=3.0.0-0 <4.0.0" + flutter: ">=2.5.0" diff --git a/splitio/example/pubspec.yaml b/splitio/example/pubspec.yaml new file mode 100644 index 0000000..49d15a3 --- /dev/null +++ b/splitio/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: splitio_example +description: Demonstrates how to use the splitio plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.16.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + splitio: + # When depending on this package from a real application you should use: + # splitio: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +flutter: + uses-material-design: true diff --git a/splitio/lib/split_client.dart b/splitio/lib/split_client.dart new file mode 100644 index 0000000..7252206 --- /dev/null +++ b/splitio/lib/split_client.dart @@ -0,0 +1,290 @@ +import 'package:splitio_platform_interface/splitio_platform_interface.dart'; + +abstract class SplitClient { + /// Performs an evaluation for the [featureFlagName] feature flag. + /// + /// This method returns the string 'control' if: there was an exception in + /// evaluating the feature flag, the SDK does not know of the existence of this + /// feature flag, and/or the feature flag was deleted through the Split user interface. + /// + /// The SDK returns the default treatment of this feature flag if: The feature flag was + /// killed, or the key did not match any of the conditions in the feature flag + /// roll-out plan. + /// + /// [featureFlagName] is the feature flag we want to evaluate. + /// + /// Optionally, a [Map] can be specified with the [attributes] parameter to + /// take into account when evaluating. + /// + /// Returns the evaluated treatment, the default treatment of this feature flag, or 'control'. + Future getTreatment(String featureFlagName, + [Map attributes = const {}]); + + /// Performs and evaluation and returns a [SplitResult] object for the + /// [featureFlagName] feature flag. This object contains the treatment alongside the + /// feature flag's configuration, if any. + /// + /// This method returns 'control' if: there was an exception in + /// evaluating the treatment, the SDK does not know of the existence of this + /// feature flag, and/or the feature flag was deleted through the Split user interface. + /// + /// The SDK returns the default treatment of this feature flag if: The feature flag was + /// killed, or the key did not match any of the conditions in the feature flag + /// roll-out plan. + /// + /// [featureFlagName] is the feature flag we want to evaluate. + /// + /// Optionally, a [Map] can be specified with the [attributes] parameter to + /// take into account when evaluating. + Future getTreatmentWithConfig(String featureFlagName, + [Map attributes = const {}]); + + /// Convenience method to perform multiple evaluations. Returns a [Map] in + /// which the keys are feature flag names and the values are treatments. + /// + /// A list of feature flag names need to be specified in [featureFlagNames]. + /// + /// Optionally, a [Map] can be specified with the [attributes] parameter to + /// take into account when evaluating. + Future> getTreatments(List featureFlagNames, + [Map attributes = const {}]); + + /// Convenience method to perform multiple evaluations. Returns a [Map] in + /// which the keys are feature flag names and the values are [SplitResult] objects. + /// + /// A list of feature flag names need to be specified in [featureFlagNames]. + /// + /// Optionally, a [Map] can be specified with the [attributes] parameter to + /// take into account when evaluating. + Future> getTreatmentsWithConfig( + List featureFlagNames, + [Map attributes = const {}]); + + /// Enqueue a new event to be sent to Split data collection services. + /// + /// [eventType] is a [String] representing the event type. + /// + /// [trafficType] optionally specifies which traffic type this event belongs + /// to. + /// + /// A [value] can be specified if desired. + /// + /// A [Map] of custom properties can be specified in [properties]. + Future track(String eventType, + {String? trafficType, + double? value, + Map properties = const {}}); + + /// Stores a custom attribute value to be used in all evaluations. + /// + /// Specify the attribute's name with [attributeName], and its value in + /// [value]. + /// + /// Returns [true] if the operation was successful; false otherwise. + Future setAttribute(String attributeName, dynamic value); + + /// Retrieves an attribute previously saved. + /// + /// [attributeName] is the name of the attribute. + Future getAttribute(String attributeName); + + /// Stores a set of custom attributes to be used in all evaluations. + /// + /// Specify a [Map] of attributes in [attributes]. + /// + /// Returns [true] if the operation was successful; false otherwise. + Future setAttributes(Map attributes); + + /// Retrieves a [Map] of every attribute currently bound. Keys are attribute + /// names, and values their respective values. + Future> getAttributes(); + + /// Removes a specific attribute from storage. + /// + /// Specify the attribute to be removed using [attributeName]. + /// + /// Returns [true] if the operation was successful; false otherwise. + Future removeAttribute(String attributeName); + + /// Removes all bound attributes. + /// + /// Returns [true] if the operation was successful; false otherwise. + Future clearAttributes(); + + /// Forces the client to upload all queued events and impressions. + /// + /// Use only if there's a need to do this on demand. Otherwise, the SDK + /// performs this automatically. + Future flush(); + + /// Removes the client from memory and stops its synchronization tasks. + Future destroy(); + + /// Returns Future that is completed when the most up-to-date information has been + /// retrieved from the Split cloud. + Future whenReady(); + + /// Returns Future that is completed once the SDK has been able to load + /// definitions from cache. This information is not guaranteed to be the most + /// up-to-date, but all the functionality will be available. + Future whenReadyFromCache(); + + /// Returns Future that is completed when changes have been made, such as creating + /// new feature flags or modifying segments. + Stream whenUpdated(); + + /// Returns Future that is completed if the SDK has not been able to get ready in time. + Future whenTimeout(); +} + +class DefaultSplitClient implements SplitClient { + final SplitioPlatform _platform; + final String _matchingKey; + final String? _bucketingKey; + + DefaultSplitClient(this._platform, this._matchingKey, this._bucketingKey); + + @override + Future getTreatment(String featureFlagName, + [Map attributes = const {}]) async { + return _platform.getTreatment( + matchingKey: _matchingKey, + bucketingKey: _bucketingKey, + splitName: featureFlagName, + attributes: attributes); + } + + @override + Future getTreatmentWithConfig(String featureFlagName, + [Map attributes = const {}]) async { + return _platform.getTreatmentWithConfig( + matchingKey: _matchingKey, + bucketingKey: _bucketingKey, + splitName: featureFlagName, + attributes: attributes); + } + + @override + Future> getTreatments(List featureFlagNames, + [Map attributes = const {}]) async { + return _platform.getTreatments( + matchingKey: _matchingKey, + bucketingKey: _bucketingKey, + splitNames: featureFlagNames, + attributes: attributes); + } + + @override + Future> getTreatmentsWithConfig( + List featureFlagNames, + [Map attributes = const {}]) async { + return _platform.getTreatmentsWithConfig( + matchingKey: _matchingKey, + bucketingKey: _bucketingKey, + splitNames: featureFlagNames, + attributes: attributes); + } + + @override + Future track(String eventType, + {String? trafficType, + double? value, + Map properties = const {}}) async { + return _platform.track( + matchingKey: _matchingKey, + bucketingKey: _bucketingKey, + eventType: eventType, + trafficType: trafficType, + value: value, + properties: properties); + } + + @override + Future setAttribute(String attributeName, dynamic value) async { + return _platform.setAttribute( + matchingKey: _matchingKey, + bucketingKey: _bucketingKey, + attributeName: attributeName, + value: value); + } + + @override + Future getAttribute(String attributeName) async { + return _platform.getAttribute( + matchingKey: _matchingKey, + bucketingKey: _bucketingKey, + attributeName: attributeName); + } + + @override + Future setAttributes(Map attributes) async { + return _platform.setAttributes( + matchingKey: _matchingKey, + bucketingKey: _bucketingKey, + attributes: attributes); + } + + @override + Future> getAttributes() async { + return _platform.getAllAttributes( + matchingKey: _matchingKey, bucketingKey: _bucketingKey); + } + + @override + Future removeAttribute(String attributeName) async { + return _platform.removeAttribute( + matchingKey: _matchingKey, + bucketingKey: _bucketingKey, + attributeName: attributeName); + } + + @override + Future clearAttributes() async { + return _platform.clearAttributes( + matchingKey: _matchingKey, bucketingKey: _bucketingKey); + } + + @override + Future flush() async { + return _platform.flush( + matchingKey: _matchingKey, bucketingKey: _bucketingKey); + } + + @override + Future destroy() async { + return _platform.destroy( + matchingKey: _matchingKey, bucketingKey: _bucketingKey); + } + + @override + Future whenReady() async { + await _platform.onReady( + matchingKey: _matchingKey, bucketingKey: _bucketingKey); + + return Future.value(this); + } + + @override + Future whenReadyFromCache() async { + await _platform.onReadyFromCache( + matchingKey: _matchingKey, bucketingKey: _bucketingKey); + + return Future.value(this); + } + + @override + Stream whenUpdated() { + return _platform + .onUpdated(matchingKey: _matchingKey, bucketingKey: _bucketingKey) + ?.map((event) => this) ?? + const Stream.empty(); + } + + @override + Future whenTimeout() async { + await _platform.onTimeout( + matchingKey: _matchingKey, bucketingKey: _bucketingKey); + + return Future.value(this); + } +} diff --git a/splitio/lib/splitio.dart b/splitio/lib/splitio.dart new file mode 100644 index 0000000..0c2c2d6 --- /dev/null +++ b/splitio/lib/splitio.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +import 'package:splitio/split_client.dart'; +import 'package:splitio_platform_interface/splitio_platform_interface.dart'; + +export 'package:splitio/split_client.dart'; +export 'package:splitio_platform_interface/split_configuration.dart'; +export 'package:splitio_platform_interface/split_impression.dart'; +export 'package:splitio_platform_interface/split_result.dart'; +export 'package:splitio_platform_interface/split_sync_config.dart'; +export 'package:splitio_platform_interface/split_view.dart'; + +typedef ClientReadinessCallback = void Function(SplitClient splitClient); + +class Splitio { + final String _sdkKey; + + final String _defaultMatchingKey; + + late String? _defaultBucketingKey; + + late SplitConfiguration? _splitConfiguration; + + final SplitioPlatform _platform = SplitioPlatform.instance; + + /// SDK instance constructor. + /// + /// Use [_sdkKey] to specify your Split SDK key. + /// + /// Use [_defaultMatchingKey] to specify a default matching key. The default + /// client will be associated with it. + /// + /// An optional [bucketingKey] can also be specified. + /// + /// Use the optional [configuration] parameter to fine tune configuration options. + Splitio(this._sdkKey, this._defaultMatchingKey, + {String? bucketingKey, SplitConfiguration? configuration}) { + _defaultBucketingKey = bucketingKey; + if (configuration != null) { + _splitConfiguration = configuration; + } else { + _splitConfiguration = SplitConfiguration(); + } + + _init(); + } + + /// Gets a [SplitClient] to interact with the SDK. + /// + /// Optionally provide a [matchingKey], otherwise the key used when creating + /// the SDK instance will be used. + /// + /// An optional [bucketingKey] can also be specified. + /// + /// While this method returns a valid [SplitClient] object, it may not be + /// initialized right away. To listen for specific client readiness events, + /// the optional callback parameters can be used: + /// + /// [onReady] is executed when the most up-to-date information has been + /// retrieved from the Split cloud. + /// + /// [onReadyFromCache] is executed once the SDK has been able to load + /// definitions from cache. This information is not guaranteed to be the most + /// up-to-date, but all the functionality will be available. + /// + /// [onUpdated] is executed when changes have been made, such as creating + /// new feature flags or modifying segments. + /// + /// [onTimeout] is executed if the SDK has not been able to get ready in time. + SplitClient client( + {String? matchingKey, + String? bucketingKey, + ClientReadinessCallback? onReady, + ClientReadinessCallback? onReadyFromCache, + ClientReadinessCallback? onUpdated, + ClientReadinessCallback? onTimeout}) { + String? key = matchingKey ?? _defaultMatchingKey; + _platform.getClient(matchingKey: key, bucketingKey: bucketingKey); + + var client = DefaultSplitClient(_platform, key, bucketingKey); + if (onReady != null) { + _platform + .onReady(matchingKey: key, bucketingKey: bucketingKey) + ?.then((val) => onReady.call(client)); + } + + if (onReadyFromCache != null) { + _platform + .onReadyFromCache(matchingKey: key, bucketingKey: bucketingKey) + ?.then((val) => onReadyFromCache.call(client)); + } + + if (onTimeout != null) { + _platform + .onTimeout(matchingKey: key, bucketingKey: bucketingKey) + ?.then((val) => onTimeout.call(client)); + } + + if (onUpdated != null) { + _platform + .onUpdated(matchingKey: key, bucketingKey: bucketingKey) + ?.listen((event) => onUpdated.call(client)); + } + + return client; + } + + Future> splitNames() async { + List splitNames = await _platform.splitNames(); + + return splitNames; + } + + Future> splits() async { + return _platform.splits(); + } + + /// If the impressionListener configuration has been enabled, + /// generated impressions will be streamed here. + Stream impressionsStream() { + return _platform.impressionsStream(); + } + + Future split(String splitName) async { + return _platform.split(splitName: splitName); + } + + Future getUserConsent() async { + return _platform.getUserConsent(); + } + + Future setUserConsent(bool enabled) async { + return _platform.setUserConsent(enabled); + } + + Future _init() { + return _platform.init( + apiKey: _sdkKey, + matchingKey: _defaultMatchingKey, + bucketingKey: _defaultBucketingKey, + sdkConfiguration: _splitConfiguration); + } +} diff --git a/splitio/pubspec.yaml b/splitio/pubspec.yaml new file mode 100644 index 0000000..8b566fe --- /dev/null +++ b/splitio/pubspec.yaml @@ -0,0 +1,30 @@ +name: splitio +description: Official plugin for split.io, the platform for controlled rollouts, which serves features to your users via feature flags to manage your complete customer experience. +version: 0.1.6 +homepage: https://split.io/ +repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio/ + +environment: + sdk: ">=2.16.2 <4.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + platforms: + android: + default_package: splitio_android + ios: + default_package: splitio_ios + +dependencies: + flutter: + sdk: flutter + splitio_android: ^0.1.6 + splitio_ios: ^0.1.6 + splitio_platform_interface: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + plugin_platform_interface: ^2.1.4 diff --git a/splitio/test/splitio_client_test.dart b/splitio/test/splitio_client_test.dart new file mode 100644 index 0000000..25e638f --- /dev/null +++ b/splitio/test/splitio_client_test.dart @@ -0,0 +1,320 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio/split_client.dart'; +import 'package:splitio_platform_interface/splitio_platform_interface.dart'; + +import 'splitio_platform_stub.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + SplitioPlatformStub _platform = SplitioPlatformStub(); + + SplitClient _getClient([SplitioPlatform? platform]) { + if (platform != null) { + return DefaultSplitClient(platform, 'matching-key', 'bucketing-key'); + } + + return DefaultSplitClient(_platform, 'matching-key', 'bucketing-key'); + } + + setUp(() { + _platform = SplitioPlatformStub(); + }); + + group('evaluation', () { + test('getTreatment without attributes', () async { + SplitClient client = _getClient(); + + client.getTreatment('split'); + + expect(_platform.methodName, 'getTreatment'); + expect(_platform.methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatment with attributes', () async { + SplitClient client = _getClient(); + + client.getTreatment('split', {'attr1': true}); + + expect(_platform.methodName, 'getTreatment'); + expect(_platform.methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatments without attributes', () async { + SplitClient client = _getClient(); + + client.getTreatments(['split1', 'split2']); + + expect(_platform.methodName, 'getTreatments'); + expect(_platform.methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatments with attributes', () async { + SplitClient client = _getClient(); + + client.getTreatments(['split1', 'split2'], {'attr1': true}); + + expect(_platform.methodName, 'getTreatments'); + expect(_platform.methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatmentWithConfig with attributes', () async { + SplitClient client = _getClient(); + + client.getTreatmentWithConfig('split1', {'attr1': true}); + + expect(_platform.methodName, 'getTreatmentWithConfig'); + expect(_platform.methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatmentWithConfig without attributes', () async { + SplitClient client = _getClient(); + + client.getTreatmentWithConfig('split1'); + + expect(_platform.methodName, 'getTreatmentWithConfig'); + expect(_platform.methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatmentsWithConfig without attributes', () async { + SplitClient client = _getClient(); + + client.getTreatmentsWithConfig(['split1', 'split2']); + + expect(_platform.methodName, 'getTreatmentsWithConfig'); + expect(_platform.methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatmentsWithConfig with attributes', () async { + SplitClient client = _getClient(); + + client.getTreatmentsWithConfig(['split1', 'split2'], {'attr1': true}); + + expect(_platform.methodName, 'getTreatmentsWithConfig'); + expect(_platform.methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + }); + + group('track', () { + test('track with traffic type & value', () async { + SplitClient client = _getClient(); + + client.track('my_event', trafficType: 'my_traffic_type', value: 25.10); + expect(_platform.methodName, 'track'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'trafficType': 'my_traffic_type', + 'value': 25.10 + }); + }); + + test('track with value', () async { + SplitClient client = _getClient(); + + client.track('my_event', value: 25.10); + expect(_platform.methodName, 'track'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'value': 25.10 + }); + }); + + test('track with traffic type', () async { + SplitClient client = _getClient(); + + client.track('my_event', trafficType: 'my_traffic_type'); + expect(_platform.methodName, 'track'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'trafficType': 'my_traffic_type', + }); + }); + }); + + group('attributes', () { + test('get single attribute', () async { + SplitClient client = _getClient(); + + client.getAttribute('attribute-name'); + expect(_platform.methodName, 'getAttribute'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'attribute-name', + }); + }); + + test('get all attributes', () async { + SplitClient client = _getClient(); + + client.getAttributes(); + expect(_platform.methodName, 'getAllAttributes'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('set attribute', () async { + SplitClient client = _getClient(); + + client.setAttribute('my_attr', 'attr_value'); + expect(_platform.methodName, 'setAttribute'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'my_attr', + 'value': 'attr_value', + }); + }); + + test('set multiple attributes', () async { + SplitClient client = _getClient(); + + client.setAttributes({ + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', 'two'], + }); + expect(_platform.methodName, 'setAttributes'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': { + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', 'two'], + } + }); + }); + + test('remove attribute', () async { + SplitClient client = _getClient(); + + client.removeAttribute('attr-name'); + expect(_platform.methodName, 'removeAttribute'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'attr-name', + }); + }); + + test('clear attributes', () async { + SplitClient client = _getClient(); + + client.clearAttributes(); + expect(_platform.methodName, 'clearAttributes'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('flush', () async { + SplitClient client = _getClient(); + + client.flush(); + expect(_platform.methodName, 'flush'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('destroy', () { + _platform.destroy( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(_platform.methodName, 'destroy'); + expect(_platform.methodArguments, + {'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key'}); + }); + }); + + group('events', () { + test('onReady', () { + _platform.onReady( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(_platform.methodName, 'onReady'); + expect(_platform.methodArguments, + {'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key'}); + }); + + test('onReadyFromCache', () { + _platform.onReadyFromCache( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(_platform.methodName, 'onReadyFromCache'); + expect(_platform.methodArguments, + {'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key'}); + }); + + test('onTimeout', () { + _platform.onTimeout( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(_platform.methodName, 'onTimeout'); + expect(_platform.methodArguments, + {'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key'}); + }); + + test('onUpdated', () { + _platform.onUpdated( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(_platform.methodName, 'onUpdated'); + expect(_platform.methodArguments, + {'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key'}); + }); + }); +} diff --git a/splitio/test/splitio_platform_stub.dart b/splitio/test/splitio_platform_stub.dart new file mode 100644 index 0000000..369cc83 --- /dev/null +++ b/splitio/test/splitio_platform_stub.dart @@ -0,0 +1,357 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:splitio_platform_interface/splitio_platform_interface.dart'; + +class SplitioPlatformStub + with MockPlatformInterfaceMixin + implements SplitioPlatform { + String methodName = ''; + Map methodArguments = {}; + + @override + Future clearAttributes( + {required String matchingKey, required String? bucketingKey}) { + methodName = 'clearAttributes'; + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey + }; + return Future.value(true); + } + + @override + Future destroy( + {required String matchingKey, required String? bucketingKey}) { + methodName = 'destroy'; + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey + }; + + return Future.value(); + } + + @override + Future flush( + {required String matchingKey, required String? bucketingKey}) { + methodName = 'flush'; + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey + }; + return Future.value(); + } + + @override + Future> getAllAttributes( + {required String matchingKey, required String? bucketingKey}) { + methodName = 'getAllAttributes'; + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey + }; + + return Future.value({}); + } + + @override + Future getAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName}) { + methodName = 'getAttribute'; + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey, + 'attributeName': attributeName + }; + + return Future.value(); + } + + @override + Future getClient( + {required String matchingKey, required String? bucketingKey}) { + methodName = 'getClient'; + + methodArguments = { + 'matchingKey': matchingKey, + }; + + if (bucketingKey != null) { + methodArguments['bucketingKey'] = bucketingKey; + } + + return Future.value(); + } + + @override + Future getTreatment( + {required String matchingKey, + required String? bucketingKey, + required String splitName, + Map attributes = const {}}) { + methodName = 'getTreatment'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey, + 'splitName': splitName, + 'attributes': attributes + }; + + return Future.value(''); + } + + @override + Future getTreatmentWithConfig( + {required String matchingKey, + required String? bucketingKey, + required String splitName, + Map attributes = const {}}) { + methodName = 'getTreatmentWithConfig'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey, + 'splitName': splitName, + 'attributes': attributes, + }; + + return Future.value(const SplitResult('on', null)); + } + + @override + Future> getTreatments( + {required String matchingKey, + required String? bucketingKey, + required List splitNames, + Map attributes = const {}}) { + methodName = 'getTreatments'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey, + 'splitName': splitNames, + 'attributes': attributes, + }; + + return Future.value({}); + } + + @override + Future> getTreatmentsWithConfig( + {required String matchingKey, + required String? bucketingKey, + required List splitNames, + Map attributes = const {}}) { + methodName = 'getTreatmentsWithConfig'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey, + 'splitName': splitNames, + 'attributes': attributes, + }; + + return Future.value({}); + } + + @override + Stream impressionsStream() { + methodName = 'impressionsStream'; + + return const Stream.empty(); + } + + @override + Future init( + {required String apiKey, + required String matchingKey, + required String? bucketingKey, + SplitConfiguration? sdkConfiguration}) { + methodName = 'init'; + + methodArguments = { + 'matchingKey': matchingKey, + 'apiKey': apiKey, + 'sdkConfiguration': sdkConfiguration?.configurationMap ?? {}, + }; + + if (bucketingKey != null) { + methodArguments['bucketingKey'] = bucketingKey; + } + + return Future.value(); + } + + @override + Future? onReady( + {required String matchingKey, required String? bucketingKey}) { + methodName = 'onReady'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey + }; + + return Future.value(); + } + + @override + Future? onReadyFromCache( + {required String matchingKey, required String? bucketingKey}) { + methodName = 'onReadyFromCache'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey + }; + + return Future.value(); + } + + @override + Future? onTimeout( + {required String matchingKey, required String? bucketingKey}) { + methodName = 'onTimeout'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey + }; + + return Future.value(); + } + + @override + Stream? onUpdated( + {required String matchingKey, required String? bucketingKey}) { + methodName = 'onUpdated'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey + }; + + return const Stream.empty(); + } + + @override + Future removeAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName}) { + methodName = 'removeAttribute'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey, + 'attributeName': attributeName + }; + + return Future.value(true); + } + + @override + Future setAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName, + required value}) { + methodName = 'setAttribute'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey, + 'attributeName': attributeName, + 'value': value, + }; + + return Future.value(true); + } + + @override + Future setAttributes( + {required String matchingKey, + required String? bucketingKey, + required Map attributes}) { + methodName = 'setAttributes'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey, + 'attributes': attributes + }; + + return Future.value(true); + } + + @override + Future split({required String splitName}) { + methodName = 'split'; + + methodArguments = {'splitName': splitName}; + + return Future.value(null); + } + + @override + Future> splitNames() { + methodName = 'splitNames'; + + return Future.value([]); + } + + @override + Future> splits() { + methodName = 'splits'; + + return Future.value([]); + } + + @override + Future track( + {required String matchingKey, + required String? bucketingKey, + required String eventType, + String? trafficType, + double? value, + Map properties = const {}}) { + methodName = 'track'; + + methodArguments = { + 'matchingKey': matchingKey, + 'bucketingKey': bucketingKey, + 'eventType': eventType + }; + + if (properties.isNotEmpty) { + methodArguments['properties'] = properties; + } + + if (trafficType != null) { + methodArguments['trafficType'] = trafficType; + } + + if (value != null) { + methodArguments['value'] = value; + } + + return Future.value(true); + } + + @override + Future getUserConsent() { + methodName = 'getUserConsent'; + + return Future.value(UserConsent.granted); + } + + @override + Future setUserConsent(bool enabled) { + methodName = 'setUserConsent'; + methodArguments['value'] = enabled; + + return Future.value(null); + } +} diff --git a/test/splitio_test.dart b/splitio/test/splitio_test.dart similarity index 50% rename from test/splitio_test.dart rename to splitio/test/splitio_test.dart index 6d46e08..e6627f2 100644 --- a/test/splitio_test.dart +++ b/splitio/test/splitio_test.dart @@ -1,51 +1,34 @@ -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:splitio/splitio.dart'; +import 'package:splitio_platform_interface/splitio_platform_interface.dart'; -void main() { - const MethodChannel channel = MethodChannel('splitio'); - - var methodArguments; - var methodName; +import 'splitio_platform_stub.dart'; +void main() { TestWidgetsFlutterBinding.ensureInitialized(); - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - methodName = methodCall.method; - methodArguments = methodCall.arguments; - - if (methodCall.method == 'splitNames') { - List emptyList = []; - return Future.value(emptyList); - } - }); - }); - - tearDown(() async { - methodName = null; - methodArguments = null; - }); + SplitioPlatformStub _platform = SplitioPlatformStub(); + SplitioPlatform.instance = _platform; group('initialization', () { test('init with matching key only', () { Splitio('api-key', 'matching-key'); - expect(methodName, 'init'); - expect(methodArguments, { + expect(_platform.methodName, 'init'); + expect(_platform.methodArguments, { 'apiKey': 'api-key', 'matchingKey': 'matching-key', - 'sdkConfiguration': {} + 'sdkConfiguration': {'readyTimeout': 10} }); }); test('init with bucketing key', () { Splitio('api-key', 'matching-key', bucketingKey: 'bucketing-key'); - expect(methodName, 'init'); - expect(methodArguments, { + expect(_platform.methodName, 'init'); + expect(_platform.methodArguments, { 'apiKey': 'api-key', 'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key', - 'sdkConfiguration': {} + 'sdkConfiguration': {'readyTimeout': 10} }); }); @@ -53,13 +36,13 @@ void main() { Splitio('api-key', 'matching-key', bucketingKey: 'bucketing-key', configuration: - SplitConfiguration(enableDebug: true, streamingEnabled: false)); - expect(methodName, 'init'); - expect(methodArguments, { + SplitConfiguration(logLevel: SplitLogLevel.debug, streamingEnabled: false)); + expect(_platform.methodName, 'init'); + expect(_platform.methodArguments, { 'apiKey': 'api-key', 'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key', - 'sdkConfiguration': {'enableDebug': true, 'streamingEnabled': false}, + 'sdkConfiguration': {'logLevel': 'debug', 'streamingEnabled': false, 'readyTimeout': 10}, }); }); }); @@ -70,8 +53,8 @@ void main() { splitio.client(); - expect(methodName, 'getClient'); - expect(methodArguments, {'matchingKey': 'matching-key'}); + expect(_platform.methodName, 'getClient'); + expect(_platform.methodArguments, {'matchingKey': 'matching-key'}); }); test('get client with new matching key', () { @@ -79,8 +62,8 @@ void main() { splitio.client(matchingKey: 'new-matching-key'); - expect(methodName, 'getClient'); - expect(methodArguments, {'matchingKey': 'new-matching-key'}); + expect(_platform.methodName, 'getClient'); + expect(_platform.methodArguments, {'matchingKey': 'new-matching-key'}); }); test('get client with new matching key and bucketing key', () { @@ -89,8 +72,8 @@ void main() { splitio.client( matchingKey: 'new-matching-key', bucketingKey: 'bucketing-key'); - expect(methodName, 'getClient'); - expect(methodArguments, + expect(_platform.methodName, 'getClient'); + expect(_platform.methodArguments, {'matchingKey': 'new-matching-key', 'bucketingKey': 'bucketing-key'}); }); }); @@ -100,22 +83,44 @@ void main() { var splitio = Splitio('api-key', 'matching-key'); splitio.splitNames(); - expect(methodName, 'splitNames'); + expect(_platform.methodName, 'splitNames'); }); test('get splits', () { var splitio = Splitio('api-key', 'matching-key'); splitio.splits(); - expect(methodName, 'splits'); + expect(_platform.methodName, 'splits'); }); test('get split', () { var splitio = Splitio('api-key', 'matching-key'); splitio.split('my_split'); - expect(methodName, 'split'); - expect(methodArguments, {'splitName': 'my_split'}); + expect(_platform.methodName, 'split'); + expect(_platform.methodArguments, {'splitName': 'my_split'}); + }); + }); + + group('userConsent', () { + test('get user consent', () async { + var splitio = Splitio('api-key', 'matching-key'); + splitio.getUserConsent(); + + expect(_platform.methodName, 'getUserConsent'); + }); + + test('set user consent', () async { + var splitio = Splitio('api-key', 'matching-key'); + splitio.setUserConsent(true); + + expect(_platform.methodName, 'setUserConsent'); + expect(_platform.methodArguments, { + 'matchingKey': 'matching-key', + 'apiKey': 'api-key', + 'sdkConfiguration': {'readyTimeout': 10}, + 'value': true, + }); }); }); } diff --git a/splitio_android/.gitignore b/splitio_android/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/splitio_android/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/splitio_android/CHANGELOG.md b/splitio_android/CHANGELOG.md new file mode 100644 index 0000000..ce894f5 --- /dev/null +++ b/splitio_android/CHANGELOG.md @@ -0,0 +1,40 @@ +# 0.1.6 (Aug 15, 2023) + +* Added `readyTimeout` configuration option. + +# 0.1.6-rc.1 (Aug 15, 2023) + +# 0.1.5 (Jul 19, 2023) + +* Updated Android SDK to `3.3.0`, which includes: + * Improved streaming architecture implementation to apply feature flag updates from the notification received which is now enhanced, improving efficiency and reliability of the whole update system. + * Added logic to do a full check of feature flags immediately when the app comes back to foreground, limited to once per minute. + +# 0.1.5-rc.1 (Jul 19, 2023) + +# 0.1.4 (May 23, 2023) + +* Added `setUserConsent` method. +* Added `getUserConsent` method. + +# 0.1.4-rc.1 (May 23, 2023) + +# 0.1.3 (May 18, 2023) + +* Updated Android SDK to `3.2.1` +* Added support for new configuration options: + * `impressionsMode` + * `syncEnabled` + * `logLevel` + * `userConsent` + * `encryptionEnabled` + +# 0.1.3-rc.1 (May 18, 2023) + +# 0.1.2+1 (Dec 7, 2022) + +Added Consumer ProGuard rules. + +# 0.1.2 (Aug 13, 2022) + +Splits from `splitio` as federated implementation. diff --git a/splitio_android/LICENSE b/splitio_android/LICENSE new file mode 100644 index 0000000..54326b0 --- /dev/null +++ b/splitio_android/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright © 2023 Split Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/splitio_android/README.md b/splitio_android/README.md new file mode 100644 index 0000000..54614af --- /dev/null +++ b/splitio_android/README.md @@ -0,0 +1,11 @@ +# splitio\_android + +The Android implementation of [`splitio`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `splitio` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/splitio +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/splitio_android/analysis_options.yaml b/splitio_android/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/splitio_android/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/splitio_android/android/.gitignore similarity index 100% rename from android/.gitignore rename to splitio_android/android/.gitignore diff --git a/android/build.gradle b/splitio_android/android/build.gradle similarity index 86% rename from android/build.gradle rename to splitio_android/android/build.gradle index 5916d7c..7a017ac 100644 --- a/android/build.gradle +++ b/splitio_android/android/build.gradle @@ -31,11 +31,12 @@ android { defaultConfig { minSdkVersion 16 + + consumerProguardFiles 'split-proguard-rules.pro' } dependencies { - implementation 'io.split.client:android-client:2.12.0' - implementation 'androidx.work:work-runtime:2.7.1' + implementation 'io.split.client:android-client:3.3.0' implementation 'com.squareup.okhttp3:okhttp:3.12.13' testImplementation 'junit:junit:4.13.2' diff --git a/splitio_android/android/gradle/wrapper/gradle-wrapper.jar b/splitio_android/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/splitio_android/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/splitio_android/android/gradle/wrapper/gradle-wrapper.properties b/splitio_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8049c68 --- /dev/null +++ b/splitio_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/splitio_android/android/gradlew b/splitio_android/android/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/splitio_android/android/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/splitio_android/android/gradlew.bat b/splitio_android/android/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/splitio_android/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/splitio_android/android/settings.gradle similarity index 100% rename from android/settings.gradle rename to splitio_android/android/settings.gradle diff --git a/splitio_android/android/split-proguard-rules.pro b/splitio_android/android/split-proguard-rules.pro new file mode 100644 index 0000000..8dc624d --- /dev/null +++ b/splitio_android/android/split-proguard-rules.pro @@ -0,0 +1,54 @@ +# Please include these rules in your project +# in order to make Split code work properly when +# using proguard +-dontwarn android.test.** +-dontwarn org.junit.** +-dontwarn com.google.common.** +-keep class io.split.android.client.utils.deserializer.EventDeserializer { *; } +-keep class io.split.android.client.dtos.** { *; } +-keep class io.split.android.client.storage.db.** { *; } +-keep class io.split.android.client.service.sseclient.EventStreamParser { *; } +-keep class io.split.android.client.service.sseclient.SseAuthToken { *; } +-keep class io.split.android.client.service.sseclient.SseJwtToken { *; } +-keep class io.split.android.client.service.sseclient.SseAuthenticationResponse { *; } +-keep class io.split.android.client.service.sseclient.notifications.** { *; } +-keepattributes Signature +-keep class com.google.gson.reflect.TypeToken { *; } +-keep class * extends com.google.gson.reflect.TypeToken +-dontwarn java.beans.BeanInfo +-dontwarn java.beans.FeatureDescriptor +-dontwarn java.beans.IntrospectionException +-dontwarn java.beans.Introspector +-dontwarn java.beans.PropertyDescriptor + +##---------------Begin: proguard configuration for Gson ---------- +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Application classes that will be serialized/deserialized over Gson +-keep class com.google.gson.examples.android.model.** { ; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken +##---------------End: proguard configuration for Gson ---------- diff --git a/android/src/main/AndroidManifest.xml b/splitio_android/android/src/main/AndroidManifest.xml similarity index 100% rename from android/src/main/AndroidManifest.xml rename to splitio_android/android/src/main/AndroidManifest.xml diff --git a/android/src/main/java/io/split/splitio/ArgumentParser.java b/splitio_android/android/src/main/java/io/split/splitio/ArgumentParser.java similarity index 100% rename from android/src/main/java/io/split/splitio/ArgumentParser.java rename to splitio_android/android/src/main/java/io/split/splitio/ArgumentParser.java diff --git a/android/src/main/java/io/split/splitio/ArgumentParserImpl.java b/splitio_android/android/src/main/java/io/split/splitio/ArgumentParserImpl.java similarity index 100% rename from android/src/main/java/io/split/splitio/ArgumentParserImpl.java rename to splitio_android/android/src/main/java/io/split/splitio/ArgumentParserImpl.java diff --git a/android/src/main/java/io/split/splitio/AttributesWrapper.java b/splitio_android/android/src/main/java/io/split/splitio/AttributesWrapper.java similarity index 100% rename from android/src/main/java/io/split/splitio/AttributesWrapper.java rename to splitio_android/android/src/main/java/io/split/splitio/AttributesWrapper.java diff --git a/android/src/main/java/io/split/splitio/Constants.java b/splitio_android/android/src/main/java/io/split/splitio/Constants.java similarity index 94% rename from android/src/main/java/io/split/splitio/Constants.java rename to splitio_android/android/src/main/java/io/split/splitio/Constants.java index 461a553..35ceb63 100644 --- a/android/src/main/java/io/split/splitio/Constants.java +++ b/splitio_android/android/src/main/java/io/split/splitio/Constants.java @@ -26,6 +26,8 @@ static class Method { static final String SPLIT_NAMES = "splitNames"; static final String SPLIT = "split"; static final String IMPRESSION_LOG = "impressionLog"; + static final String GET_USER_CONSENT = "getUserConsent"; + static final String SET_USER_CONSENT = "setUserConsent"; } static class Argument { diff --git a/android/src/main/java/io/split/splitio/EvaluationWrapper.java b/splitio_android/android/src/main/java/io/split/splitio/EvaluationWrapper.java similarity index 100% rename from android/src/main/java/io/split/splitio/EvaluationWrapper.java rename to splitio_android/android/src/main/java/io/split/splitio/EvaluationWrapper.java diff --git a/android/src/main/java/io/split/splitio/Helper.java b/splitio_android/android/src/main/java/io/split/splitio/Helper.java similarity index 100% rename from android/src/main/java/io/split/splitio/Helper.java rename to splitio_android/android/src/main/java/io/split/splitio/Helper.java diff --git a/android/src/main/java/io/split/splitio/ImpressionListenerImp.java b/splitio_android/android/src/main/java/io/split/splitio/ImpressionListenerImp.java similarity index 100% rename from android/src/main/java/io/split/splitio/ImpressionListenerImp.java rename to splitio_android/android/src/main/java/io/split/splitio/ImpressionListenerImp.java diff --git a/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java b/splitio_android/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java similarity index 76% rename from android/src/main/java/io/split/splitio/SplitClientConfigHelper.java rename to splitio_android/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java index 962c5bc..80f58cc 100644 --- a/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java +++ b/splitio_android/android/src/main/java/io/split/splitio/SplitClientConfigHelper.java @@ -6,12 +6,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import io.split.android.client.ServiceEndpoints; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFilter; import io.split.android.client.SyncConfig; import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.shared.UserConsent; +import io.split.android.client.utils.logger.SplitLogLevel; class SplitClientConfigHelper { @@ -36,11 +39,17 @@ class SplitClientConfigHelper { private static final String SYNC_CONFIG = "syncConfig"; private static final String SYNC_CONFIG_NAMES = "syncConfigNames"; private static final String SYNC_CONFIG_PREFIXES = "syncConfigPrefixes"; + private static final String IMPRESSIONS_MODE = "impressionsMode"; + private static final String SYNC_ENABLED = "syncEnabled"; + private static final String USER_CONSENT = "userConsent"; + private static final String ENCRYPTION_ENABLED = "encryptionEnabled"; + private static final String LOG_LEVEL = "logLevel"; + private static final String READY_TIMEOUT = "readyTimeout"; /** * Creates a {@link SplitClientConfig} object from a map. * - * @param configurationMap Map of config values. + * @param configurationMap Map of config values. * @param impressionListener Optional ImpressionListener. * @return {@link SplitClientConfig} object. */ @@ -94,7 +103,7 @@ static SplitClientConfig fromMap(@NonNull Map configurationMap, Boolean enableDebug = getBoolean(configurationMap, ENABLE_DEBUG); if (enableDebug != null && enableDebug) { - builder.enableDebug(); + builder.logLevel(SplitLogLevel.DEBUG); } Boolean streamingEnabled = getBoolean(configurationMap, STREAMING_ENABLED); @@ -156,6 +165,54 @@ static SplitClientConfig fromMap(@NonNull Map configurationMap, builder.syncConfig(syncConfigBuilder.build()); } + String impressionsMode = getString(configurationMap, IMPRESSIONS_MODE); + if (impressionsMode != null) { + builder.impressionsMode(impressionsMode); + } + + Boolean syncEnabled = getBoolean(configurationMap, SYNC_ENABLED); + if (syncEnabled != null) { + builder.syncEnabled(syncEnabled); + } + + String userConsent = getString(configurationMap, USER_CONSENT); + if (userConsent != null) { + if ("unknown".equalsIgnoreCase(userConsent)) { + builder.userConsent(UserConsent.UNKNOWN); + } else if ("declined".equalsIgnoreCase(userConsent)) { + builder.userConsent(UserConsent.DECLINED); + } else { + builder.userConsent(UserConsent.GRANTED); + } + } + + Boolean encryptionEnabled = getBoolean(configurationMap, ENCRYPTION_ENABLED); + if (encryptionEnabled != null) { + builder.encryptionEnabled(encryptionEnabled); + } + + String logLevel = getString(configurationMap, LOG_LEVEL); + if (logLevel != null) { + if ("verbose".equalsIgnoreCase(logLevel)) { + builder.logLevel(SplitLogLevel.VERBOSE); + } else if ("debug".equalsIgnoreCase(logLevel)) { + builder.logLevel(SplitLogLevel.DEBUG); + } else if ("info".equalsIgnoreCase(logLevel)) { + builder.logLevel(SplitLogLevel.INFO); + } else if ("warning".equalsIgnoreCase(logLevel)) { + builder.logLevel(SplitLogLevel.WARNING); + } else if ("error".equalsIgnoreCase(logLevel)) { + builder.logLevel(SplitLogLevel.ERROR); + } else { + builder.logLevel(SplitLogLevel.NONE); + } + } + + Integer readyTimeout = getInteger(configurationMap, READY_TIMEOUT); + if (readyTimeout != null) { + builder.ready((int) TimeUnit.SECONDS.toMillis(readyTimeout)); // Android SDK uses this parameter in millis + } + return builder.serviceEndpoints(serviceEndpointsBuilder.build()).build(); } diff --git a/android/src/main/java/io/split/splitio/SplitFactoryProvider.java b/splitio_android/android/src/main/java/io/split/splitio/SplitFactoryProvider.java similarity index 100% rename from android/src/main/java/io/split/splitio/SplitFactoryProvider.java rename to splitio_android/android/src/main/java/io/split/splitio/SplitFactoryProvider.java diff --git a/android/src/main/java/io/split/splitio/SplitFactoryProviderImpl.java b/splitio_android/android/src/main/java/io/split/splitio/SplitFactoryProviderImpl.java similarity index 100% rename from android/src/main/java/io/split/splitio/SplitFactoryProviderImpl.java rename to splitio_android/android/src/main/java/io/split/splitio/SplitFactoryProviderImpl.java diff --git a/android/src/main/java/io/split/splitio/SplitMethodParser.java b/splitio_android/android/src/main/java/io/split/splitio/SplitMethodParser.java similarity index 100% rename from android/src/main/java/io/split/splitio/SplitMethodParser.java rename to splitio_android/android/src/main/java/io/split/splitio/SplitMethodParser.java diff --git a/android/src/main/java/io/split/splitio/SplitMethodParserImpl.java b/splitio_android/android/src/main/java/io/split/splitio/SplitMethodParserImpl.java similarity index 97% rename from android/src/main/java/io/split/splitio/SplitMethodParserImpl.java rename to splitio_android/android/src/main/java/io/split/splitio/SplitMethodParserImpl.java index d3c7f1a..fb3ad5d 100644 --- a/android/src/main/java/io/split/splitio/SplitMethodParserImpl.java +++ b/splitio_android/android/src/main/java/io/split/splitio/SplitMethodParserImpl.java @@ -27,10 +27,12 @@ import static io.split.splitio.Constants.Method.GET_TREATMENTS; import static io.split.splitio.Constants.Method.GET_TREATMENTS_WITH_CONFIG; import static io.split.splitio.Constants.Method.GET_TREATMENT_WITH_CONFIG; +import static io.split.splitio.Constants.Method.GET_USER_CONSENT; import static io.split.splitio.Constants.Method.INIT; import static io.split.splitio.Constants.Method.REMOVE_ATTRIBUTE; import static io.split.splitio.Constants.Method.SET_ATTRIBUTE; import static io.split.splitio.Constants.Method.SET_ATTRIBUTES; +import static io.split.splitio.Constants.Method.SET_USER_CONSENT; import static io.split.splitio.Constants.Method.SPLIT; import static io.split.splitio.Constants.Method.SPLITS; import static io.split.splitio.Constants.Method.SPLIT_NAMES; @@ -195,6 +197,13 @@ public void onMethodCall(String methodName, Object arguments, @NonNull MethodCha case SPLIT: result.success(getSplitViewAsMap(mSplitWrapper.split(mArgumentParser.getStringArgument(SPLIT_NAME, arguments)))); break; + case GET_USER_CONSENT: + result.success(mSplitWrapper.getUserConsent()); + break; + case SET_USER_CONSENT: + mSplitWrapper.setUserConsent(mArgumentParser.getBooleanArgument(VALUE, arguments)); + result.success(null); + break; default: result.notImplemented(); break; diff --git a/android/src/main/java/io/split/splitio/SplitProviderHelper.java b/splitio_android/android/src/main/java/io/split/splitio/SplitProviderHelper.java similarity index 100% rename from android/src/main/java/io/split/splitio/SplitProviderHelper.java rename to splitio_android/android/src/main/java/io/split/splitio/SplitProviderHelper.java diff --git a/android/src/main/java/io/split/splitio/SplitWrapper.java b/splitio_android/android/src/main/java/io/split/splitio/SplitWrapper.java similarity index 91% rename from android/src/main/java/io/split/splitio/SplitWrapper.java rename to splitio_android/android/src/main/java/io/split/splitio/SplitWrapper.java index f1b9d4b..54faa28 100644 --- a/android/src/main/java/io/split/splitio/SplitWrapper.java +++ b/splitio_android/android/src/main/java/io/split/splitio/SplitWrapper.java @@ -24,4 +24,8 @@ interface SplitWrapper extends EvaluationWrapper, AttributesWrapper { @Nullable SplitView split(String splitName); + + String getUserConsent(); + + void setUserConsent(boolean enabled); } diff --git a/android/src/main/java/io/split/splitio/SplitWrapperImpl.java b/splitio_android/android/src/main/java/io/split/splitio/SplitWrapperImpl.java similarity index 93% rename from android/src/main/java/io/split/splitio/SplitWrapperImpl.java rename to splitio_android/android/src/main/java/io/split/splitio/SplitWrapperImpl.java index ec90e0b..b445908 100644 --- a/android/src/main/java/io/split/splitio/SplitWrapperImpl.java +++ b/splitio_android/android/src/main/java/io/split/splitio/SplitWrapperImpl.java @@ -15,6 +15,7 @@ import io.split.android.client.SplitResult; import io.split.android.client.api.Key; import io.split.android.client.api.SplitView; +import io.split.android.client.shared.UserConsent; import io.split.android.client.utils.ConcurrentSet; import io.split.android.grammar.Treatments; @@ -222,4 +223,21 @@ public List splits() { public SplitView split(String splitName) { return mSplitFactory.manager().split(splitName); } + + @Override + public String getUserConsent() { + UserConsent userConsent = mSplitFactory.getUserConsent(); + if (userConsent == UserConsent.GRANTED) { + return "granted"; + } else if (userConsent == UserConsent.DECLINED) { + return "declined"; + } else { + return "unknown"; + } + } + + @Override + public void setUserConsent(boolean enabled) { + mSplitFactory.setUserConsent(enabled); + } } diff --git a/android/src/main/java/io/split/splitio/SplitioPlugin.java b/splitio_android/android/src/main/java/io/split/splitio/SplitioPlugin.java similarity index 97% rename from android/src/main/java/io/split/splitio/SplitioPlugin.java rename to splitio_android/android/src/main/java/io/split/splitio/SplitioPlugin.java index a2645d0..372c399 100644 --- a/android/src/main/java/io/split/splitio/SplitioPlugin.java +++ b/splitio_android/android/src/main/java/io/split/splitio/SplitioPlugin.java @@ -23,7 +23,7 @@ public class SplitioPlugin implements FlutterPlugin, MethodCallHandler { @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { - channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "splitio"); + channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "split.io/splitio_android"); channel.setMethodCallHandler(this); SplitFactoryProvider provider = getSplitFactoryProvider(flutterPluginBinding.getApplicationContext()); methodParser = new SplitMethodParserImpl(flutterPluginBinding.getApplicationContext(), channel, provider); diff --git a/android/src/test/java/io/split/splitio/ArgumentParserImplTest.java b/splitio_android/android/src/test/java/io/split/splitio/ArgumentParserImplTest.java similarity index 100% rename from android/src/test/java/io/split/splitio/ArgumentParserImplTest.java rename to splitio_android/android/src/test/java/io/split/splitio/ArgumentParserImplTest.java diff --git a/android/src/test/java/io/split/splitio/ImpressionListenerImpTest.java b/splitio_android/android/src/test/java/io/split/splitio/ImpressionListenerImpTest.java similarity index 100% rename from android/src/test/java/io/split/splitio/ImpressionListenerImpTest.java rename to splitio_android/android/src/test/java/io/split/splitio/ImpressionListenerImpTest.java diff --git a/splitio_android/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java b/splitio_android/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java new file mode 100644 index 0000000..aa9b7f4 --- /dev/null +++ b/splitio_android/android/src/test/java/io/split/splitio/SplitClientConfigHelperTest.java @@ -0,0 +1,154 @@ +package io.split.splitio; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFilter; +import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.service.impressions.ImpressionsMode; +import io.split.android.client.shared.UserConsent; +import io.split.android.client.utils.logger.SplitLogLevel; + +public class SplitClientConfigHelperTest { + + @Test + public void configValuesAreMappedCorrectly() { + Map configValues = new HashMap<>(); + + configValues.put("featuresRefreshRate", 80000); + configValues.put("segmentsRefreshRate", 70000); + configValues.put("impressionsRefreshRate", 60000); + configValues.put("telemetryRefreshRate", 2000); + configValues.put("eventsQueueSize", 3999); + configValues.put("impressionsQueueSize", 2999); + configValues.put("eventFlushInterval", 40000); + configValues.put("eventsPerPush", 5000); + configValues.put("trafficType", "none"); + configValues.put("enableDebug", true); + configValues.put("streamingEnabled", true); + configValues.put("persistentAttributesEnabled", true); + configValues.put("sdkEndpoint", "apiEndpoint.split.io"); + configValues.put("eventsEndpoint", "eventsEndpoint.split.io"); + configValues.put("authServiceEndpoint", "sseAuthServiceEndpoint.split.io"); + configValues.put("streamingServiceEndpoint", "streamingServiceEndpoint.split.io"); + configValues.put("telemetryServiceEndpoint", "telemetryServiceEndpoint.split.io"); + Map> syncConfigMap = new HashMap<>(); + syncConfigMap.put("syncConfigNames", Arrays.asList("split1", "split2")); + syncConfigMap.put("syncConfigPrefixes", Arrays.asList("split_", "my_split_")); + configValues.put("syncConfig", syncConfigMap); + configValues.put("impressionsMode", "none"); + configValues.put("syncEnabled", false); + configValues.put("userConsent", "declined"); + configValues.put("encryptionEnabled", true); + configValues.put("logLevel", "verbose"); + + SplitClientConfig splitClientConfig = SplitClientConfigHelper + .fromMap(configValues, mock(ImpressionListener.class)); + + assertEquals(80000, splitClientConfig.featuresRefreshRate()); + assertEquals(70000, splitClientConfig.segmentsRefreshRate()); + assertEquals(60000, splitClientConfig.impressionsRefreshRate()); + assertEquals(2000, splitClientConfig.telemetryRefreshRate()); + assertEquals(3999, splitClientConfig.eventsQueueSize()); + assertEquals(2999, splitClientConfig.impressionsQueueSize()); + assertEquals(40000, splitClientConfig.eventFlushInterval()); + assertEquals(5000, splitClientConfig.eventsPerPush()); + assertEquals("none", splitClientConfig.trafficType()); + assertTrue(splitClientConfig.streamingEnabled()); + assertTrue(splitClientConfig.persistentAttributesEnabled()); + assertEquals("apiEndpoint.split.io", splitClientConfig.endpoint()); + assertEquals("eventsEndpoint.split.io", splitClientConfig.eventsEndpoint()); + assertEquals("sseAuthServiceEndpoint.split.io", splitClientConfig.authServiceUrl()); + assertEquals("streamingServiceEndpoint.split.io", splitClientConfig.streamingServiceUrl()); + assertEquals("telemetryServiceEndpoint.split.io", splitClientConfig.telemetryEndpoint()); + assertEquals(Arrays.asList("split1", "split2"), splitClientConfig.syncConfig().getFilters().get(0).getValues()); + assertEquals(SplitFilter.Type.BY_NAME, splitClientConfig.syncConfig().getFilters().get(0).getType()); + assertEquals(Arrays.asList("split_", "my_split_"), splitClientConfig.syncConfig().getFilters().get(1).getValues()); + assertEquals(SplitFilter.Type.BY_PREFIX, splitClientConfig.syncConfig().getFilters().get(1).getType()); + assertEquals(ImpressionsMode.NONE, splitClientConfig.impressionsMode()); + assertFalse(splitClientConfig.syncEnabled()); + assertEquals(UserConsent.DECLINED, splitClientConfig.userConsent()); + assertTrue(splitClientConfig.encryptionEnabled()); + assertEquals(SplitLogLevel.VERBOSE, splitClientConfig.logLevel()); + } + + @Test + public void enableDebugLogLevelIsMappedCorrectly() { + Map configValues = new HashMap<>(); + + configValues.put("enableDebug", true); + + SplitClientConfig splitClientConfig = SplitClientConfigHelper + .fromMap(configValues, mock(ImpressionListener.class)); + + assertEquals(SplitLogLevel.DEBUG, splitClientConfig.logLevel()); + } + + @Test + public void logLevelsAreMappedCorrectly() { + Map verboseConfigValues = Collections.singletonMap("logLevel", "verbose"); + Map debugConfigValues = Collections.singletonMap("logLevel", "debug"); + Map infoConfigValues = Collections.singletonMap("logLevel", "info"); + Map warnConfigValues = Collections.singletonMap("logLevel", "warning"); + Map errorConfigValues = Collections.singletonMap("logLevel", "error"); + Map noneConfigValues = Collections.singletonMap("logLevel", "none"); + + SplitClientConfig verboseConfig = SplitClientConfigHelper + .fromMap(verboseConfigValues, mock(ImpressionListener.class)); + SplitClientConfig debugConfig = SplitClientConfigHelper.fromMap(debugConfigValues, mock(ImpressionListener.class)); + SplitClientConfig infoConfig = SplitClientConfigHelper.fromMap(infoConfigValues, mock(ImpressionListener.class)); + SplitClientConfig warnConfig = SplitClientConfigHelper.fromMap(warnConfigValues, mock(ImpressionListener.class)); + SplitClientConfig errorConfig = SplitClientConfigHelper.fromMap(errorConfigValues, mock(ImpressionListener.class)); + SplitClientConfig noneConfig = SplitClientConfigHelper.fromMap(noneConfigValues, mock(ImpressionListener.class)); + + assertEquals(SplitLogLevel.VERBOSE, verboseConfig.logLevel()); + assertEquals(SplitLogLevel.DEBUG, debugConfig.logLevel()); + assertEquals(SplitLogLevel.INFO, infoConfig.logLevel()); + assertEquals(SplitLogLevel.WARNING, warnConfig.logLevel()); + assertEquals(SplitLogLevel.ERROR, errorConfig.logLevel()); + assertEquals(SplitLogLevel.NONE, noneConfig.logLevel()); + } + + @Test + public void userConsentIsMappedCorrectly() { + SplitClientConfig grantedConfig = SplitClientConfigHelper + .fromMap(Collections.singletonMap("userConsent", "granted"), mock(ImpressionListener.class)); + SplitClientConfig unknownConfig = SplitClientConfigHelper + .fromMap(Collections.singletonMap("userConsent", "unknown"), mock(ImpressionListener.class)); + SplitClientConfig declinedConfig = SplitClientConfigHelper + .fromMap(Collections.singletonMap("userConsent", "declined"), mock(ImpressionListener.class)); + SplitClientConfig anyConfig = SplitClientConfigHelper + .fromMap(Collections.singletonMap("userConsent", "any"), mock(ImpressionListener.class)); + + assertEquals(UserConsent.GRANTED, grantedConfig.userConsent()); + assertEquals(UserConsent.UNKNOWN, unknownConfig.userConsent()); + assertEquals(UserConsent.DECLINED, declinedConfig.userConsent()); + assertEquals(UserConsent.GRANTED, anyConfig.userConsent()); + } + + @Test + public void impressionsModeValuesAreMappedCorrectly() { + SplitClientConfig debugConfig = SplitClientConfigHelper + .fromMap(Collections.singletonMap("impressionsMode", "debug"), mock(ImpressionListener.class)); + SplitClientConfig noneConfig = SplitClientConfigHelper + .fromMap(Collections.singletonMap("impressionsMode", "none"), mock(ImpressionListener.class)); + SplitClientConfig optimizedConfig = SplitClientConfigHelper + .fromMap(Collections.singletonMap("impressionsMode", "optimized"), mock(ImpressionListener.class)); + + assertEquals(ImpressionsMode.DEBUG, debugConfig.impressionsMode()); + assertEquals(ImpressionsMode.NONE, noneConfig.impressionsMode()); + assertEquals(ImpressionsMode.OPTIMIZED, optimizedConfig.impressionsMode()); + } +} diff --git a/android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java b/splitio_android/android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java similarity index 94% rename from android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java rename to splitio_android/android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java index 3245c38..538ea0e 100644 --- a/android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java +++ b/splitio_android/android/src/test/java/io/split/splitio/SplitMethodParserImplTest.java @@ -475,4 +475,33 @@ public void initialization() { eq("bucketing-key"), argThat(splitClientConfig -> splitClientConfig.impressionListener() != null && !splitClientConfig.streamingEnabled())); } + + @Test + public void getUserConsent() { + when(mSplitWrapper.getUserConsent()).thenReturn("granted"); + mMethodParser.onMethodCall("getUserConsent", Collections.emptyMap(), mResult); + + verify(mResult).success("granted"); + verify(mSplitWrapper).getUserConsent(); + } + + @Test + public void setUserConsentEnabled() { + when(mArgumentParser.getBooleanArgument("value", Collections.singletonMap("value", true))).thenReturn(true); + when(mArgumentParser.getBooleanArgument("value", Collections.singletonMap("value", false))).thenReturn(false); + mMethodParser.onMethodCall("setUserConsent", Collections.singletonMap("value", true), mResult); + + verify(mResult).success(null); + verify(mSplitWrapper).setUserConsent(true); + } + + @Test + public void setUserConsentDisabled() { + when(mArgumentParser.getBooleanArgument("value", Collections.singletonMap("value", true))).thenReturn(true); + when(mArgumentParser.getBooleanArgument("value", Collections.singletonMap("value", false))).thenReturn(false); + mMethodParser.onMethodCall("setUserConsent", Collections.singletonMap("value", false), mResult); + + verify(mResult).success(null); + verify(mSplitWrapper).setUserConsent(false); + } } diff --git a/android/src/test/java/io/split/splitio/SplitProviderHelperImplTest.java b/splitio_android/android/src/test/java/io/split/splitio/SplitProviderHelperImplTest.java similarity index 100% rename from android/src/test/java/io/split/splitio/SplitProviderHelperImplTest.java rename to splitio_android/android/src/test/java/io/split/splitio/SplitProviderHelperImplTest.java diff --git a/android/src/test/java/io/split/splitio/SplitWrapperImplTest.java b/splitio_android/android/src/test/java/io/split/splitio/SplitWrapperImplTest.java similarity index 94% rename from android/src/test/java/io/split/splitio/SplitWrapperImplTest.java rename to splitio_android/android/src/test/java/io/split/splitio/SplitWrapperImplTest.java index ceb5572..200f785 100644 --- a/android/src/test/java/io/split/splitio/SplitWrapperImplTest.java +++ b/splitio_android/android/src/test/java/io/split/splitio/SplitWrapperImplTest.java @@ -22,6 +22,7 @@ import io.split.android.client.SplitFactory; import io.split.android.client.SplitManager; import io.split.android.client.api.Key; +import io.split.android.client.shared.UserConsent; public class SplitWrapperImplTest { @@ -277,4 +278,19 @@ public void testSplit() { verify(managerMock).split("my_split"); } + + @Test + public void testGetUserConsent() { + when(mSplitFactory.getUserConsent()).thenReturn(UserConsent.DECLINED); + String userConsent = mSplitWrapper.getUserConsent(); + + verify(mSplitFactory).getUserConsent(); + assertEquals("declined", userConsent); + } + + @Test + public void testSetUserConsent() { + mSplitWrapper.setUserConsent(true); + verify(mSplitFactory).setUserConsent(true); + } } diff --git a/splitio_android/lib/splitio_android.dart b/splitio_android/lib/splitio_android.dart new file mode 100644 index 0000000..bdffa45 --- /dev/null +++ b/splitio_android/lib/splitio_android.dart @@ -0,0 +1,15 @@ +import 'package:flutter/services.dart'; +import 'package:splitio_platform_interface/splitio_platform_interface.dart'; + +const MethodChannel _methodChannel = MethodChannel('split.io/splitio_android'); + +/// Implementation for Android of [SplitioPlatform]. +class SplitioAndroid extends MethodChannelPlatform { + /// Registers this class as the default platform implementation. + static void registerWith() { + SplitioPlatform.instance = SplitioAndroid(); + } + + @override + MethodChannel get methodChannel => _methodChannel; +} diff --git a/splitio_android/pubspec.yaml b/splitio_android/pubspec.yaml new file mode 100644 index 0000000..3381173 --- /dev/null +++ b/splitio_android/pubspec.yaml @@ -0,0 +1,27 @@ +name: splitio_android +description: The official Android implementation of splitio Flutter plugin. +repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio_android +version: 0.1.6 + +environment: + sdk: ">=2.16.2 <4.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: splitio + platforms: + android: + package: io.split.splitio + pluginClass: SplitioPlugin + dartPluginClass: SplitioAndroid + +dependencies: + flutter: + sdk: flutter + splitio_platform_interface: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 diff --git a/splitio_android/test/splitio_android_test.dart b/splitio_android/test/splitio_android_test.dart new file mode 100644 index 0000000..7cc1f68 --- /dev/null +++ b/splitio_android/test/splitio_android_test.dart @@ -0,0 +1,540 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_android/splitio_android.dart'; +import 'package:splitio_platform_interface/split_configuration.dart'; + +void main() { + const MethodChannel _channel = MethodChannel('split.io/splitio_android'); + + String methodName = ''; + dynamic methodArguments; + + TestWidgetsFlutterBinding.ensureInitialized(); + + SplitioAndroid _platform = SplitioAndroid(); + + void _simulateMethodInvocation(String methodName, + {String key = 'key', + String? bucketingKey, + Map? arguments}) { + arguments ??= {}; + arguments.addAll({'matchingKey': key, 'bucketingKey': bucketingKey}); + _channel.invokeMethod(methodName, arguments); + } + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_channel, (MethodCall methodCall) async { + methodName = methodCall.method; + methodArguments = methodCall.arguments; + + _platform.handle(methodCall); + switch (methodCall.method) { + case 'getTreatment': + return ''; + case 'getTreatments': + return {'split1': 'on', 'split2': 'off'}; + case 'getTreatmentsWithConfig': + return { + 'split1': {'treatment': 'on', 'config': null}, + 'split2': {'treatment': 'off', 'config': null} + }; + case 'track': + return true; + case 'getAttribute': + return true; + case 'getAllAttributes': + return { + 'attr_1': true, + 'attr_2': ['list-element'], + 'attr_3': 28.20 + }; + case 'setAttribute': + case 'setAttributes': + case 'removeAttribute': + case 'clearAttributes': + return true; + } + return null; + }); + }); + + group('evaluation', () { + test('getTreatment without attributes', () async { + _platform.getTreatment( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split'); + + expect(methodName, 'getTreatment'); + expect(methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatment with attributes', () async { + _platform.getTreatment( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split', + attributes: {'attr1': true}); + + expect(methodName, 'getTreatment'); + expect(methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatments without attributes', () async { + _platform.getTreatments( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2']); + + expect(methodName, 'getTreatments'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatments with attributes', () async { + _platform.getTreatments( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + attributes: {'attr1': true}); + + expect(methodName, 'getTreatments'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatmentWithConfig with attributes', () async { + _platform.getTreatmentWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split1', + attributes: {'attr1': true}); + + expect(methodName, 'getTreatmentWithConfig'); + expect(methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatmentWithConfig without attributes', () async { + _platform.getTreatmentWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split1'); + + expect(methodName, 'getTreatmentWithConfig'); + expect(methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatmentsWithConfig without attributes', () async { + _platform.getTreatmentsWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2']); + + expect(methodName, 'getTreatmentsWithConfig'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatmentsWithConfig with attributes', () async { + _platform.getTreatmentsWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + attributes: {'attr1': true}); + + expect(methodName, 'getTreatmentsWithConfig'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + }); + + group('track', () { + test('track with traffic type & value', () async { + _platform.track( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + eventType: 'my_event', + trafficType: 'my_traffic_type', + value: 25.10); + expect(methodName, 'track'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'trafficType': 'my_traffic_type', + 'value': 25.10 + }); + }); + + test('track with value', () async { + _platform.track( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + eventType: 'my_event', + value: 25.10); + expect(methodName, 'track'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'value': 25.10 + }); + }); + + test('track with traffic type', () async { + _platform.track( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + eventType: 'my_event', + trafficType: 'my_traffic_type'); + expect(methodName, 'track'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'trafficType': 'my_traffic_type', + }); + }); + }); + + group('attributes', () { + test('get single attribute', () async { + _platform.getAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'attribute-name'); + expect(methodName, 'getAttribute'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'attribute-name', + }); + }); + + test('get all attributes', () async { + _platform.getAllAttributes( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'getAllAttributes'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('set attribute', () async { + _platform.setAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'my_attr', + value: 'attr_value'); + expect(methodName, 'setAttribute'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'my_attr', + 'value': 'attr_value', + }); + }); + + test('set multiple attributes', () async { + _platform.setAttributes( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributes: { + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', 'two'], + }); + expect(methodName, 'setAttributes'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': { + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', 'two'], + } + }); + }); + + test('remove attribute', () async { + _platform.removeAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'attr-name'); + expect(methodName, 'removeAttribute'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'attr-name', + }); + }); + + test('clear attributes', () async { + _platform.clearAttributes( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'clearAttributes'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('flush', () async { + _platform.flush( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'flush'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('destroy', () async { + _platform.destroy( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'destroy'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + }); + + group('initialization', () { + test('init with matching key only', () { + _platform.init( + apiKey: 'api-key', matchingKey: 'matching-key', bucketingKey: null); + + expect(methodName, 'init'); + expect(methodArguments, { + 'apiKey': 'api-key', + 'matchingKey': 'matching-key', + 'sdkConfiguration': {} + }); + }); + + test('init with bucketing key', () { + _platform.init( + apiKey: 'api-key', + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key'); + expect(methodName, 'init'); + expect(methodArguments, { + 'apiKey': 'api-key', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'sdkConfiguration': {} + }); + }); + + test('init with config', () { + _platform.init( + apiKey: 'api-key', + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + sdkConfiguration: + SplitConfiguration(logLevel: SplitLogLevel.error, streamingEnabled: false, readyTimeout: 1)); + expect(methodName, 'init'); + expect(methodArguments, { + 'apiKey': 'api-key', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'sdkConfiguration': {'logLevel': 'error', 'streamingEnabled': false, 'readyTimeout': 1}, + }); + }); + }); + + group('client', () { + test('get client with no keys', () { + _platform.getClient(matchingKey: 'matching-key', bucketingKey: null); + + expect(methodName, 'getClient'); + expect(methodArguments, {'matchingKey': 'matching-key'}); + }); + + test('get client with new matching key', () { + _platform.getClient(matchingKey: 'new-matching-key', bucketingKey: null); + + expect(methodName, 'getClient'); + expect(methodArguments, {'matchingKey': 'new-matching-key'}); + }); + + test('get client with new matching key and bucketing key', () { + _platform.getClient( + matchingKey: 'new-matching-key', bucketingKey: 'bucketing-key'); + + expect(methodName, 'getClient'); + expect(methodArguments, + {'matchingKey': 'new-matching-key', 'bucketingKey': 'bucketing-key'}); + }); + }); + + group('manager', () { + test('get split names', () { + _platform.splitNames(); + + expect(methodName, 'splitNames'); + }); + + test('get splits', () { + _platform.splits(); + + expect(methodName, 'splits'); + }); + + test('get split', () { + _platform.split(splitName: 'my_split'); + + expect(methodName, 'split'); + expect(methodArguments, {'splitName': 'my_split'}); + }); + }); + + group('events', () { + test('onReady', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onReady = _platform + .onReady(matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.then((value) => true); + + _simulateMethodInvocation('clientReady', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onReady, completion(equals(true))); + }); + + test('onReadyFromCache', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onReadyFromCache = _platform + .onReadyFromCache( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.then((value) => true); + + _simulateMethodInvocation('clientReadyFromCache', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onReadyFromCache, completion(equals(true))); + }); + + test('onTimeout', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onTimeout = _platform + .onTimeout(matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.then((value) => true); + + _simulateMethodInvocation('clientTimeout', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onTimeout, completion(equals(true))); + }); + + test('onUpdated', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onUpdated = _platform + .onUpdated(matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.first + .then((value) => true); + + _simulateMethodInvocation('clientUpdated', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onUpdated, completion(equals(true))); + }); + }); + + test('impressions', () { + _platform.impressionsStream().listen( + expectAsync1((impression) { + expect(impression.key, 'key'); + expect(impression.bucketingKey, null); + expect(impression.split, 'split'); + expect(impression.treatment, 'treatment'); + expect(impression.time, 3000); + expect(impression.appliedRule, 'appliedRule'); + expect(impression.changeNumber, 200); + expect(impression.attributes, {}); + }), + ); + _simulateMethodInvocation('impressionLog', key: 'matching-key', arguments: { + 'key': 'key', + 'bucketingKey': 'bucketingKey', + 'split': 'split', + 'treatment': 'treatment', + 'time': 3000, + 'appliedRule': 'appliedRule', + 'changeNumber': 200, + 'attributes': {} + }); + }); + + group('userConsent', () { + test('get user consent', () async { + UserConsent userConsent = await _platform.getUserConsent(); + + expect(methodName, 'getUserConsent'); + expect(userConsent, UserConsent.declined); + }); + + test('set user consent enabled', () { + _platform.setUserConsent(true); + + expect(methodName, 'setUserConsent'); + expect(methodArguments, {'enabled': true}); + }); + + test('set user consent disabled', () { + _platform.setUserConsent(false); + + expect(methodName, 'setUserConsent'); + expect(methodArguments, {'enabled': false}); + }); + }); +} diff --git a/splitio_ios/.gitignore b/splitio_ios/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/splitio_ios/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/splitio_ios/CHANGELOG.md b/splitio_ios/CHANGELOG.md new file mode 100644 index 0000000..b84abfb --- /dev/null +++ b/splitio_ios/CHANGELOG.md @@ -0,0 +1,36 @@ +# 0.1.6 (Aug 15, 2023) + +* Added `readyTimeout` configuration option. + +# 0.1.6-rc.1 (Aug 15, 2023) + +# 0.1.5 (Jul 19, 2023) + +* Updated iOS SDK to `2.21.0`, which includes: + * Improved streaming architecture implementation to apply feature flag updates from the notification received which is now enhanced, improving efficiency and reliability of the whole update system. + * Added logic to do a full check of feature flags immediately when the app comes back to foreground, limited to once per minute. + * Updated SplitResult init to be public in order to improve testability. + +# 0.1.5-rc.1 (Jul 19, 2023) + +# 0.1.4 (May 23, 2023) + +* Added `setUserConsent` method. +* Added `getUserConsent` method. + +# 0.1.4-rc.1 (May 23, 2023) + +# 0.1.3 (May 18, 2023) +* Updated iOS SDK to `2.20.1` +* Added support for new configuration options: + * `impressionsMode` + * `syncEnabled` + * `logLevel` + * `userConsent` + * `encryptionEnabled` + +# 0.1.3-rc.1 (May 18, 2023) + +# 0.1.2 (Aug 13, 2022) + +Splits from `splitio` as federated implementation. diff --git a/splitio_ios/LICENSE b/splitio_ios/LICENSE new file mode 100644 index 0000000..54326b0 --- /dev/null +++ b/splitio_ios/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright © 2023 Split Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/splitio_ios/README.md b/splitio_ios/README.md new file mode 100644 index 0000000..d25f02e --- /dev/null +++ b/splitio_ios/README.md @@ -0,0 +1,11 @@ +# splitio\_ios + +The iOS implementation of [`splitio`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `splitio` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/splitio +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/splitio_ios/analysis_options.yaml b/splitio_ios/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/splitio_ios/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/splitio_ios/example/.gitignore b/splitio_ios/example/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/splitio_ios/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/splitio_ios/example/ios/.gitignore b/splitio_ios/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/splitio_ios/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/splitio_ios/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from example/ios/Flutter/AppFrameworkInfo.plist rename to splitio_ios/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/splitio_ios/example/ios/Flutter/Debug.xcconfig b/splitio_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/splitio_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/splitio_ios/example/ios/Flutter/Release.xcconfig b/splitio_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/splitio_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/splitio_ios/example/ios/Podfile similarity index 98% rename from example/ios/Podfile rename to splitio_ios/example/ios/Podfile index 727dd77..e3a7db2 100644 --- a/example/ios/Podfile +++ b/splitio_ios/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/splitio_ios/example/ios/Podfile.lock b/splitio_ios/example/ios/Podfile.lock new file mode 100644 index 0000000..1040a02 --- /dev/null +++ b/splitio_ios/example/ios/Podfile.lock @@ -0,0 +1,29 @@ +PODS: + - Flutter (1.0.0) + - Split (2.21.0) + - splitio_ios (0.3.0): + - Flutter + - Split (~> 2.21.0) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - splitio_ios (from `.symlinks/plugins/splitio_ios/ios`) + +SPEC REPOS: + trunk: + - Split + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + splitio_ios: + :path: ".symlinks/plugins/splitio_ios/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Split: e4324a8fe3fcb19c36a9e4fd84a893fc6d4bbeb0 + splitio_ios: 8293cb4e46661f9403f92a0d143c5bf6ad5053d4 + +PODFILE CHECKSUM: 9eb4e99975f80022b373afce31dc8b3feb22df78 + +COCOAPODS: 1.11.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/splitio_ios/example/ios/Runner.xcodeproj/project.pbxproj similarity index 99% rename from example/ios/Runner.xcodeproj/project.pbxproj rename to splitio_ios/example/ios/Runner.xcodeproj/project.pbxproj index ca1a98b..7f3b9bb 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/splitio_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -193,7 +193,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 030A5D79BA57E380D8657A73 /* [CP] Embed Pods Frameworks */, + 83B902C28BA5AB1DD426315E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -212,7 +212,7 @@ C512775C2847F3D800B7D1A2 /* Sources */, C512775D2847F3D800B7D1A2 /* Frameworks */, C512775E2847F3D800B7D1A2 /* Resources */, - 8639C5B3E80E5EE46CFFE931 /* [CP] Embed Pods Frameworks */, + 456FD0F25BD4B27148539A17 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -285,23 +285,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 030A5D79BA57E380D8657A73 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 06FEC5AC68198B8F6612EC6F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -338,6 +321,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 456FD0F25BD4B27148539A17 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SplitTests/Pods-SplitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SplitTests/Pods-SplitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SplitTests/Pods-SplitTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 6EB34F8858EB9AEFDA66439B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -360,21 +360,21 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 8639C5B3E80E5EE46CFFE931 /* [CP] Embed Pods Frameworks */ = { + 83B902C28BA5AB1DD426315E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SplitTests/Pods-SplitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SplitTests/Pods-SplitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SplitTests/Pods-SplitTests-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { diff --git a/splitio_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/splitio_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/splitio_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/splitio_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/splitio_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/splitio_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/splitio_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/splitio_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/splitio_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/splitio_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/splitio_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..087c2dd --- /dev/null +++ b/splitio_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/splitio_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/splitio_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/splitio_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/splitio_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/splitio_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/splitio_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/splitio_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/splitio_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/splitio_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/splitio_ios/example/ios/Runner/AppDelegate.swift b/splitio_ios/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/splitio_ios/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/splitio_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/splitio_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/splitio_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/splitio_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/splitio_ios/example/ios/Runner/Base.lproj/Main.storyboard b/splitio_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/splitio_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/splitio_ios/example/ios/Runner/Info.plist similarity index 100% rename from example/ios/Runner/Info.plist rename to splitio_ios/example/ios/Runner/Info.plist diff --git a/splitio_ios/example/ios/Runner/Runner-Bridging-Header.h b/splitio_ios/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/splitio_ios/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/SplitTests/ArgumentParserTests.swift b/splitio_ios/example/ios/SplitTests/ArgumentParserTests.swift similarity index 98% rename from example/ios/SplitTests/ArgumentParserTests.swift rename to splitio_ios/example/ios/SplitTests/ArgumentParserTests.swift index 9b67744..dcdf716 100644 --- a/example/ios/SplitTests/ArgumentParserTests.swift +++ b/splitio_ios/example/ios/SplitTests/ArgumentParserTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import splitio +@testable import splitio_ios class ArgumentParserTests: XCTestCase { diff --git a/example/ios/SplitTests/ExtensionsTests.swift b/splitio_ios/example/ios/SplitTests/ExtensionsTests.swift similarity index 100% rename from example/ios/SplitTests/ExtensionsTests.swift rename to splitio_ios/example/ios/SplitTests/ExtensionsTests.swift diff --git a/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift b/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift new file mode 100644 index 0000000..e107644 --- /dev/null +++ b/splitio_ios/example/ios/SplitTests/SplitClientConfigHelperTests.swift @@ -0,0 +1,92 @@ +import XCTest +@testable import splitio_ios +@testable import Split + +class SplitClientConfigHelperTests: XCTestCase { + + func testConfigValuesAreMappedCorrectly() throws { + let configValues: [String: Any?] = ["featuresRefreshRate": 80000, + "segmentsRefreshRate": 70000, + "impressionsRefreshRate": 60000, + "telemetryRefreshRate": 2000, + "eventsQueueSize": 3999, + "impressionsQueueSize": 2999, + "eventFlushInterval": 40000, + "eventsPerPush": 5000, + "trafficType": "none", + "connectionTimeOut": 10, + "readTimeout": 25, + "disableLabels": true, + "proxyHost": "https://proxy", + "ready": 25, + "streamingEnabled": true, + "persistentAttributesEnabled": true, + "syncConfig": ["syncConfigNames": ["split1", "split2"], "syncConfigPrefixes": ["split_", "my_split_"]], + "impressionsMode": "none", + "syncEnabled": false, + "userConsent": "declined", + "encryptionEnabled": true, + "logLevel": "verbose", + "readyTimeout": 10 + ] + + let splitClientConfig = SplitClientConfigHelper.fromMap(configurationMap: configValues, impressionListener: nil) + + XCTAssert(80000 == splitClientConfig.featuresRefreshRate) + XCTAssert(70000 == splitClientConfig.segmentsRefreshRate) + XCTAssert(60000 == splitClientConfig.impressionRefreshRate) + XCTAssert(2000 == splitClientConfig.telemetryRefreshRate) + XCTAssert(3999 == splitClientConfig.eventsQueueSize) + XCTAssert(2999 == splitClientConfig.impressionsQueueSize) + XCTAssert(40000 == splitClientConfig.eventsPushRate) + XCTAssert(5000 == splitClientConfig.eventsPerPush) + XCTAssert("none" == splitClientConfig.trafficType) + XCTAssert(splitClientConfig.streamingEnabled) + XCTAssert(splitClientConfig.persistentAttributesEnabled) + XCTAssertEqual("NONE", splitClientConfig.impressionsMode) + XCTAssertFalse(splitClientConfig.syncEnabled) + XCTAssertEqual(.declined, splitClientConfig.userConsent) + XCTAssertTrue(splitClientConfig.encryptionEnabled) + XCTAssertEqual(.verbose, splitClientConfig.logLevel) + XCTAssertEqual(10000, splitClientConfig.sdkReadyTimeOut) + } + + func testEnableDebugLogLevelIsMappedCorrectly() { + let configValues: [String: Any] = ["enableDebug": true] + let splitClientConfig = SplitClientConfigHelper.fromMap(configurationMap: configValues, impressionListener: nil) + XCTAssertEqual(SplitLogLevel.debug, splitClientConfig.logLevel) + } + + func testLogLevelsAreMappedCorrectly() { + let logLevels = ["verbose", "debug", "info", "warning", "error", "none"] + let expectedLogLevels: [SplitLogLevel] = [.verbose, .debug, .info, .warning, .error, .none] + + for (index, logLevel) in logLevels.enumerated() { + let configValues: [String: Any] = ["logLevel": logLevel] + let config = SplitClientConfigHelper.fromMap(configurationMap: configValues, impressionListener: nil) + XCTAssertEqual(expectedLogLevels[index], config.logLevel) + } + } + + func testUserConsentIsMappedCorrectly() { + let userConsents = ["granted", "unknown", "declined", "any"] + let expectedUserConsents: [UserConsent] = [.granted, .unknown, .declined, .granted] + + for (index, userConsent) in userConsents.enumerated() { + let configValues: [String: Any] = ["userConsent": userConsent] + let config = SplitClientConfigHelper.fromMap(configurationMap: configValues, impressionListener: nil) + XCTAssertEqual(expectedUserConsents[index], config.userConsent) + } + } + + func testImpressionsModeValuesAreMappedCorrectly() { + let impressionsModes = ["debug", "none", "optimized"] + let expectedImpressionsModes: [ImpressionsMode] = [.debug, .none, .optimized] + + for (index, impressionsMode) in impressionsModes.enumerated() { + let configValues: [String: Any] = ["impressionsMode": impressionsMode] + let config = SplitClientConfigHelper.fromMap(configurationMap: configValues, impressionListener: nil) + XCTAssertEqual(expectedImpressionsModes[index].rawValue, config.impressionsMode) + } + } +} diff --git a/example/ios/SplitTests/SplitMethodParserTests.swift b/splitio_ios/example/ios/SplitTests/SplitMethodParserTests.swift similarity index 95% rename from example/ios/SplitTests/SplitMethodParserTests.swift rename to splitio_ios/example/ios/SplitTests/SplitMethodParserTests.swift index 7e65312..e2d96a2 100644 --- a/example/ios/SplitTests/SplitMethodParserTests.swift +++ b/splitio_ios/example/ios/SplitTests/SplitMethodParserTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import splitio +@testable import splitio_ios @testable import Split class SplitMethodParserTests: XCTestCase { @@ -297,6 +297,28 @@ class SplitMethodParserTests: XCTestCase { XCTAssert(providerHelper.splitClientConfigValue?.impressionListener != nil) XCTAssert(providerHelper.splitClientConfigValue?.streamingEnabled == false) } + + func testGetUserConsent() { + methodParser?.onMethodCall(methodName: "getUserConsent", arguments: [], result: { (_: Any?) in }) + + guard let wrapper = splitWrapper as? SplitWrapperStub else { + XCTFail() + return + } + + XCTAssert(wrapper.userConsent == "unknown") + } + + func testSetUserConsent() { + methodParser?.onMethodCall(methodName: "setUserConsent", arguments: [], result: { (_: Any?) in }) + + guard let wrapper = splitWrapper as? SplitWrapperStub else { + XCTFail() + return + } + + XCTAssert(wrapper.userConsent == "declined") + } } class SplitWrapperStub: SplitWrapper { @@ -316,6 +338,7 @@ class SplitWrapperStub: SplitWrapper { var attributeNameValue: String = "" var splitsCalled = false var splitNamesCalled = false + var userConsent = "unknown" func getClient(matchingKey: String, bucketingKey: String?) -> SplitClient? { matchingKeyValue = matchingKey @@ -440,6 +463,18 @@ class SplitWrapperStub: SplitWrapper { splitNameValue = splitName return nil } + + func getUserConsent() -> String { + return userConsent + } + + func setUserConsent(enabled: Bool) { + if (enabled) { + userConsent = "granted" + } else { + userConsent = "declined" + } + } } class SplitProviderHelperStub: SplitProviderHelper { diff --git a/example/ios/SplitTests/SplitProviderHelperTests.swift b/splitio_ios/example/ios/SplitTests/SplitProviderHelperTests.swift similarity index 96% rename from example/ios/SplitTests/SplitProviderHelperTests.swift rename to splitio_ios/example/ios/SplitTests/SplitProviderHelperTests.swift index e9269fd..4de13ea 100644 --- a/example/ios/SplitTests/SplitProviderHelperTests.swift +++ b/splitio_ios/example/ios/SplitTests/SplitProviderHelperTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import splitio +@testable import splitio_ios @testable import Split class SplitProviderHelperTests: XCTestCase { diff --git a/example/ios/SplitTests/SplitTests.swift b/splitio_ios/example/ios/SplitTests/SplitTests.swift similarity index 93% rename from example/ios/SplitTests/SplitTests.swift rename to splitio_ios/example/ios/SplitTests/SplitTests.swift index 217e792..e4c02ad 100644 --- a/example/ios/SplitTests/SplitTests.swift +++ b/splitio_ios/example/ios/SplitTests/SplitTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import splitio +@testable import splitio_ios @testable import Split class SplitTests: XCTestCase { @@ -181,6 +181,26 @@ class SplitTests: XCTestCase { let split = splitWrapper.split(splitName: "my-split") XCTAssert(manager.splitNameValue == "my-split") } + + func testGetUserConsent() { + let manager = SplitManagerStub() + let factoryProvider = SplitFactoryProviderStub(manager: manager) + splitWrapper = DefaultSplitWrapper(splitFactoryProvider: factoryProvider) + let userConsent = splitWrapper.getUserConsent() + XCTAssert(userConsent == "unknown") + } + + func testSetUserConsent() { + let manager = SplitManagerStub() + let factoryProvider = SplitFactoryProviderStub(manager: manager) + splitWrapper = DefaultSplitWrapper(splitFactoryProvider: factoryProvider) + splitWrapper.setUserConsent(enabled: true) + let grantedUserConsent = splitWrapper.getUserConsent() + splitWrapper.setUserConsent(enabled: false) + let declinedUserConsent = splitWrapper.getUserConsent() + XCTAssert(grantedUserConsent == "granted") + XCTAssert(declinedUserConsent == "declined") + } } class SplitFactoryProviderStub: SplitFactoryProvider { @@ -235,12 +255,15 @@ class SplitFactoryStub: SplitFactory { var apiKey: String + var userConsent: UserConsent + init(apiKey: String, client: SplitClient) { self.apiKey = apiKey self.client = client self.nilBucketingKeyClient = SplitClientStub() manager = SplitManagerStub() version = "0.0.0-stub" + userConsent = .unknown } convenience init(apiKey: String) { @@ -267,6 +290,14 @@ class SplitFactoryStub: SplitFactory { func client(matchingKey: String, bucketingKey: String?) -> SplitClient { return client } + + func setUserConsent(enabled: Bool) { + if (enabled) { + self.userConsent = .granted + } else { + self.userConsent = .declined + } + } } class SplitClientStub: SplitClient { diff --git a/splitio_ios/example/lib/main.dart b/splitio_ios/example/lib/main.dart new file mode 100644 index 0000000..20461ef --- /dev/null +++ b/splitio_ios/example/lib/main.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:splitio_ios/splitio_ios.dart'; + +/// Replace these with valid values +const String _apiKey = 'api-key'; +const String _matchingKey = 'user-id'; + +void main() { + runApp(const SplitioExampleApp()); +} + +/// Splitio example home widget +class SplitioExampleApp extends StatefulWidget { + /// Default Constructor + const SplitioExampleApp({Key? key}) : super(key: key); + + @override + State createState() { + return _SplitioExampleAppState(); + } +} + +class _SplitioExampleAppState extends State { + bool _sdkReady = false; + bool _sdkReadyFromCache = false; + + final _split = SplitioIOS(); + + @override + void initState() { + super.initState(); + _initClients(); + } + + void _initClients() { + _split.init(apiKey: _apiKey, matchingKey: _matchingKey, bucketingKey: null); + + _split.getClient(matchingKey: _matchingKey, bucketingKey: null); + + _split + .onReady(matchingKey: _matchingKey, bucketingKey: null) + ?.then((value) { + setState(() { + _sdkReady = true; + }); + }); + + _split + .onReadyFromCache(matchingKey: _matchingKey, bucketingKey: null) + ?.then((value) { + setState(() { + _sdkReadyFromCache = true; + }); + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('split.io example app'), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'SDK ready: $_sdkReady', + style: + const TextStyle(fontWeight: FontWeight.bold, fontSize: 24), + ), + Text( + 'SDK ready from cache: $_sdkReadyFromCache', + style: + const TextStyle(fontWeight: FontWeight.bold, fontSize: 24), + ), + Padding( + padding: const EdgeInsets.fromLTRB(32, 8, 32, 8), + child: TextField( + decoration: + const InputDecoration(hintText: 'Enter split name'), + onChanged: (text) { + setState(() {}); + }, + ), + ), + Visibility( + visible: !(_sdkReady || _sdkReadyFromCache), + child: const CircularProgressIndicator()) + ], + )), + )), + ), + ); + } +} diff --git a/example/pubspec.lock b/splitio_ios/example/pubspec.lock similarity index 90% rename from example/pubspec.lock rename to splitio_ios/example/pubspec.lock index 3d5ec72..ead8f9f 100644 --- a/example/pubspec.lock +++ b/splitio_ios/example/pubspec.lock @@ -49,7 +49,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" fake_async: dependency: transitive description: @@ -109,6 +109,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" sky_engine: dependency: transitive description: flutter @@ -121,13 +128,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" - splitio: + splitio_ios: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.1.1" + version: "0.1.2" + splitio_platform_interface: + dependency: transitive + description: + path: "../../splitio_platform_interface" + relative: true + source: path + version: "1.0.0" stack_trace: dependency: transitive description: diff --git a/splitio_ios/example/pubspec.yaml b/splitio_ios/example/pubspec.yaml new file mode 100644 index 0000000..52656f8 --- /dev/null +++ b/splitio_ios/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: splitio_example +description: Demonstrates how to use the splitio_ios plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.16.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + splitio_ios: + # When depending on this package from a real application you should use: + # splitio: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +flutter: + uses-material-design: true diff --git a/ios/.gitignore b/splitio_ios/ios/.gitignore similarity index 100% rename from ios/.gitignore rename to splitio_ios/ios/.gitignore diff --git a/ios/Assets/.gitkeep b/splitio_ios/ios/Assets/.gitkeep similarity index 100% rename from ios/Assets/.gitkeep rename to splitio_ios/ios/Assets/.gitkeep diff --git a/ios/Classes/ArgumentParser.swift b/splitio_ios/ios/Classes/ArgumentParser.swift similarity index 100% rename from ios/Classes/ArgumentParser.swift rename to splitio_ios/ios/Classes/ArgumentParser.swift diff --git a/ios/Classes/Constants.swift b/splitio_ios/ios/Classes/Constants.swift similarity index 93% rename from ios/Classes/Constants.swift rename to splitio_ios/ios/Classes/Constants.swift index c5e6cce..bea9d65 100644 --- a/ios/Classes/Constants.swift +++ b/splitio_ios/ios/Classes/Constants.swift @@ -24,6 +24,8 @@ enum Method: String { case splits = "splits" case split = "split" case impressionLog = "impressionLog" + case getUserConsent = "getUserConsent" + case setUserConsent = "setUserConsent" } enum Argument: String { diff --git a/ios/Classes/Extensions.swift b/splitio_ios/ios/Classes/Extensions.swift similarity index 100% rename from ios/Classes/Extensions.swift rename to splitio_ios/ios/Classes/Extensions.swift diff --git a/ios/Classes/SplitClientConfigHelper.swift b/splitio_ios/ios/Classes/SplitClientConfigHelper.swift similarity index 77% rename from ios/Classes/SplitClientConfigHelper.swift rename to splitio_ios/ios/Classes/SplitClientConfigHelper.swift index f6aad57..2febd19 100644 --- a/ios/Classes/SplitClientConfigHelper.swift +++ b/splitio_ios/ios/Classes/SplitClientConfigHelper.swift @@ -24,6 +24,12 @@ class SplitClientConfigHelper { static private let SYNC_CONFIG = "syncConfig" static private let SYNC_CONFIG_NAMES = "syncConfigNames" static private let SYNC_CONFIG_PREFIXES = "syncConfigPrefixes" + static private let IMPRESSIONS_MODE = "impressionsMode" + static private let SYNC_ENABLED = "syncEnabled" + static private let USER_CONSENT = "userConsent" + static private let ENCRYPTION_ENABLED = "encryptionEnabled" + static private let LOG_LEVEL = "logLevel" + static private let READY_TIMEOUT = "readyTimeout" static func fromMap(configurationMap: [String: Any?], impressionListener: SplitImpressionListener?) -> SplitClientConfig { let config = SplitClientConfig() @@ -84,7 +90,11 @@ class SplitClientConfigHelper { if configurationMap[ENABLE_DEBUG] != nil { if let enableDebug = configurationMap[ENABLE_DEBUG] as? Bool { - config.isDebugModeEnabled = enableDebug + if (enableDebug) { + config.logLevel = .debug + } else { + config.logLevel = .none + } } } @@ -150,6 +160,50 @@ class SplitClientConfigHelper { config.sync = syncConfigBuilder.build() } } + + if let impressionsMode = configurationMap[IMPRESSIONS_MODE] as? String { + config.impressionsMode = impressionsMode.uppercased() + } + + if let syncEnabled = configurationMap[SYNC_ENABLED] as? Bool { + config.syncEnabled = syncEnabled + } + + if let userConsent = configurationMap[USER_CONSENT] as? String { + switch userConsent.lowercased() { + case "unknown": + config.userConsent = .unknown + case "declined": + config.userConsent = .declined + default: + config.userConsent = .granted + } + } + + if let encryptionEnabled = configurationMap[ENCRYPTION_ENABLED] as? Bool { + config.encryptionEnabled = encryptionEnabled + } + + if let logLevel = configurationMap[LOG_LEVEL] as? String { + switch logLevel.lowercased() { + case "verbose": + config.logLevel = .verbose + case "debug": + config.logLevel = .debug + case "info": + config.logLevel = .info + case "warning": + config.logLevel = .warning + case "error": + config.logLevel = .error + default: + config.logLevel = .none + } + } + + if let readyTimeout = configurationMap[READY_TIMEOUT] as? Int { + config.sdkReadyTimeOut = readyTimeout * 1000 // iOS SDK uses this parameter in millis + } config.serviceEndpoints = serviceEndpointsBuilder.build() diff --git a/ios/Classes/SplitFactoryProvider.swift b/splitio_ios/ios/Classes/SplitFactoryProvider.swift similarity index 100% rename from ios/Classes/SplitFactoryProvider.swift rename to splitio_ios/ios/Classes/SplitFactoryProvider.swift diff --git a/ios/Classes/SplitMethodParser.swift b/splitio_ios/ios/Classes/SplitMethodParser.swift similarity index 98% rename from ios/Classes/SplitMethodParser.swift rename to splitio_ios/ios/Classes/SplitMethodParser.swift index d4c6aba..6c8a216 100644 --- a/ios/Classes/SplitMethodParser.swift +++ b/splitio_ios/ios/Classes/SplitMethodParser.swift @@ -133,6 +133,12 @@ class DefaultSplitMethodParser: SplitMethodParser { case .split: result(SplitView.asMap(splitView: splitWrapper?.split(splitName: argumentParser.getStringArgument(argumentName: .splitName, arguments: arguments) ?? ""))) break + case .getUserConsent: + result(splitWrapper?.getUserConsent()) + break + case .setUserConsent: + splitWrapper?.setUserConsent(enabled: argumentParser.getBooleanArgument(argumentName: .value, arguments: arguments)) + result(nil) default: result(FlutterMethodNotImplemented) break diff --git a/ios/Classes/SplitWrapper.swift b/splitio_ios/ios/Classes/SplitWrapper.swift similarity index 92% rename from ios/Classes/SplitWrapper.swift rename to splitio_ios/ios/Classes/SplitWrapper.swift index f9b65d9..69e764f 100644 --- a/ios/Classes/SplitWrapper.swift +++ b/splitio_ios/ios/Classes/SplitWrapper.swift @@ -16,6 +16,10 @@ protocol SplitWrapper: EvaluationWrapper, AttributesWrapper { func splits() -> [SplitView] func split(splitName: String) -> SplitView? + + func getUserConsent() -> String + + func setUserConsent(enabled: Bool) } protocol EvaluationWrapper { @@ -227,4 +231,26 @@ class DefaultSplitWrapper: SplitWrapper { return nil } } + + func getUserConsent() -> String { + if let splitFactory = splitFactory { + let userConsent: UserConsent = splitFactory.userConsent + + if (userConsent == .granted) { + return "granted" + } else if (userConsent == .declined) { + return "declined" + } else { + return "unknown" + } + } else { + return "unknown" + } + } + + func setUserConsent(enabled: Bool) { + if let splitFactory = splitFactory { + splitFactory.setUserConsent(enabled: enabled) + } + } } diff --git a/ios/Classes/SplitioPlugin.h b/splitio_ios/ios/Classes/SplitioPlugin.h similarity index 100% rename from ios/Classes/SplitioPlugin.h rename to splitio_ios/ios/Classes/SplitioPlugin.h diff --git a/ios/Classes/SplitioPlugin.m b/splitio_ios/ios/Classes/SplitioPlugin.m similarity index 77% rename from ios/Classes/SplitioPlugin.m rename to splitio_ios/ios/Classes/SplitioPlugin.m index 68beac7..0b18eab 100644 --- a/ios/Classes/SplitioPlugin.m +++ b/splitio_ios/ios/Classes/SplitioPlugin.m @@ -1,11 +1,11 @@ #import "SplitioPlugin.h" -#if __has_include() -#import +#if __has_include() +#import #else // Support project import fallback if the generated compatibility header // is not copied when this plugin is created as a library. // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "splitio-Swift.h" +#import "splitio_ios-Swift.h" #endif @implementation SplitioPlugin diff --git a/ios/Classes/SwiftSplitioPlugin.swift b/splitio_ios/ios/Classes/SwiftSplitioPlugin.swift similarity index 87% rename from ios/Classes/SwiftSplitioPlugin.swift rename to splitio_ios/ios/Classes/SwiftSplitioPlugin.swift index 659b2d3..30df9c7 100644 --- a/ios/Classes/SwiftSplitioPlugin.swift +++ b/splitio_ios/ios/Classes/SwiftSplitioPlugin.swift @@ -6,7 +6,7 @@ public class SwiftSplitioPlugin: NSObject, FlutterPlugin { private var methodParser: SplitMethodParser? public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "splitio", binaryMessenger: registrar.messenger()) + let channel = FlutterMethodChannel(name: "split.io/splitio_ios", binaryMessenger: registrar.messenger()) let instance = SwiftSplitioPlugin() let externalProvider: SplitFactoryProvider? = UIApplication.shared.delegate as? SplitFactoryProvider instance.methodParser = DefaultSplitMethodParser(methodChannel: channel, splitFactoryProvider: externalProvider) diff --git a/ios/splitio.podspec b/splitio_ios/ios/splitio_ios.podspec similarity index 80% rename from ios/splitio.podspec rename to splitio_ios/ios/splitio_ios.podspec index d501394..61453f6 100644 --- a/ios/splitio.podspec +++ b/splitio_ios/ios/splitio_ios.podspec @@ -1,10 +1,10 @@ # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint splitio.podspec` to validate before publishing. +# Run `pod lib lint splitio_ios.podspec` to validate before publishing. # Pod::Spec.new do |s| - s.name = 'splitio' - s.version = '0.1.0' + s.name = 'splitio_ios' + s.version = '0.3.0' s.summary = 'split.io official Flutter plugin.' s.description = <<-DESC split.io official Flutter plugin. @@ -15,7 +15,7 @@ split.io official Flutter plugin. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'Split', '~> 2.15.0' + s.dependency 'Split', '~> 2.21.0' s.platform = :ios, '9.0' # Flutter.framework does not contain a i386 slice. diff --git a/splitio_ios/lib/splitio_ios.dart b/splitio_ios/lib/splitio_ios.dart new file mode 100644 index 0000000..3d4c812 --- /dev/null +++ b/splitio_ios/lib/splitio_ios.dart @@ -0,0 +1,15 @@ +import 'package:flutter/services.dart'; +import 'package:splitio_platform_interface/splitio_platform_interface.dart'; + +const MethodChannel _methodChannel = MethodChannel('split.io/splitio_ios'); + +/// Implementation for iOS of [SplitioPlatform]. +class SplitioIOS extends MethodChannelPlatform { + /// Registers this class as the default platform implementation. + static void registerWith() { + SplitioPlatform.instance = SplitioIOS(); + } + + @override + MethodChannel get methodChannel => _methodChannel; +} diff --git a/splitio_ios/pubspec.yaml b/splitio_ios/pubspec.yaml new file mode 100644 index 0000000..9539b32 --- /dev/null +++ b/splitio_ios/pubspec.yaml @@ -0,0 +1,26 @@ +name: splitio_ios +description: The official iOS implementation of splitio Flutter plugin. +repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio_ios +version: 0.1.6 + +environment: + sdk: ">=2.16.2 <4.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: splitio + platforms: + ios: + pluginClass: SwiftSplitioPlugin + dartPluginClass: SplitioIOS + +dependencies: + flutter: + sdk: flutter + splitio_platform_interface: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 diff --git a/splitio_ios/test/splitio_ios_test.dart b/splitio_ios/test/splitio_ios_test.dart new file mode 100644 index 0000000..d037cb3 --- /dev/null +++ b/splitio_ios/test/splitio_ios_test.dart @@ -0,0 +1,517 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_ios/splitio_ios.dart'; +import 'package:splitio_platform_interface/split_configuration.dart'; + +void main() { + const MethodChannel _channel = MethodChannel('split.io/splitio_ios'); + + String methodName = ''; + dynamic methodArguments; + + TestWidgetsFlutterBinding.ensureInitialized(); + + SplitioIOS _platform = SplitioIOS(); + + void _simulateMethodInvocation(String methodName, + {String key = 'key', + String? bucketingKey, + Map? arguments}) { + arguments ??= {}; + arguments.addAll({'matchingKey': key, 'bucketingKey': bucketingKey}); + _channel.invokeMethod(methodName, arguments); + } + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_channel, (MethodCall methodCall) async { + methodName = methodCall.method; + methodArguments = methodCall.arguments; + + _platform.handle(methodCall); + switch (methodCall.method) { + case 'getTreatment': + return ''; + case 'getTreatments': + return {'split1': 'on', 'split2': 'off'}; + case 'getTreatmentsWithConfig': + return { + 'split1': {'treatment': 'on', 'config': null}, + 'split2': {'treatment': 'off', 'config': null} + }; + case 'track': + return true; + case 'getAttribute': + return true; + case 'getAllAttributes': + return { + 'attr_1': true, + 'attr_2': ['list-element'], + 'attr_3': 28.20 + }; + case 'setAttribute': + case 'setAttributes': + case 'removeAttribute': + case 'clearAttributes': + return true; + } + return null; + }); + }); + + group('evaluation', () { + test('getTreatment without attributes', () async { + _platform.getTreatment( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split'); + + expect(methodName, 'getTreatment'); + expect(methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatment with attributes', () async { + _platform.getTreatment( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split', + attributes: {'attr1': true}); + + expect(methodName, 'getTreatment'); + expect(methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatments without attributes', () async { + _platform.getTreatments( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2']); + + expect(methodName, 'getTreatments'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatments with attributes', () async { + _platform.getTreatments( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + attributes: {'attr1': true}); + + expect(methodName, 'getTreatments'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatmentWithConfig with attributes', () async { + _platform.getTreatmentWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split1', + attributes: {'attr1': true}); + + expect(methodName, 'getTreatmentWithConfig'); + expect(methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatmentWithConfig without attributes', () async { + _platform.getTreatmentWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split1'); + + expect(methodName, 'getTreatmentWithConfig'); + expect(methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatmentsWithConfig without attributes', () async { + _platform.getTreatmentsWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2']); + + expect(methodName, 'getTreatmentsWithConfig'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatmentsWithConfig with attributes', () async { + _platform.getTreatmentsWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + attributes: {'attr1': true}); + + expect(methodName, 'getTreatmentsWithConfig'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + }); + + group('track', () { + test('track with traffic type & value', () async { + _platform.track( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + eventType: 'my_event', + trafficType: 'my_traffic_type', + value: 25.10); + expect(methodName, 'track'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'trafficType': 'my_traffic_type', + 'value': 25.10 + }); + }); + + test('track with value', () async { + _platform.track( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + eventType: 'my_event', + value: 25.10); + expect(methodName, 'track'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'value': 25.10 + }); + }); + + test('track with traffic type', () async { + _platform.track( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + eventType: 'my_event', + trafficType: 'my_traffic_type'); + expect(methodName, 'track'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'trafficType': 'my_traffic_type', + }); + }); + }); + + group('attributes', () { + test('get single attribute', () async { + _platform.getAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'attribute-name'); + expect(methodName, 'getAttribute'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'attribute-name', + }); + }); + + test('get all attributes', () async { + _platform.getAllAttributes( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'getAllAttributes'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('set attribute', () async { + _platform.setAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'my_attr', + value: 'attr_value'); + expect(methodName, 'setAttribute'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'my_attr', + 'value': 'attr_value', + }); + }); + + test('set multiple attributes', () async { + _platform.setAttributes( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributes: { + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', 'two'], + }); + expect(methodName, 'setAttributes'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': { + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', 'two'], + } + }); + }); + + test('remove attribute', () async { + _platform.removeAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'attr-name'); + expect(methodName, 'removeAttribute'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'attr-name', + }); + }); + + test('clear attributes', () async { + _platform.clearAttributes( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'clearAttributes'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('flush', () async { + _platform.flush( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'flush'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('destroy', () async { + _platform.destroy( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'destroy'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + }); + + group('initialization', () { + test('init with matching key only', () { + _platform.init( + apiKey: 'api-key', matchingKey: 'matching-key', bucketingKey: null); + + expect(methodName, 'init'); + expect(methodArguments, { + 'apiKey': 'api-key', + 'matchingKey': 'matching-key', + 'sdkConfiguration': {} + }); + }); + + test('init with bucketing key', () { + _platform.init( + apiKey: 'api-key', + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key'); + expect(methodName, 'init'); + expect(methodArguments, { + 'apiKey': 'api-key', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'sdkConfiguration': {} + }); + }); + + test('init with config', () { + _platform.init( + apiKey: 'api-key', + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + sdkConfiguration: + SplitConfiguration(logLevel: SplitLogLevel.warning, streamingEnabled: false)); + expect(methodName, 'init'); + expect(methodArguments, { + 'apiKey': 'api-key', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'sdkConfiguration': {'logLevel': 'warning', 'streamingEnabled': false}, + }); + }); + }); + + group('client', () { + test('get client with no keys', () { + _platform.getClient(matchingKey: 'matching-key', bucketingKey: null); + + expect(methodName, 'getClient'); + expect(methodArguments, {'matchingKey': 'matching-key'}); + }); + + test('get client with new matching key', () { + _platform.getClient(matchingKey: 'new-matching-key', bucketingKey: null); + + expect(methodName, 'getClient'); + expect(methodArguments, {'matchingKey': 'new-matching-key'}); + }); + + test('get client with new matching key and bucketing key', () { + _platform.getClient( + matchingKey: 'new-matching-key', bucketingKey: 'bucketing-key'); + + expect(methodName, 'getClient'); + expect(methodArguments, + {'matchingKey': 'new-matching-key', 'bucketingKey': 'bucketing-key'}); + }); + }); + + group('manager', () { + test('get split names', () { + _platform.splitNames(); + + expect(methodName, 'splitNames'); + }); + + test('get splits', () { + _platform.splits(); + + expect(methodName, 'splits'); + }); + + test('get split', () { + _platform.split(splitName: 'my_split'); + + expect(methodName, 'split'); + expect(methodArguments, {'splitName': 'my_split'}); + }); + }); + + group('events', () { + test('onReady', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onReady = _platform + .onReady(matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.then((value) => true); + + _simulateMethodInvocation('clientReady', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onReady, completion(equals(true))); + }); + + test('onReadyFromCache', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onReadyFromCache = _platform + .onReadyFromCache( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.then((value) => true); + + _simulateMethodInvocation('clientReadyFromCache', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onReadyFromCache, completion(equals(true))); + }); + + test('onTimeout', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onTimeout = _platform + .onTimeout(matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.then((value) => true); + + _simulateMethodInvocation('clientTimeout', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onTimeout, completion(equals(true))); + }); + + test('onUpdated', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onUpdated = _platform + .onUpdated(matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.first + .then((value) => true); + + _simulateMethodInvocation('clientUpdated', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onUpdated, completion(equals(true))); + }); + }); + + test('impressions', () { + _platform.impressionsStream().listen( + expectAsync1((impression) { + expect(impression.key, 'key'); + expect(impression.bucketingKey, null); + expect(impression.split, 'split'); + expect(impression.treatment, 'treatment'); + expect(impression.time, 3000); + expect(impression.appliedRule, 'appliedRule'); + expect(impression.changeNumber, 200); + expect(impression.attributes, {}); + }), + ); + _simulateMethodInvocation('impressionLog', key: 'matching-key', arguments: { + 'key': 'key', + 'bucketingKey': 'bucketingKey', + 'split': 'split', + 'treatment': 'treatment', + 'time': 3000, + 'appliedRule': 'appliedRule', + 'changeNumber': 200, + 'attributes': {} + }); + }); +} diff --git a/splitio_platform_interface/.gitignore b/splitio_platform_interface/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/splitio_platform_interface/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/splitio_platform_interface/CHANGELOG.md b/splitio_platform_interface/CHANGELOG.md new file mode 100644 index 0000000..a46f530 --- /dev/null +++ b/splitio_platform_interface/CHANGELOG.md @@ -0,0 +1,27 @@ +# 1.3.0 (Aug 15, 2023) + +* Added `readyTimeout` configuration option. + +# 1.3.0-rc.1 (Aug 15, 2023) + +# 1.2.0 (May 23, 2023) + +* Added `setUserConsent` method. +* Added `getUserConsent` method. + +# 1.2.0-rc.1 (May 23, 2023) + +# 1.1.0 (May 18, 2023) +* Added support for new configuration options: + * `impressionsMode` + * `syncEnabled` + * `logLevel` + * `userConsent` + * `encryptionEnabled` +* Deprecated `enableDebug` configuration in favor of `logLevel`. + +# 1.1.0-rc.1 (May 18, 2023) + +# 1.0.0 (Aug 13, 2022) + +Initial release. diff --git a/splitio_platform_interface/LICENSE b/splitio_platform_interface/LICENSE new file mode 100644 index 0000000..54326b0 --- /dev/null +++ b/splitio_platform_interface/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright © 2023 Split Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/splitio_platform_interface/README.md b/splitio_platform_interface/README.md new file mode 100644 index 0000000..5492428 --- /dev/null +++ b/splitio_platform_interface/README.md @@ -0,0 +1,26 @@ +# splitio_platform_interface + +A common platform interface for the [`splitio`][1] plugin. + +This interface allows platform-specific implementations of the `splitio` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `splitio`, extend +[`SplitioPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`SplitioPlatform` by calling +`SplitioPlatform.instance = MySplitioPlatform()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../splitio +[2]: lib/splitio_platform_interface.dart diff --git a/splitio_platform_interface/analysis_options.yaml b/splitio_platform_interface/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/splitio_platform_interface/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/splitio_platform_interface/lib/events/split_method_call_handler.dart b/splitio_platform_interface/lib/events/split_method_call_handler.dart new file mode 100644 index 0000000..6a0db02 --- /dev/null +++ b/splitio_platform_interface/lib/events/split_method_call_handler.dart @@ -0,0 +1,79 @@ +import 'dart:async'; + +import 'package:splitio_platform_interface/method_call_handler.dart'; + +class SplitEventMethodCallHandler implements MethodCallHandler { + static const String _eventClientReady = 'clientReady'; + static const String _eventClientReadyFromCache = 'clientReadyFromCache'; + static const String _eventClientTimeout = 'clientTimeout'; + static const String _eventClientUpdated = 'clientUpdated'; + + final String _matchingKey; + final String? _bucketingKey; + final StreamController _updateStreamCompleter = StreamController(); + + final Map> _clientEventCallbacks = { + _eventClientReady: Completer(), + _eventClientReadyFromCache: Completer(), + _eventClientTimeout: Completer(), + }; + + final Map _triggeredClientEvents = { + _eventClientReady: false, + _eventClientReadyFromCache: false, + _eventClientTimeout: false, + }; + + SplitEventMethodCallHandler(this._matchingKey, this._bucketingKey); + + @override + Future handle(String methodName, dynamic methodArguments) async { + if (_clientEventCallbacks.containsKey(methodName)) { + if (_matchingKey == methodArguments['matchingKey'] && + _bucketingKey == methodArguments['bucketingKey']) { + var clientEventCallback = _clientEventCallbacks[methodName]; + if (clientEventCallback != null && !clientEventCallback.isCompleted) { + clientEventCallback.complete(); + } + + if (_triggeredClientEvents.containsKey(methodName)) { + _triggeredClientEvents[methodName] = true; + } + } + } else if (methodName == _eventClientUpdated && + _updateStreamCompleter.hasListener && + !_updateStreamCompleter.isPaused && + !_updateStreamCompleter.isClosed) { + _updateStreamCompleter.add(null); + } + } + + Future onReady() { + return _onEvent(_eventClientReady); + } + + Future onReadyFromCache() { + return _onEvent(_eventClientReadyFromCache); + } + + Stream onUpdated() { + return _updateStreamCompleter.stream; + } + + Future onTimeout() { + return _onEvent(_eventClientTimeout); + } + + void destroy() { + _updateStreamCompleter.close(); + } + + Future _onEvent(String sdkEvent) { + if (_triggeredClientEvents.containsKey(sdkEvent) && + _triggeredClientEvents[sdkEvent] == true) { + return Future.value(); + } + + return (_clientEventCallbacks[sdkEvent])?.future ?? Future.error(Object()); + } +} diff --git a/lib/impressions/impressions_method_call_handler.dart b/splitio_platform_interface/lib/impressions/impressions_method_call_handler.dart similarity index 59% rename from lib/impressions/impressions_method_call_handler.dart rename to splitio_platform_interface/lib/impressions/impressions_method_call_handler.dart index d09527a..ea4e4a4 100644 --- a/lib/impressions/impressions_method_call_handler.dart +++ b/splitio_platform_interface/lib/impressions/impressions_method_call_handler.dart @@ -1,20 +1,19 @@ import 'dart:async'; -import 'package:flutter/services.dart'; -import 'package:splitio/impressions/split_impression.dart'; -import 'package:splitio/method_call_handler.dart'; +import 'package:splitio_platform_interface/method_call_handler.dart'; +import 'package:splitio_platform_interface/split_impression.dart'; class ImpressionsMethodCallHandler extends StreamMethodCallHandler { final _streamController = StreamController(); @override - Future handle(MethodCall call) async { - if (call.method == 'impressionLog') { + Future handle(String methodName, dynamic methodArguments) async { + if (methodName == 'impressionLog') { if (_streamController.hasListener && !_streamController.isPaused && !_streamController.isClosed) { _streamController.add(Impression.fromMap( - Map.from(call.arguments ?? {}))); + Map.from(methodArguments ?? {}))); } } } diff --git a/lib/method_call_handler.dart b/splitio_platform_interface/lib/method_call_handler.dart similarity index 61% rename from lib/method_call_handler.dart rename to splitio_platform_interface/lib/method_call_handler.dart index 42c7c4f..6ef88ac 100644 --- a/lib/method_call_handler.dart +++ b/splitio_platform_interface/lib/method_call_handler.dart @@ -1,7 +1,5 @@ -import 'package:flutter/services.dart'; - abstract class MethodCallHandler { - Future handle(MethodCall call); + Future handle(String methodName, dynamic methodArguments); } abstract class StreamMethodCallHandler extends MethodCallHandler { diff --git a/splitio_platform_interface/lib/method_channel_platform.dart b/splitio_platform_interface/lib/method_channel_platform.dart new file mode 100644 index 0000000..4bc0400 --- /dev/null +++ b/splitio_platform_interface/lib/method_channel_platform.dart @@ -0,0 +1,348 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:splitio_platform_interface/splitio_platform_interface.dart'; + +const String _controlTreatment = 'control'; +const SplitResult _controlResult = SplitResult(_controlTreatment, null); +const MethodChannel _methodChannel = MethodChannel('splitio'); + +class MethodChannelPlatform extends SplitioPlatform { + MethodChannel get methodChannel => _methodChannel; + + final Map _handlers = {}; + + final ImpressionsMethodCallHandler _impressionsMethodCallHandler = + ImpressionsMethodCallHandler(); + + @visibleForTesting + Future handle(MethodCall call) async { + _impressionsMethodCallHandler.handle(call.method, call.arguments); + for (MethodCallHandler handler in _handlers.values) { + handler.handle(call.method, call.arguments); + } + } + + @override + Future init( + {required String apiKey, + required String matchingKey, + required String? bucketingKey, + SplitConfiguration? sdkConfiguration}) { + methodChannel.setMethodCallHandler((call) => handle(call)); + + Map arguments = { + 'apiKey': apiKey, + 'matchingKey': matchingKey, + 'sdkConfiguration': sdkConfiguration?.configurationMap ?? SplitConfiguration().configurationMap, // If sdkConfiguration is null, create a new SplitConfiguration to apply default values + }; + + if (bucketingKey != null) { + arguments.addAll({'bucketingKey': bucketingKey}); + } + + return methodChannel.invokeMethod('init', arguments); + } + + @override + Future getClient( + {required String matchingKey, required String? bucketingKey}) { + _handlers.addAll({ + _buildMapKey(matchingKey, bucketingKey): + SplitEventMethodCallHandler(matchingKey, bucketingKey) + }); + + return methodChannel.invokeMethod( + 'getClient', _buildParameters(matchingKey, bucketingKey)); + } + + @override + Future clearAttributes( + {required String matchingKey, required String? bucketingKey}) async { + return await methodChannel.invokeMethod( + 'clearAttributes', _buildParameters(matchingKey, bucketingKey)); + } + + @override + Future destroy( + {required String matchingKey, required String? bucketingKey}) async { + var handlerKey = _buildMapKey(matchingKey, bucketingKey); + _handlers[handlerKey]?.destroy(); + _handlers.remove(handlerKey); + + return await methodChannel.invokeMethod( + 'destroy', _buildParameters(matchingKey, bucketingKey)); + } + + @override + Future flush( + {required String matchingKey, required String? bucketingKey}) async { + return await methodChannel.invokeMethod( + 'flush', _buildParameters(matchingKey, bucketingKey)); + } + + @override + Future> getAllAttributes( + {required String matchingKey, required String? bucketingKey}) async { + return (await methodChannel.invokeMapMethod('getAllAttributes', + _buildParameters(matchingKey, bucketingKey))) + ?.map((key, value) => MapEntry(key, value)) ?? + {}; + } + + @override + Future getAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName}) { + return methodChannel.invokeMethod( + 'getAttribute', + _buildParameters( + matchingKey, bucketingKey, {'attributeName': attributeName})); + } + + @override + Future getTreatment( + {required String matchingKey, + required String? bucketingKey, + required String splitName, + Map attributes = const {}}) async { + return await methodChannel.invokeMethod( + 'getTreatment', + _buildParameters(matchingKey, bucketingKey, + {'splitName': splitName, 'attributes': attributes})) ?? + _controlTreatment; + } + + @override + Future getTreatmentWithConfig( + {required String matchingKey, + required String? bucketingKey, + required String splitName, + Map attributes = const {}}) async { + Map? treatment = (await methodChannel.invokeMapMethod( + 'getTreatmentWithConfig', + _buildParameters(matchingKey, bucketingKey, + {'splitName': splitName, 'attributes': attributes}))) + ?.entries + .first + .value; + if (treatment == null) { + return _controlResult; + } + + return SplitResult(treatment['treatment'], treatment['config']); + } + + @override + Future> getTreatments( + {required String matchingKey, + required String? bucketingKey, + required List splitNames, + Map attributes = const {}}) async { + Map? treatments = await methodChannel.invokeMapMethod( + 'getTreatments', + _buildParameters(matchingKey, bucketingKey, + {'splitName': splitNames, 'attributes': attributes})); + + return treatments + ?.map((key, value) => MapEntry(key, value)) ?? + {for (var item in splitNames) item: _controlTreatment}; + } + + @override + Future> getTreatmentsWithConfig( + {required String matchingKey, + required String? bucketingKey, + required List splitNames, + Map attributes = const {}}) async { + Map? treatments = await methodChannel.invokeMapMethod( + 'getTreatmentsWithConfig', + _buildParameters(matchingKey, bucketingKey, + {'splitName': splitNames, 'attributes': attributes})); + + return treatments?.map((key, value) => + MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? + {for (var item in splitNames) item: _controlResult}; + } + + @override + Future removeAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName}) async { + return await methodChannel.invokeMethod( + 'removeAttribute', + _buildParameters( + matchingKey, bucketingKey, {'attributeName': attributeName})); + } + + @override + Future setAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName, + required value}) async { + var result = await methodChannel.invokeMethod( + 'setAttribute', + _buildParameters(matchingKey, bucketingKey, + {'attributeName': attributeName, 'value': value})); + + if (result is bool) { + return result; + } + + return false; + } + + @override + Future setAttributes( + {required String matchingKey, + required String? bucketingKey, + required Map attributes}) async { + var result = await methodChannel.invokeMethod( + 'setAttributes', + _buildParameters( + matchingKey, bucketingKey, {'attributes': attributes})); + + if (result is bool) { + return result; + } + + return false; + } + + @override + Future split({required String splitName}) async { + Map? mapResult = + await methodChannel.invokeMapMethod('split', {'splitName': splitName}); + + if (mapResult == null) { + return null; + } + + return SplitView.fromEntry(mapResult); + } + + @override + Future> splitNames() async { + List splitNames = + await methodChannel.invokeListMethod('splitNames') ?? []; + + return splitNames; + } + + @override + Future> splits() async { + List callResult = (await methodChannel + .invokeListMethod>('splits') ?? + []); + + List splits = []; + for (var element in callResult) { + SplitView? splitView = SplitView.fromEntry(element); + if (splitView != null) { + splits.add(splitView); + } + } + + return Future.value(splits); + } + + @override + Future track( + {required String matchingKey, + required String? bucketingKey, + required String eventType, + String? trafficType, + double? value, + Map properties = const {}}) async { + var parameters = + _buildParameters(matchingKey, bucketingKey, {'eventType': eventType}); + + if (trafficType != null) { + parameters['trafficType'] = trafficType; + } + + if (value != null) { + parameters['value'] = value; + } + + try { + return await methodChannel.invokeMethod('track', parameters) as bool; + } on Exception catch (_) { + return false; + } + } + + Map _buildParameters( + String matchingKey, String? bucketingKey, + [Map parameters = const {}]) { + Map result = {}; + result.addAll(parameters); + result.addAll(_getKeysMap(matchingKey, bucketingKey)); + + return result; + } + + Map _getKeysMap(String matchingKey, String? bucketingKey) { + Map result = {'matchingKey': matchingKey}; + + if (bucketingKey != null) { + result.addAll({'bucketingKey': bucketingKey}); + } + + return result; + } + + @override + Future? onReady( + {required String matchingKey, required String? bucketingKey}) { + return _handlers[_buildMapKey(matchingKey, bucketingKey)]?.onReady(); + } + + @override + Future? onReadyFromCache( + {required String matchingKey, required String? bucketingKey}) { + return _handlers[_buildMapKey(matchingKey, bucketingKey)] + ?.onReadyFromCache(); + } + + @override + Stream? onUpdated( + {required String matchingKey, required String? bucketingKey}) { + return _handlers[_buildMapKey(matchingKey, bucketingKey)]?.onUpdated(); + } + + @override + Future? onTimeout( + {required String matchingKey, required String? bucketingKey}) { + return _handlers[_buildMapKey(matchingKey, bucketingKey)]?.onTimeout(); + } + + @override + Stream impressionsStream() { + return _impressionsMethodCallHandler.stream(); + } + + @override + Future getUserConsent() async { + String userConsent = + (await methodChannel.invokeMethod('getUserConsent')) as String; + if (userConsent == 'granted') { + return UserConsent.granted; + } else if (userConsent == 'declined') { + return UserConsent.declined; + } else { + return UserConsent.unknown; + } + } + + @override + Future setUserConsent(bool enabled) async { + await methodChannel.invokeMethod('setUserConsent', {'value': enabled}); + } + + String _buildMapKey(String matchingKey, String? bucketingKey) { + return '${matchingKey}_$bucketingKey'; + } +} diff --git a/lib/split_configuration.dart b/splitio_platform_interface/lib/split_configuration.dart similarity index 59% rename from lib/split_configuration.dart rename to splitio_platform_interface/lib/split_configuration.dart index 5e93df9..08496aa 100644 --- a/lib/split_configuration.dart +++ b/splitio_platform_interface/lib/split_configuration.dart @@ -1,11 +1,11 @@ -import 'package:splitio/split_sync_config.dart'; +import 'package:splitio_platform_interface/split_sync_config.dart'; class SplitConfiguration { final Map configurationMap = {}; /// Initializes the Split configuration. /// - /// [featuresRefreshRate] the SDK polls Split servers for changes to feature splits at this rate (in seconds). + /// [featuresRefreshRate] the SDK polls Split servers for changes to feature flags at this rate (in seconds). /// /// [segmentsRefreshRate] The SDK polls Split servers for changes to segments at this rate (in seconds). /// @@ -23,7 +23,7 @@ class SplitConfiguration { /// /// [trafficType] The default traffic type for events tracked using the track method. If not specified, every track call should specify a traffic type. /// - /// [enableDebug] If true, the SDK will log debug messages to the console. + /// [enableDebug] (Deprecated; use logLevel instead) If true, the SDK will log debug messages to the console. /// /// [streamingEnabled] Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. /// @@ -31,7 +31,19 @@ class SplitConfiguration { /// /// [impressionListener] Enables impression listener. If true, generated impressions will be streamed in the impressionsStream() method of Splitio. /// - /// [syncConfig] Use it to filter specific splits to be synced and evaluated by the SDK. If not set, all splits will be downloaded. + /// [syncConfig] Use it to filter specific feature flags to be synced and evaluated by the SDK. If not set, all feature flags will be downloaded. + /// + /// [impressionsMode] This configuration defines how impressions (decisioning events) are queued. Supported modes are [ImpressionsMode.optimized], [ImpressionsMode.none], and [ImpressionsMode.debug]. In [ImpressionsMode.optimized] mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In [ImpressionsMode.none] mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use [ImpressionsMode.none] when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In [ImpressionsMode.debug] mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. + /// + /// [syncEnabled] Controls the SDK continuous synchronization flags. When true, a running SDK processes rollout plan updates performed in the Split user interface (default). When false, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. + /// + /// [userConsent] User consent status used to control the tracking of events and impressions. Possible values are [UserConsent.granted], [UserConsent.declined], and [UserConsent.unknown]. + /// + /// [encryptionEnabled] If set to true, the local database contents is encrypted. Defaults to false. + /// + /// [logLevel] Enables logging according to the level specified. Options are [SplitLogLevel.verbose], [SplitLogLevel.none], [SplitLogLevel.debug], [SplitLogLevel.info], [SplitLogLevel.warning], and [SplitLogLevel.error]. + /// + /// [readyTimeout] Maximum amount of time in seconds to wait before firing the SDK_READY_TIMED_OUT event. Defaults to 10 seconds. SplitConfiguration({ int? featuresRefreshRate, int? segmentsRefreshRate, @@ -42,7 +54,7 @@ class SplitConfiguration { int? eventFlushInterval, int? eventsPerPush, String? trafficType, - bool? enableDebug, + @Deprecated('Use logLevel instead') bool? enableDebug, bool? streamingEnabled, bool? persistentAttributesEnabled, bool? impressionListener, @@ -52,6 +64,12 @@ class SplitConfiguration { String? streamingServiceEndpoint, String? telemetryServiceEndpoint, SyncConfig? syncConfig, + ImpressionsMode? impressionsMode, + bool? syncEnabled, + UserConsent? userConsent, + bool? encryptionEnabled, + SplitLogLevel? logLevel, + int? readyTimeout = 10, }) { if (featuresRefreshRate != null) { configurationMap['featuresRefreshRate'] = featuresRefreshRate; @@ -132,5 +150,43 @@ class SplitConfiguration { 'syncConfigPrefixes': syncConfig.prefixes.toList(growable: false) }; } + + if (impressionsMode != null) { + configurationMap['impressionsMode'] = impressionsMode.name; + } + + if (syncEnabled != null) { + configurationMap['syncEnabled'] = syncEnabled; + } + + if (userConsent != null) { + configurationMap['userConsent'] = userConsent.name; + } + + if (encryptionEnabled != null) { + configurationMap['encryptionEnabled'] = encryptionEnabled; + } + + if (logLevel != null) { + configurationMap['logLevel'] = logLevel.name; + } + + if (readyTimeout != null) { + configurationMap['readyTimeout'] = readyTimeout; + } } } + +enum ImpressionsMode { + debug, + optimized, + none, +} + +enum UserConsent { + granted, + declined, + unknown, +} + +enum SplitLogLevel { verbose, debug, info, warning, error, none } diff --git a/lib/impressions/split_impression.dart b/splitio_platform_interface/lib/split_impression.dart similarity index 100% rename from lib/impressions/split_impression.dart rename to splitio_platform_interface/lib/split_impression.dart diff --git a/lib/split_result.dart b/splitio_platform_interface/lib/split_result.dart similarity index 100% rename from lib/split_result.dart rename to splitio_platform_interface/lib/split_result.dart diff --git a/splitio_platform_interface/lib/split_sync_config.dart b/splitio_platform_interface/lib/split_sync_config.dart new file mode 100644 index 0000000..6eec23d --- /dev/null +++ b/splitio_platform_interface/lib/split_sync_config.dart @@ -0,0 +1,20 @@ +class SyncConfig { + late Set _names; + late Set _prefixes; + + Set get names => _names; + + Set get prefixes => _prefixes; + + SyncConfig( + {List names = const [], List prefixes = const []}) { + _names = names.toSet(); + _prefixes = prefixes.toSet(); + } + + SyncConfig.fromSet( + {Set names = const {}, Set prefixes = const {}}) { + _names = names; + _prefixes = prefixes; + } +} diff --git a/lib/split_view.dart b/splitio_platform_interface/lib/split_view.dart similarity index 100% rename from lib/split_view.dart rename to splitio_platform_interface/lib/split_view.dart diff --git a/splitio_platform_interface/lib/splitio_platform_interface.dart b/splitio_platform_interface/lib/splitio_platform_interface.dart new file mode 100644 index 0000000..3e71adb --- /dev/null +++ b/splitio_platform_interface/lib/splitio_platform_interface.dart @@ -0,0 +1,196 @@ +import 'dart:async'; + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:splitio_platform_interface/method_channel_platform.dart'; +import 'package:splitio_platform_interface/split_configuration.dart'; +import 'package:splitio_platform_interface/split_impression.dart'; +import 'package:splitio_platform_interface/split_result.dart'; +import 'package:splitio_platform_interface/split_view.dart'; + +export 'package:splitio_platform_interface/events/split_method_call_handler.dart'; +export 'package:splitio_platform_interface/impressions/impressions_method_call_handler.dart'; +export 'package:splitio_platform_interface/method_call_handler.dart'; +export 'package:splitio_platform_interface/method_channel_platform.dart'; +export 'package:splitio_platform_interface/split_configuration.dart'; +export 'package:splitio_platform_interface/split_impression.dart'; +export 'package:splitio_platform_interface/split_result.dart'; +export 'package:splitio_platform_interface/split_view.dart'; +export 'package:splitio_platform_interface/splitio_platform_interface.dart'; + +abstract class _FactoryPlatform { + Future getClient( + {required String matchingKey, required String? bucketingKey}) { + throw UnimplementedError(); + } + + Future init({ + required String apiKey, + required String matchingKey, + required String? bucketingKey, + SplitConfiguration? sdkConfiguration, + }) { + throw UnimplementedError(); + } + + Future split({required String splitName}) { + throw UnimplementedError(); + } + + Future> splitNames() { + throw UnimplementedError(); + } + + Future> splits() { + throw UnimplementedError(); + } + + Stream impressionsStream() { + throw UnimplementedError(); + } + + Future getUserConsent() { + throw UnimplementedError(); + } + + Future setUserConsent(bool enabled) { + throw UnimplementedError(); + } +} + +abstract class _ClientPlatform { + Future getTreatment( + {required String matchingKey, + required String? bucketingKey, + required String splitName, + Map attributes = const {}}) { + throw UnimplementedError(); + } + + Future getTreatmentWithConfig( + {required String matchingKey, + required String? bucketingKey, + required String splitName, + Map attributes = const {}}) { + throw UnimplementedError(); + } + + Future> getTreatments( + {required String matchingKey, + required String? bucketingKey, + required List splitNames, + Map attributes = const {}}) { + throw UnimplementedError(); + } + + Future> getTreatmentsWithConfig( + {required String matchingKey, + required String? bucketingKey, + required List splitNames, + Map attributes = const {}}) { + throw UnimplementedError(); + } + + Future> getAllAttributes( + {required String matchingKey, required String? bucketingKey}) { + throw UnimplementedError(); + } + + Future setAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName, + required dynamic value}) { + throw UnimplementedError(); + } + + Future setAttributes( + {required String matchingKey, + required String? bucketingKey, + required Map attributes}) { + throw UnimplementedError(); + } + + Future getAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName}) { + throw UnimplementedError(); + } + + Future removeAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName}) { + throw UnimplementedError(); + } + + Future clearAttributes( + {required String matchingKey, required String? bucketingKey}) { + throw UnimplementedError(); + } + + Future flush( + {required String matchingKey, required String? bucketingKey}) { + throw UnimplementedError(); + } + + Future destroy( + {required String matchingKey, required String? bucketingKey}) { + throw UnimplementedError(); + } + + Future track( + {required String matchingKey, + required String? bucketingKey, + required String eventType, + String? trafficType, + double? value, + Map properties = const {}}) { + throw UnimplementedError(); + } + + Future? onReady( + {required String matchingKey, required String? bucketingKey}) { + throw UnimplementedError(); + } + + Future? onReadyFromCache( + {required String matchingKey, required String? bucketingKey}) { + throw UnimplementedError(); + } + + Stream? onUpdated( + {required String matchingKey, required String? bucketingKey}) { + throw UnimplementedError(); + } + + Future? onTimeout( + {required String matchingKey, required String? bucketingKey}) { + throw UnimplementedError(); + } +} + +/// The interface that implementations of splitio must implement. +/// +/// Platform implementations should extend this class rather than implement it as `splitio` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [SplitioPlatform] methods. +abstract class SplitioPlatform extends PlatformInterface + with _FactoryPlatform, _ClientPlatform { + SplitioPlatform() : super(token: _token); + + static SplitioPlatform _instance = MethodChannelPlatform(); + + static final Object _token = Object(); + + static SplitioPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [SplitioPlatform] when they register themselves. + static set instance(SplitioPlatform instance) { + PlatformInterface.verify(instance, _token); + _instance = instance; + } +} diff --git a/splitio_platform_interface/pubspec.yaml b/splitio_platform_interface/pubspec.yaml new file mode 100644 index 0000000..0a73b0f --- /dev/null +++ b/splitio_platform_interface/pubspec.yaml @@ -0,0 +1,20 @@ +name: splitio_platform_interface +description: A common platform interface for the splitio plugin. +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.3.0 +repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio_platform_interface + +environment: + sdk: ">=2.16.2 <4.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.4 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 diff --git a/test/impressions_method_call_handler_test.dart b/splitio_platform_interface/test/impressions_method_call_handler_test.dart similarity index 77% rename from test/impressions_method_call_handler_test.dart rename to splitio_platform_interface/test/impressions_method_call_handler_test.dart index f919f9a..f6836bd 100644 --- a/test/impressions_method_call_handler_test.dart +++ b/splitio_platform_interface/test/impressions_method_call_handler_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:splitio/impressions/impressions_method_call_handler.dart'; -import 'package:splitio/impressions/split_impression.dart'; +import 'package:splitio_platform_interface/impressions/impressions_method_call_handler.dart'; +import 'package:splitio_platform_interface/split_impression.dart'; void main() { test('correct impressionLog method call emits value on stream', () async { @@ -34,8 +34,9 @@ void main() { }), ); - impressionsMethodCallHandler - .handle(const MethodCall('impressionLog', sourceMap)); + const methodCall = MethodCall('impressionLog', sourceMap); + impressionsMethodCallHandler.handle( + methodCall.method, methodCall.arguments); }); test('other method names are ignored', () async { @@ -47,6 +48,8 @@ void main() { expect(event, null); }, count: 0), ); - impressionsMethodCallHandler.handle(const MethodCall('clientReady')); + const methodCall = MethodCall('clientReady'); + impressionsMethodCallHandler.handle( + methodCall.method, methodCall.arguments); }); } diff --git a/splitio_platform_interface/test/method_channel_platform_test.dart b/splitio_platform_interface/test/method_channel_platform_test.dart new file mode 100644 index 0000000..d03c4f5 --- /dev/null +++ b/splitio_platform_interface/test/method_channel_platform_test.dart @@ -0,0 +1,542 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_platform_interface/method_channel_platform.dart'; +import 'package:splitio_platform_interface/split_configuration.dart'; + +void main() { + const MethodChannel _channel = MethodChannel('splitio'); + + String methodName = ''; + dynamic methodArguments; + + TestWidgetsFlutterBinding.ensureInitialized(); + + MethodChannelPlatform _platform = MethodChannelPlatform(); + + void _simulateMethodInvocation(String methodName, + {String key = 'key', + String? bucketingKey, + Map? arguments}) { + arguments ??= {}; + arguments.addAll({'matchingKey': key, 'bucketingKey': bucketingKey}); + _channel.invokeMethod(methodName, arguments); + } + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_channel, (MethodCall methodCall) async { + methodName = methodCall.method; + methodArguments = methodCall.arguments; + + _platform.handle(methodCall); + switch (methodCall.method) { + case 'getTreatment': + return ''; + case 'getTreatments': + return {'split1': 'on', 'split2': 'off'}; + case 'getTreatmentsWithConfig': + return { + 'split1': {'treatment': 'on', 'config': null}, + 'split2': {'treatment': 'off', 'config': null} + }; + case 'track': + return true; + case 'getAttribute': + return true; + case 'getAllAttributes': + return { + 'attr_1': true, + 'attr_2': ['list-element'], + 'attr_3': 28.20 + }; + case 'setAttribute': + case 'setAttributes': + case 'removeAttribute': + case 'clearAttributes': + return true; + case 'getUserConsent': + return 'declined'; + } + return null; + }); + }); + + group('evaluation', () { + test('getTreatment without attributes', () async { + _platform.getTreatment( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split'); + + expect(methodName, 'getTreatment'); + expect(methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatment with attributes', () async { + _platform.getTreatment( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split', + attributes: {'attr1': true}); + + expect(methodName, 'getTreatment'); + expect(methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatments without attributes', () async { + _platform.getTreatments( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2']); + + expect(methodName, 'getTreatments'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatments with attributes', () async { + _platform.getTreatments( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + attributes: {'attr1': true}); + + expect(methodName, 'getTreatments'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatmentWithConfig with attributes', () async { + _platform.getTreatmentWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split1', + attributes: {'attr1': true}); + + expect(methodName, 'getTreatmentWithConfig'); + expect(methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + + test('getTreatmentWithConfig without attributes', () async { + _platform.getTreatmentWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split1'); + + expect(methodName, 'getTreatmentWithConfig'); + expect(methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatmentsWithConfig without attributes', () async { + _platform.getTreatmentsWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2']); + + expect(methodName, 'getTreatmentsWithConfig'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {} + }); + }); + + test('getTreatmentsWithConfig with attributes', () async { + _platform.getTreatmentsWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + attributes: {'attr1': true}); + + expect(methodName, 'getTreatmentsWithConfig'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {'attr1': true} + }); + }); + }); + + group('track', () { + test('track with traffic type & value', () async { + _platform.track( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + eventType: 'my_event', + trafficType: 'my_traffic_type', + value: 25.10); + expect(methodName, 'track'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'trafficType': 'my_traffic_type', + 'value': 25.10 + }); + }); + + test('track with value', () async { + _platform.track( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + eventType: 'my_event', + value: 25.10); + expect(methodName, 'track'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'value': 25.10 + }); + }); + + test('track with traffic type', () async { + _platform.track( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + eventType: 'my_event', + trafficType: 'my_traffic_type'); + expect(methodName, 'track'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'eventType': 'my_event', + 'trafficType': 'my_traffic_type', + }); + }); + }); + + group('attributes', () { + test('get single attribute', () async { + _platform.getAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'attribute-name'); + expect(methodName, 'getAttribute'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'attribute-name', + }); + }); + + test('get all attributes', () async { + _platform.getAllAttributes( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'getAllAttributes'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('set attribute', () async { + _platform.setAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'my_attr', + value: 'attr_value'); + expect(methodName, 'setAttribute'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'my_attr', + 'value': 'attr_value', + }); + }); + + test('set multiple attributes', () async { + _platform.setAttributes( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributes: { + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', 'two'], + }); + expect(methodName, 'setAttributes'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': { + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', 'two'], + } + }); + }); + + test('remove attribute', () async { + _platform.removeAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'attr-name'); + expect(methodName, 'removeAttribute'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributeName': 'attr-name', + }); + }); + + test('clear attributes', () async { + _platform.clearAttributes( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'clearAttributes'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('flush', () async { + _platform.flush( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'flush'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + + test('destroy', () async { + _platform.destroy( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + expect(methodName, 'destroy'); + expect(methodArguments, { + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + }); + }); + }); + + group('initialization', () { + test('init with matching key only', () { + _platform.init( + apiKey: 'api-key', matchingKey: 'matching-key', bucketingKey: null); + + expect(methodName, 'init'); + expect(methodArguments, { + 'apiKey': 'api-key', + 'matchingKey': 'matching-key', + 'sdkConfiguration': {'readyTimeout': 10} + }); + }); + + test('init with bucketing key', () { + _platform.init( + apiKey: 'api-key', + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key'); + expect(methodName, 'init'); + expect(methodArguments, { + 'apiKey': 'api-key', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'sdkConfiguration': {'readyTimeout': 10} + }); + }); + + test('init with config', () { + _platform.init( + apiKey: 'api-key', + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + sdkConfiguration: SplitConfiguration( + logLevel: SplitLogLevel.debug, streamingEnabled: false)); + expect(methodName, 'init'); + expect(methodArguments, { + 'apiKey': 'api-key', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'sdkConfiguration': {'logLevel': 'debug', 'streamingEnabled': false, 'readyTimeout' : 10}, + }); + }); + }); + + group('client', () { + test('get client with no keys', () { + _platform.getClient(matchingKey: 'matching-key', bucketingKey: null); + + expect(methodName, 'getClient'); + expect(methodArguments, {'matchingKey': 'matching-key'}); + }); + + test('get client with new matching key', () { + _platform.getClient(matchingKey: 'new-matching-key', bucketingKey: null); + + expect(methodName, 'getClient'); + expect(methodArguments, {'matchingKey': 'new-matching-key'}); + }); + + test('get client with new matching key and bucketing key', () { + _platform.getClient( + matchingKey: 'new-matching-key', bucketingKey: 'bucketing-key'); + + expect(methodName, 'getClient'); + expect(methodArguments, + {'matchingKey': 'new-matching-key', 'bucketingKey': 'bucketing-key'}); + }); + }); + + group('manager', () { + test('get split names', () { + _platform.splitNames(); + + expect(methodName, 'splitNames'); + }); + + test('get splits', () { + _platform.splits(); + + expect(methodName, 'splits'); + }); + + test('get split', () { + _platform.split(splitName: 'my_split'); + + expect(methodName, 'split'); + expect(methodArguments, {'splitName': 'my_split'}); + }); + }); + + group('events', () { + test('onReady', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onReady = _platform + .onReady(matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.then((value) => true); + + _simulateMethodInvocation('clientReady', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onReady, completion(equals(true))); + }); + + test('onReadyFromCache', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onReadyFromCache = _platform + .onReadyFromCache( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.then((value) => true); + + _simulateMethodInvocation('clientReadyFromCache', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onReadyFromCache, completion(equals(true))); + }); + + test('onTimeout', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onTimeout = _platform + .onTimeout(matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.then((value) => true); + + _simulateMethodInvocation('clientTimeout', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onTimeout, completion(equals(true))); + }); + + test('onUpdated', () { + _platform.getClient( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + Future? onUpdated = _platform + .onUpdated(matchingKey: 'matching-key', bucketingKey: 'bucketing-key') + ?.first + .then((value) => true); + + _simulateMethodInvocation('clientUpdated', + key: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(onUpdated, completion(equals(true))); + }); + }); + + test('impressions', () { + _platform.impressionsStream().listen( + expectAsync1((impression) { + expect(impression.key, 'key'); + expect(impression.bucketingKey, null); + expect(impression.split, 'split'); + expect(impression.treatment, 'treatment'); + expect(impression.time, 3000); + expect(impression.appliedRule, 'appliedRule'); + expect(impression.changeNumber, 200); + expect(impression.attributes, {}); + }), + ); + _simulateMethodInvocation('impressionLog', key: 'matching-key', arguments: { + 'key': 'key', + 'bucketingKey': 'bucketingKey', + 'split': 'split', + 'treatment': 'treatment', + 'time': 3000, + 'appliedRule': 'appliedRule', + 'changeNumber': 200, + 'attributes': {} + }); + }); + + group('userConsent', () { + test('get user consent', () async { + UserConsent userConsent = await _platform.getUserConsent(); + + expect(methodName, 'getUserConsent'); + expect(userConsent, UserConsent.declined); + }); + + test('set user consent enabled', () { + _platform.setUserConsent(true); + + expect(methodName, 'setUserConsent'); + expect(methodArguments, {'value': true}); + }); + + test('set user consent disabled', () { + _platform.setUserConsent(false); + + expect(methodName, 'setUserConsent'); + expect(methodArguments, {'value': false}); + }); + }); +} diff --git a/splitio_platform_interface/test/split_event_method_call_handler_test.dart b/splitio_platform_interface/test/split_event_method_call_handler_test.dart new file mode 100644 index 0000000..7d2f760 --- /dev/null +++ b/splitio_platform_interface/test/split_event_method_call_handler_test.dart @@ -0,0 +1,99 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_platform_interface/events/split_method_call_handler.dart'; + +void main() { + const MethodChannel _channel = MethodChannel('splitio'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + SplitEventMethodCallHandler splitEventMethodCallHandler = + SplitEventMethodCallHandler('key', 'bucketing'); + + void _simulateMethodInvocation(String methodName, + {String key = 'key', String bucketingKey = 'bucketing'}) { + _channel.invokeMethod( + methodName, {'matchingKey': key, 'bucketingKey': bucketingKey}); + } + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_channel, (MethodCall methodCall) { + splitEventMethodCallHandler.handle( + methodCall.method, methodCall.arguments); + return null; + }); + }); + + group('client events', () { + test('test client ready', () async { + Future future = + splitEventMethodCallHandler.onReady().then((value) => true); + _simulateMethodInvocation('clientReady'); + + expect(future, completion(equals(true))); + }); + + test('test multiple client ready', () async { + Future future = + splitEventMethodCallHandler.onReady().then((value) => true); + _simulateMethodInvocation('clientReady'); + + expect(future, completion(equals(true))); + + _simulateMethodInvocation('clientReady'); + expect(future, completion(equals(true))); + }); + + test('test client ready from cache', () async { + Future future = + splitEventMethodCallHandler.onReadyFromCache().then((value) => true); + _simulateMethodInvocation('clientReadyFromCache'); + + expect(future, completion(equals(true))); + }); + + test('test multiple client ready from cache', () async { + Future future = + splitEventMethodCallHandler.onReady().then((value) => true); + _simulateMethodInvocation('clientReadyFromCache'); + + expect(future, completion(equals(true))); + + _simulateMethodInvocation('clientReadyFromCache'); + expect(future, completion(equals(true))); + }); + + test('test client timeout', () async { + Future future = + splitEventMethodCallHandler.onTimeout().then((value) => true); + _simulateMethodInvocation('clientTimeout'); + + expect(future, completion(equals(true))); + }); + + test('test multiple client timeout', () async { + Future future = + splitEventMethodCallHandler.onReady().then((value) => true); + _simulateMethodInvocation('clientTimeout'); + + expect(future, completion(equals(true))); + + _simulateMethodInvocation('clientTimeout'); + expect(future, completion(equals(true))); + }); + + test('test client updated', () async { + var count = 0; + splitEventMethodCallHandler + .onUpdated() + .map((event) => ++count) + .take(3) + .toList() + .then((value) => expect(value, [1, 2])); + + _simulateMethodInvocation('clientUpdated'); + _simulateMethodInvocation('clientUpdated'); + }); + }); +} diff --git a/test/split_impression_test.dart b/splitio_platform_interface/test/split_impression_test.dart similarity index 94% rename from test/split_impression_test.dart rename to splitio_platform_interface/test/split_impression_test.dart index e792441..b993d2a 100644 --- a/test/split_impression_test.dart +++ b/splitio_platform_interface/test/split_impression_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:splitio/impressions/split_impression.dart'; +import 'package:splitio_platform_interface/split_impression.dart'; void main() { test('from map creates correct Impression', () { diff --git a/test/split_view_test.dart b/splitio_platform_interface/test/split_view_test.dart similarity index 93% rename from test/split_view_test.dart rename to splitio_platform_interface/test/split_view_test.dart index af0578a..0268b42 100644 --- a/test/split_view_test.dart +++ b/splitio_platform_interface/test/split_view_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:splitio/split_view.dart'; +import 'package:splitio_platform_interface/split_view.dart'; void main() { test('split view from empty map results in null object', () { diff --git a/test/splitio_configuration_test.dart b/splitio_platform_interface/test/splitio_configuration_test.dart similarity index 66% rename from test/splitio_configuration_test.dart rename to splitio_platform_interface/test/splitio_configuration_test.dart index c04ed72..0e22f77 100644 --- a/test/splitio_configuration_test.dart +++ b/splitio_platform_interface/test/splitio_configuration_test.dart @@ -1,10 +1,10 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:splitio/split_configuration.dart'; +import 'package:splitio_platform_interface/split_configuration.dart'; +import 'package:splitio_platform_interface/split_sync_config.dart'; void main() { test('valuesAreMappedCorrectly', () async { final SplitConfiguration config = SplitConfiguration( - enableDebug: true, eventFlushInterval: 2000, eventsPerPush: 300, eventsQueueSize: 250, @@ -21,7 +21,16 @@ void main() { eventsEndpoint: 'eventsEndpoint.split.io', authServiceEndpoint: 'authServiceEndpoint.split.io', streamingServiceEndpoint: 'streamingServiceEndpoint.split.io', - telemetryServiceEndpoint: 'telemetryServiceEndpoint.split.io'); + telemetryServiceEndpoint: 'telemetryServiceEndpoint.split.io', + syncConfig: + SyncConfig(names: ['one', 'two', 'three'], prefixes: ['pre1']), + impressionsMode: ImpressionsMode.none, + syncEnabled: false, + userConsent: UserConsent.declined, + encryptionEnabled: true, + logLevel: SplitLogLevel.debug, + readyTimeout: 1 + ); expect(config.configurationMap['eventFlushInterval'], 2000); expect(config.configurationMap['eventsPerPush'], 300); @@ -44,11 +53,22 @@ void main() { expect(config.configurationMap['telemetryServiceEndpoint'], 'telemetryServiceEndpoint.split.io'); expect(config.configurationMap['impressionListener'], true); + expect(config.configurationMap['syncConfig']['syncConfigNames'], + ['one', 'two', 'three']); + expect( + config.configurationMap['syncConfig']['syncConfigPrefixes'], ['pre1']); + expect(config.configurationMap['impressionsMode'], 'none'); + expect(config.configurationMap['syncEnabled'], false); + expect(config.configurationMap['userConsent'], 'declined'); + expect(config.configurationMap['encryptionEnabled'], true); + expect(config.configurationMap['logLevel'], 'debug'); + expect(config.configurationMap['readyTimeout'], 1); }); test('noSpecialValuesLeavesMapEmpty', () async { final SplitConfiguration config = SplitConfiguration(); - expect(config.configurationMap.isEmpty, true); + expect(config.configurationMap.length, 1); + expect(config.configurationMap['readyTimeout'], 10); }); } diff --git a/test/method_channel_manager_test.dart b/test/method_channel_manager_test.dart deleted file mode 100644 index 0c98d25..0000000 --- a/test/method_channel_manager_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:splitio/channel/method_channel_manager.dart'; -import 'package:splitio/method_call_handler.dart'; - -void main() { - late MethodChannelStub _channel; - late MethodChannelManager _methodChannelManager; - - setUp(() { - _channel = MethodChannelStub('mock'); - TestWidgetsFlutterBinding.ensureInitialized(); - _methodChannelManager = MethodChannelManager(_channel); - }); - - test('handle calls are delegated to each handler', () { - var mockMethodHandler1 = MethodCallHandlerStub(); - var mockMethodHandler2 = MethodCallHandlerStub(); - var mockMethodHandler3 = MethodCallHandlerStub(); - _methodChannelManager.addHandler(mockMethodHandler1); - _methodChannelManager.addHandler(mockMethodHandler2); - _methodChannelManager.addHandler(mockMethodHandler3); - - _methodChannelManager.handle(const MethodCall('test-method')); - - expect(mockMethodHandler1.handledCalls, {'test-method'}); - expect(mockMethodHandler2.handledCalls, {'test-method'}); - expect(mockMethodHandler3.handledCalls, {'test-method'}); - }); - - test('invokeMethod calls are delegated to channel', () { - _methodChannelManager.invokeMethod('any-method'); - - expect(_channel.calledMethods, {'invokeMethod'}); - }); - - test('invokeMapMethod calls are delegated to channel', () { - _methodChannelManager.invokeMapMethod('any-method'); - - expect(_channel.calledMethods, {'invokeMapMethod'}); - }); - - test('invokeListMethod calls are delegated to channel', () { - _methodChannelManager.invokeListMethod('any-method'); - - expect(_channel.calledMethods, {'invokeListMethod'}); - }); - - test('removed call handlers do not handle methods', () { - final MethodCallHandlerStub handler1 = MethodCallHandlerStub(); - final MethodCallHandlerStub handler2 = MethodCallHandlerStub(); - final MethodCallHandlerStub handler3 = MethodCallHandlerStub(); - _methodChannelManager.addHandler(handler1); - _methodChannelManager.addHandler(handler2); - _methodChannelManager.addHandler(handler3); - - _methodChannelManager.removeHandler(handler2); - - _methodChannelManager.handle(const MethodCall('test-method')); - - expect(handler1.handledCalls, {'test-method'}); - expect(handler2.handledCalls, isEmpty); - expect(handler3.handledCalls, {'test-method'}); - }); -} - -class MethodCallHandlerStub extends MethodCallHandler { - final Set handledCalls = {}; - - @override - Future handle(MethodCall call) async { - handledCalls.add(call.method); - } -} - -class MethodChannelStub extends MethodChannel { - MethodChannelStub(String name) : super(name); - Set calledMethods = {}; - - @override - Future invokeMethod(String method, [dynamic arguments]) async { - calledMethods.add('invokeMethod'); - return Future.value(null); - } - - @override - Future?> invokeMapMethod(String method, - [dynamic arguments]) async { - calledMethods.add('invokeMapMethod'); - return Future.value(null); - } - - @override - Future?> invokeListMethod(String method, - [dynamic arguments]) async { - calledMethods.add('invokeListMethod'); - return Future.value(null); - } -} diff --git a/test/split_event_listener_test.dart b/test/split_event_listener_test.dart deleted file mode 100644 index 3099e8f..0000000 --- a/test/split_event_listener_test.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:splitio/channel/method_channel_manager.dart'; -import 'package:splitio/events/split_events_listener.dart'; -import 'package:splitio/events/split_method_call_handler.dart'; -import 'package:splitio/split_client.dart'; -import 'package:splitio/split_result.dart'; - -void main() { - const MethodChannel _channel = MethodChannel('splitio'); - - TestWidgetsFlutterBinding.ensureInitialized(); - - SplitClientMock splitClientMock = SplitClientMock(); - SplitEventMethodCallHandler splitEventMethodCallHandler = - SplitEventMethodCallHandler('key', 'bucketing', splitClientMock); - late final MethodChannelManager _methodChannelWrapper = - MethodChannelManager(_channel); - - void _simulateMethodInvocation(String methodName, - {String key = 'key', String bucketingKey = 'bucketing'}) { - _channel.invokeMethod( - methodName, {'matchingKey': key, 'bucketingKey': bucketingKey}); - } - - setUp(() { - _channel.setMockMethodCallHandler((MethodCall methodCall) { - splitEventMethodCallHandler.handle(methodCall); - }); - }); - - group('client events', () { - test('test client ready', () async { - SplitEventsListener eventListener = DefaultEventsListener( - _methodChannelWrapper, splitEventMethodCallHandler); - Future future = eventListener.onReady().then((value) => true); - _simulateMethodInvocation('clientReady'); - - expect(future, completion(equals(true))); - }); - - test('test multiple client ready', () async { - SplitEventsListener eventListener = DefaultEventsListener( - _methodChannelWrapper, splitEventMethodCallHandler); - Future future = eventListener.onReady().then((value) => true); - _simulateMethodInvocation('clientReady'); - - expect(future, completion(equals(true))); - - _simulateMethodInvocation('clientReady'); - expect(future, completion(equals(true))); - }); - - test('test client ready from cache', () async { - SplitEventsListener eventListener = DefaultEventsListener( - _methodChannelWrapper, splitEventMethodCallHandler); - Future future = - eventListener.onReadyFromCache().then((value) => true); - _simulateMethodInvocation('clientReadyFromCache'); - - expect(future, completion(equals(true))); - }); - - test('test multiple client ready from cache', () async { - SplitEventsListener eventListener = DefaultEventsListener( - _methodChannelWrapper, splitEventMethodCallHandler); - Future future = eventListener.onReady().then((value) => true); - _simulateMethodInvocation('clientReadyFromCache'); - - expect(future, completion(equals(true))); - - _simulateMethodInvocation('clientReadyFromCache'); - expect(future, completion(equals(true))); - }); - - test('test client timeout', () async { - SplitEventsListener eventListener = DefaultEventsListener( - _methodChannelWrapper, splitEventMethodCallHandler); - Future future = eventListener.onTimeout().then((value) => true); - _simulateMethodInvocation('clientTimeout'); - - expect(future, completion(equals(true))); - }); - - test('test multiple client timeout', () async { - SplitEventsListener eventListener = DefaultEventsListener( - _methodChannelWrapper, splitEventMethodCallHandler); - Future future = eventListener.onReady().then((value) => true); - _simulateMethodInvocation('clientTimeout'); - - expect(future, completion(equals(true))); - - _simulateMethodInvocation('clientTimeout'); - expect(future, completion(equals(true))); - }); - - test('test client updated', () async { - SplitEventsListener eventListener = DefaultEventsListener( - _methodChannelWrapper, splitEventMethodCallHandler); - var count = 0; - eventListener - .onUpdated() - .map((event) => ++count) - .take(3) - .toList() - .then((value) => expect(value, [1, 2])); - - _simulateMethodInvocation('clientUpdated'); - _simulateMethodInvocation('clientUpdated'); - }); - }); -} - -class SplitClientMock extends SplitClient { - Map calledMethods = {}; - - @override - Future clearAttributes() { - calledMethods.update('clearAttributes', (value) => value + 1, - ifAbsent: () => 1); - return Future.value(true); - } - - @override - Future destroy() { - calledMethods.update('destroy', (value) => value + 1, ifAbsent: () => 1); - return Future.value(null); - } - - @override - Future flush() { - calledMethods.update('flush', (value) => value + 1, ifAbsent: () => 1); - return Future.value(null); - } - - @override - Future getAttribute(String attributeName) { - calledMethods.update('getAttribute', (value) => value + 1, - ifAbsent: () => 1); - return Future.value('value'); - } - - @override - Future> getAttributes() { - calledMethods.update('getAttributes', (value) => value + 1, - ifAbsent: () => 1); - return Future.value({'attr1': 'value1'}); - } - - @override - Future getTreatment(String splitName, - [Map attributes = const {}]) { - calledMethods.update('getTreatment', (value) => value + 1, - ifAbsent: () => 1); - return Future.value('treatment'); - } - - @override - Future getTreatmentWithConfig(String splitName, - [Map attributes = const {}]) { - calledMethods.update('getTreatmentWithConfig', (value) => value + 1, - ifAbsent: () => 1); - return Future.value(const SplitResult('treatment', null)); - } - - @override - Future> getTreatments(List splitNames, - [Map attributes = const {}]) { - calledMethods.update('getTreatments', (value) => value + 1, - ifAbsent: () => 1); - return Future.value({'split1': 'treatment'}); - } - - @override - Future> getTreatmentsWithConfig( - List splitNames, - [Map attributes = const {}]) { - calledMethods.update('getTreatmentsWithConfig', (value) => value + 1, - ifAbsent: () => 1); - return Future.value({'split1': const SplitResult('treatment', null)}); - } - - @override - Future whenReady() { - calledMethods.update('onReady', (value) => value + 1, ifAbsent: () => 1); - return Future.value(this); - } - - @override - Future whenReadyFromCache() { - calledMethods.update('onReadyFromCache', (value) => value + 1, - ifAbsent: () => 1); - return Future.value(this); - } - - @override - Future whenTimeout() { - calledMethods.update('onTimeout', (value) => value + 1, ifAbsent: () => 1); - return Future.value(this); - } - - @override - Stream whenUpdated() { - calledMethods.update('onUpdated', (value) => value + 1, ifAbsent: () => 1); - return Stream.value(this); - } - - @override - Future removeAttribute(String attributeName) { - calledMethods.update('removeAttribute', (value) => value + 1, - ifAbsent: () => 1); - return Future.value(true); - } - - @override - Future setAttribute(String attributeName, value) { - calledMethods.update('setAttribute', (value) => value + 1, - ifAbsent: () => 1); - return Future.value(true); - } - - @override - Future setAttributes(Map attributes) { - calledMethods.update('setAttributes', (value) => value + 1, - ifAbsent: () => 1); - return Future.value(true); - } - - @override - Future track(String eventType, - {String? trafficType, - double? value, - Map properties = const {}}) { - calledMethods.update('track', (value) => value + 1, ifAbsent: () => 1); - return Future.value(true); - } -} diff --git a/test/splitio_client_test.dart b/test/splitio_client_test.dart deleted file mode 100644 index 52e7a44..0000000 --- a/test/splitio_client_test.dart +++ /dev/null @@ -1,421 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:splitio/channel/method_channel_manager.dart'; -import 'package:splitio/events/split_events_listener.dart'; -import 'package:splitio/split_client.dart'; - -void main() { - const MethodChannel _channel = MethodChannel('splitio'); - - String methodName = ''; - dynamic methodArguments; - - TestWidgetsFlutterBinding.ensureInitialized(); - - MethodChannelManager _methodChannelWrapper = MethodChannelManager(_channel); - - SplitClient _getClient([SplitEventsListener? splitEventsListener]) { - if (splitEventsListener != null) { - return DefaultSplitClient.withEventListener(_methodChannelWrapper, - 'matching-key', 'bucketing-key', splitEventsListener); - } - - return DefaultSplitClient( - _methodChannelWrapper, 'matching-key', 'bucketing-key'); - } - - setUp(() { - _channel.setMockMethodCallHandler((MethodCall methodCall) async { - methodName = methodCall.method; - methodArguments = methodCall.arguments; - - switch (methodCall.method) { - case 'getTreatment': - return ''; - case 'getTreatments': - return {'split1': 'on', 'split2': 'off'}; - case 'getTreatmentsWithConfig': - return { - 'split1': {'treatment': 'on', 'config': null}, - 'split2': {'treatment': 'off', 'config': null} - }; - case 'track': - return true; - case 'getAttribute': - return true; - case 'getAllAttributes': - return { - 'attr_1': true, - 'attr_2': ['list-element'], - 'attr_3': 28.20 - }; - case 'setAttribute': - case 'setAttributes': - case 'removeAttribute': - case 'clearAttributes': - return true; - } - }); - }); - - group('evaluation', () { - test('getTreatment without attributes', () async { - SplitClient client = _getClient(); - - client.getTreatment('split'); - - expect(methodName, 'getTreatment'); - expect(methodArguments, { - 'splitName': 'split', - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributes': {} - }); - }); - - test('getTreatment with attributes', () async { - SplitClient client = _getClient(); - - client.getTreatment('split', {'attr1': true}); - - expect(methodName, 'getTreatment'); - expect(methodArguments, { - 'splitName': 'split', - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributes': {'attr1': true} - }); - }); - - test('getTreatments without attributes', () async { - SplitClient client = _getClient(); - - client.getTreatments(['split1', 'split2']); - - expect(methodName, 'getTreatments'); - expect(methodArguments, { - 'splitName': ['split1', 'split2'], - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributes': {} - }); - }); - - test('getTreatments with attributes', () async { - SplitClient client = _getClient(); - - client.getTreatments(['split1', 'split2'], {'attr1': true}); - - expect(methodName, 'getTreatments'); - expect(methodArguments, { - 'splitName': ['split1', 'split2'], - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributes': {'attr1': true} - }); - }); - - test('getTreatmentWithConfig with attributes', () async { - SplitClient client = _getClient(); - - client.getTreatmentWithConfig('split1', {'attr1': true}); - - expect(methodName, 'getTreatmentWithConfig'); - expect(methodArguments, { - 'splitName': 'split1', - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributes': {'attr1': true} - }); - }); - - test('getTreatmentWithConfig without attributes', () async { - SplitClient client = _getClient(); - - client.getTreatmentWithConfig('split1'); - - expect(methodName, 'getTreatmentWithConfig'); - expect(methodArguments, { - 'splitName': 'split1', - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributes': {} - }); - }); - - test('getTreatmentsWithConfig without attributes', () async { - SplitClient client = _getClient(); - - client.getTreatmentsWithConfig(['split1', 'split2']); - - expect(methodName, 'getTreatmentsWithConfig'); - expect(methodArguments, { - 'splitName': ['split1', 'split2'], - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributes': {} - }); - }); - - test('getTreatmentsWithConfig with attributes', () async { - SplitClient client = _getClient(); - - client.getTreatmentsWithConfig(['split1', 'split2'], {'attr1': true}); - - expect(methodName, 'getTreatmentsWithConfig'); - expect(methodArguments, { - 'splitName': ['split1', 'split2'], - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributes': {'attr1': true} - }); - }); - }); - - group('track', () { - test('track with traffic type & value', () async { - SplitClient client = _getClient(); - - client.track('my_event', trafficType: 'my_traffic_type', value: 25.10); - expect(methodName, 'track'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'eventType': 'my_event', - 'trafficType': 'my_traffic_type', - 'value': 25.10 - }); - }); - - test('track with value', () async { - SplitClient client = _getClient(); - - client.track('my_event', value: 25.10); - expect(methodName, 'track'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'eventType': 'my_event', - 'value': 25.10 - }); - }); - - test('track with traffic type', () async { - SplitClient client = _getClient(); - - client.track('my_event', trafficType: 'my_traffic_type'); - expect(methodName, 'track'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'eventType': 'my_event', - 'trafficType': 'my_traffic_type', - }); - }); - }); - - group('attributes', () { - test('get single attribute', () async { - SplitClient client = _getClient(); - - client.getAttribute('attribute-name'); - expect(methodName, 'getAttribute'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributeName': 'attribute-name', - }); - }); - - test('get all attributes', () async { - SplitClient client = _getClient(); - - client.getAttributes(); - expect(methodName, 'getAllAttributes'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - }); - }); - - test('set attribute', () async { - SplitClient client = _getClient(); - - client.setAttribute('my_attr', 'attr_value'); - expect(methodName, 'setAttribute'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributeName': 'my_attr', - 'value': 'attr_value', - }); - }); - - test('set multiple attributes', () async { - SplitClient client = _getClient(); - - client.setAttributes({ - 'bool_attr': true, - 'number_attr': 25.56, - 'string_attr': 'attr-value', - 'list_attr': ['one', 'two'], - }); - expect(methodName, 'setAttributes'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributes': { - 'bool_attr': true, - 'number_attr': 25.56, - 'string_attr': 'attr-value', - 'list_attr': ['one', 'two'], - } - }); - }); - - test('remove attribute', () async { - SplitClient client = _getClient(); - - client.removeAttribute('attr-name'); - expect(methodName, 'removeAttribute'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - 'attributeName': 'attr-name', - }); - }); - - test('clear attributes', () async { - SplitClient client = _getClient(); - - client.clearAttributes(); - expect(methodName, 'clearAttributes'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - }); - }); - - test('flush', () async { - SplitClient client = _getClient(); - - client.flush(); - expect(methodName, 'flush'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - }); - }); - - test('destroy', () async { - var splitEventsListenerStub = SplitEventsListenerStub(); - SplitClient client = _getClient(splitEventsListenerStub); - - client.destroy(); - - expect(splitEventsListenerStub.calledMethods['destroy'], 1); - - client.destroy(); - expect(methodName, 'destroy'); - expect(methodArguments, { - 'matchingKey': 'matching-key', - 'bucketingKey': 'bucketing-key', - }); - }); - }); - - group('events', () { - test('onReady is returned from events listener', () { - var splitEventsListenerStub = SplitEventsListenerStub(); - SplitClient client = _getClient(splitEventsListenerStub); - splitEventsListenerStub.attachClient(client); - - var future = client.whenReady().then((value) => client == value); - expect(splitEventsListenerStub.calledMethods['onReady'], 1); - expect(splitEventsListenerStub.calledMethods['onReadyFromCache'], null); - expect(splitEventsListenerStub.calledMethods['onTimeout'], null); - expect(splitEventsListenerStub.calledMethods['onUpdated'], null); - expect(future, completion(equals(true))); - }); - - test('onReadyFromCache is returned from events listener', () { - var splitEventsListenerStub = SplitEventsListenerStub(); - SplitClient client = _getClient(splitEventsListenerStub); - splitEventsListenerStub.attachClient(client); - - var future = client.whenReadyFromCache().then((value) => client == value); - expect(splitEventsListenerStub.calledMethods['onReady'], null); - expect(splitEventsListenerStub.calledMethods['onReadyFromCache'], 1); - expect(splitEventsListenerStub.calledMethods['onTimeout'], null); - expect(splitEventsListenerStub.calledMethods['onUpdated'], null); - expect(future, completion(equals(true))); - }); - - test('onTimeout is returned from events listener', () { - var splitEventsListenerStub = SplitEventsListenerStub(); - SplitClient client = _getClient(splitEventsListenerStub); - splitEventsListenerStub.attachClient(client); - - var future = client.whenTimeout().then((value) => client == value); - expect(splitEventsListenerStub.calledMethods['onReady'], null); - expect(splitEventsListenerStub.calledMethods['onReadyFromCache'], null); - expect(splitEventsListenerStub.calledMethods['onTimeout'], 1); - expect(splitEventsListenerStub.calledMethods['onUpdated'], null); - expect(future, completion(equals(true))); - }); - - test('onUpdated is returned from events listener', () { - var splitEventsListenerStub = SplitEventsListenerStub(); - SplitClient client = _getClient(splitEventsListenerStub); - splitEventsListenerStub.attachClient(client); - - var future = - client.whenUpdated().first.then(((value) => client == value)); - expect(splitEventsListenerStub.calledMethods['onReady'], null); - expect(splitEventsListenerStub.calledMethods['onReadyFromCache'], null); - expect(splitEventsListenerStub.calledMethods['onTimeout'], null); - expect(splitEventsListenerStub.calledMethods['onUpdated'], 1); - expect(future, completion(equals(true))); - }); - }); -} - -class SplitEventsListenerStub extends SplitEventsListener { - Map calledMethods = {}; - - late final Future _clientFuture; - - void attachClient(SplitClient splitClient) { - _clientFuture = Future.value(splitClient); - } - - @override - Future onReady() { - calledMethods.update('onReady', (value) => value + 1, ifAbsent: () => 1); - return _clientFuture; - } - - @override - Future onReadyFromCache() { - calledMethods.update('onReadyFromCache', (value) => value + 1, - ifAbsent: () => 1); - return _clientFuture; - } - - @override - Future onTimeout() { - calledMethods.update('onTimeout', (value) => value + 1, ifAbsent: () => 1); - return _clientFuture; - } - - @override - Stream onUpdated() { - calledMethods.update('onUpdated', (value) => value + 1, ifAbsent: () => 1); - return Stream.fromFuture(_clientFuture); - } - - @override - void destroy() { - calledMethods.update('destroy', (value) => value + 1, ifAbsent: () => 1); - } -}