diff --git a/apps/flutter_client_contract_test_service/pubspec.lock b/apps/flutter_client_contract_test_service/pubspec.lock index c256ee0a..6a71a8d7 100644 --- a/apps/flutter_client_contract_test_service/pubspec.lock +++ b/apps/flutter_client_contract_test_service/pubspec.lock @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -388,33 +388,36 @@ packages: source: hosted version: "6.8.0" launchdarkly_common_client: - dependency: "direct overridden" + dependency: transitive description: - path: "../../packages/common_client" - relative: true - source: path - version: "1.9.0" + name: launchdarkly_common_client + sha256: aeefe7bc4b7bf995fa23a5165846a7320aa1eaa48089f19d150a0144fbf5bfad + url: "https://pub.dev" + source: hosted + version: "1.10.0" launchdarkly_dart_common: - dependency: "direct overridden" + dependency: transitive description: - path: "../../packages/common" - relative: true - source: path - version: "1.8.0" + name: launchdarkly_dart_common + sha256: e36fb8d943ea7a5b99e7ecdc361174ca95e05533904dd92175816ae5c9723eee + url: "https://pub.dev" + source: hosted + version: "1.8.1" launchdarkly_event_source_client: - dependency: "direct overridden" + dependency: transitive description: - path: "../../packages/event_source_client" - relative: true - source: path - version: "2.0.1" + name: launchdarkly_event_source_client + sha256: c248a81bc353d44f7f18803ff625d784d640265bf8bbc32c63f1d2d91911b88f + url: "https://pub.dev" + source: hosted + version: "2.1.0" launchdarkly_flutter_client_sdk: dependency: "direct main" description: path: "../../packages/flutter_client_sdk" relative: true source: path - version: "4.15.0" + version: "4.16.0" lints: dependency: "direct dev" description: @@ -451,10 +454,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: diff --git a/apps/sse_contract_test_service/pubspec.lock b/apps/sse_contract_test_service/pubspec.lock index 6e42e705..b6af2058 100644 --- a/apps/sse_contract_test_service/pubspec.lock +++ b/apps/sse_contract_test_service/pubspec.lock @@ -311,7 +311,7 @@ packages: path: "../../packages/event_source_client" relative: true source: path - version: "2.0.1" + version: "2.1.0" lints: dependency: "direct dev" description: diff --git a/packages/flutter_client_sdk/example/ios/.gitignore b/packages/flutter_client_sdk/example/ios/.gitignore index 7a7f9873..b33aacc0 100644 --- a/packages/flutter_client_sdk/example/ios/.gitignore +++ b/packages/flutter_client_sdk/example/ios/.gitignore @@ -1,4 +1,7 @@ **/dgph +# CocoaPods (regenerated by `flutter run`/`pod install`) +Podfile +Podfile.lock *.mode1v3 *.mode2v3 *.moved-aside diff --git a/packages/flutter_client_sdk/example/macos/.gitignore b/packages/flutter_client_sdk/example/macos/.gitignore index 746adbb6..d04ed895 100644 --- a/packages/flutter_client_sdk/example/macos/.gitignore +++ b/packages/flutter_client_sdk/example/macos/.gitignore @@ -2,6 +2,10 @@ **/Flutter/ephemeral/ **/Pods/ +# CocoaPods (regenerated by `flutter run`/`pod install`) +Podfile +Podfile.lock + # Xcode-related **/dgph **/xcuserdata/ diff --git a/packages/flutter_client_sdk/lib/src/ld_client.dart b/packages/flutter_client_sdk/lib/src/ld_client.dart index ecaa3f47..0a13871b 100644 --- a/packages/flutter_client_sdk/lib/src/ld_client.dart +++ b/packages/flutter_client_sdk/lib/src/ld_client.dart @@ -6,6 +6,7 @@ import 'connection_manager.dart'; import 'flutter_state_detector.dart'; import 'persistence/shared_preferences_persistence.dart'; import 'platform_env_reporter.dart'; +import 'plugin.dart'; const sdkName = 'FlutterClientSdk'; const sdkVersion = '4.16.0'; // x-release-please-version @@ -38,6 +39,7 @@ const sdkVersion = '4.16.0'; // x-release-please-version interface class LDClient { late final LDCommonClient _client; late final ConnectionManager _connectionManager; + late final PluginEnvironmentMetadata _pluginEnvironmentMetadata; /// Stream which emits data source status changes. /// @@ -93,15 +95,14 @@ interface class LDClient { final sdkPluginMetadata = PluginSdkMetadata(name: sdkName, version: sdkVersion); + _pluginEnvironmentMetadata = PluginEnvironmentMetadata( + sdk: sdkPluginMetadata, + application: config.applicationInfo, + credential: PluginCredentialInfo( + type: _client.credentialType, value: config.sdkCredential)); + safeRegisterPlugins( - this, - PluginEnvironmentMetadata( - sdk: sdkPluginMetadata, - application: config.applicationInfo, - credential: PluginCredentialInfo( - type: _client.credentialType, value: config.sdkCredential)), - config.plugins, - config.logger); + this, _pluginEnvironmentMetadata, config.plugins, config.logger); } /// Initialize the SDK. @@ -358,4 +359,24 @@ interface class LDClient { void addHook(Hook hook) { _client.addHook(hook); } + + /// Registers a plugin with this SDK instance after the client has been + /// constructed. + /// + /// Bundled hooks from the plugin are added before [Plugin.register] is + /// invoked. If reading [Plugin.hooks] throws, the plugin is not registered. + /// If [Plugin.register] throws, the error is logged and not rethrown. + void registerPlugin(Plugin plugin) { + final hooks = safeGetPluginHooks(plugin, _client.logger); + if (hooks == null) { + return; + } + + for (final hook in hooks) { + addHook(hook); + } + + safeRegisterPlugins( + this, _pluginEnvironmentMetadata, [plugin], _client.logger); + } } diff --git a/packages/flutter_client_sdk/pubspec.yaml b/packages/flutter_client_sdk/pubspec.yaml index 7354b7e6..7d1e8f7b 100644 --- a/packages/flutter_client_sdk/pubspec.yaml +++ b/packages/flutter_client_sdk/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: sdk: flutter package_info_plus: ">=4.2.0 <11.0.0" device_info_plus: ">=9.1.1 <14.0.0" - launchdarkly_common_client: 1.9.0 + launchdarkly_common_client: 1.10.0 shared_preferences: ^2.2.2 connectivity_plus: ">=5.0.2 <8.0.0" web: ^1.1.1 diff --git a/packages/flutter_client_sdk/test/ld_client_plugin_test.dart b/packages/flutter_client_sdk/test/ld_client_plugin_test.dart index 0cf7b619..9b74a40a 100644 --- a/packages/flutter_client_sdk/test/ld_client_plugin_test.dart +++ b/packages/flutter_client_sdk/test/ld_client_plugin_test.dart @@ -70,20 +70,27 @@ final class TestPlugin extends Plugin { final String pluginName; final List _hooks; final bool shouldThrowOnRegister; + final bool shouldThrowOnGetHooks; final PluginMetadata _metadata; int registerCallCount = 0; LDClient? lastClientReceived; PluginEnvironmentMetadata? lastEnvironmentMetadataReceived; - TestPlugin(this.pluginName, this._hooks, {this.shouldThrowOnRegister = false}) + TestPlugin(this.pluginName, this._hooks, + {this.shouldThrowOnRegister = false, this.shouldThrowOnGetHooks = false}) : _metadata = PluginMetadata(name: pluginName); @override PluginMetadata get metadata => _metadata; @override - List get hooks => _hooks; + List get hooks { + if (shouldThrowOnGetHooks) { + throw Exception('Test exception from plugin $pluginName'); + } + return _hooks; + } @override void register( @@ -99,6 +106,29 @@ final class TestPlugin extends Plugin { } } +final class EvaluationOnRegisterPlugin extends Plugin { + final TestHook hook; + final PluginMetadata _metadata; + + int registerCallCount = 0; + + EvaluationOnRegisterPlugin(this.hook) + : _metadata = PluginMetadata(name: 'evaluation-on-register-plugin'); + + @override + PluginMetadata get metadata => _metadata; + + @override + List get hooks => [hook]; + + @override + void register( + LDClient client, PluginEnvironmentMetadata environmentMetadata) { + registerCallCount++; + client.boolVariation('test-flag', false); + } +} + final class _WifiConnected extends ConnectivityPlatform { final StreamController> _controller = StreamController(); @@ -393,4 +423,79 @@ void main() { client.close(); }); }); + + group('LDClient registerPlugin', () { + test('registers plugin with environment metadata', () { + final plugin = TestPlugin('runtime-plugin', []); + final client = createTestClient(); + + client.registerPlugin(plugin); + + expect(plugin.registerCallCount, equals(1)); + expect(plugin.lastClientReceived, same(client)); + expect(plugin.lastEnvironmentMetadataReceived, isNotNull); + expect(plugin.lastEnvironmentMetadataReceived!.sdk.name, + equals('FlutterClientSdk')); + expect(plugin.lastEnvironmentMetadataReceived!.credential.value, + equals('test-mobile-key')); + + client.close(); + }); + + test('activates bundled hooks after registerPlugin', () { + final hook = TestHook('runtime-plugin-hook'); + final plugin = TestPlugin('runtime-plugin', [hook]); + final client = createTestClient(); + + client.registerPlugin(plugin); + hook.callLog.clear(); + client.boolVariation('test-flag', false); + + expect(hook.callLog.any((call) => call.startsWith('beforeEvaluation')), + isTrue); + expect(hook.callLog.any((call) => call.startsWith('afterEvaluation')), + isTrue); + + client.close(); + }); + + test('adds hooks before plugin register is invoked', () { + final hook = TestHook('runtime-order-hook'); + final plugin = EvaluationOnRegisterPlugin(hook); + final client = createTestClient(); + + client.registerPlugin(plugin); + + expect(plugin.registerCallCount, equals(1)); + expect(hook.callLog.any((call) => call.startsWith('beforeEvaluation')), + isTrue); + expect(hook.callLog.any((call) => call.startsWith('afterEvaluation')), + isTrue); + + client.close(); + }); + + test('does not register plugin when hooks getter throws', () { + final plugin = + TestPlugin('bad-hooks-plugin', [], shouldThrowOnGetHooks: true); + final client = createTestClient(); + + client.registerPlugin(plugin); + + expect(plugin.registerCallCount, equals(0)); + + client.close(); + }); + + test('does not throw when plugin register throws', () { + final plugin = + TestPlugin('bad-register-plugin', [], shouldThrowOnRegister: true); + final client = createTestClient(); + + expect(() => client.registerPlugin(plugin), returnsNormally); + expect(plugin.registerCallCount, equals(1)); + + client.close(); + }); + }); }