diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 896a28a5..ff67f652 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -12,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -28,16 +23,16 @@ if (flutterVersionName == null) { } android { - namespace 'com.usercentrics.sdk.flutter_example' - compileSdk 35 + namespace "com.usercentrics.sdk.flutter_example" + compileSdk 36 compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -46,8 +41,8 @@ android { defaultConfig { applicationId "com.usercentrics.sdk.flutter_example" - minSdkVersion 21 // webview_flutter requirement - targetSdkVersion 31 + minSdk flutter.minSdkVersion + targetSdk 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true @@ -66,6 +61,5 @@ flutter { } dependencies { - implementation "androidx.multidex:multidex:$multidex_version" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.multidex:multidex:2.0.1" } diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 6093aa86..dc9f40ec 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,9 +18,9 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.3.2" apply false - id "com.android.library" version "8.3.2" apply false - id "org.jetbrains.kotlin.android" version "1.9.24" apply false + id "com.android.application" version "8.6.0" apply false + id "com.android.library" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ':app' diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/lib/main.dart b/example/lib/main.dart index 17e932f9..90091706 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -11,30 +11,9 @@ void main() { runApp(const MyApp()); } -class MyApp extends StatefulWidget { +class MyApp extends StatelessWidget { const MyApp({super.key}); - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - void initState() { - super.initState(); - - _initializeUsercentrics(); - } - - void _initializeUsercentrics() { - /// Initialize Usercentrics with your configuration only once. - /// We should not call `initialize` directly inside [build]. - Usercentrics.initialize( - settingsId: 'Yi9N3aXia', - loggerLevel: UsercentricsLoggerLevel.debug, - ); - } - @override Widget build(BuildContext context) { return const MaterialApp( @@ -43,6 +22,8 @@ class _MyAppState extends State { } } +enum _SdkStatus { idle, loading, ready, error } + class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -51,23 +32,39 @@ class HomePage extends StatefulWidget { } class HomePageState extends State { - @override - void initState() { - super.initState(); - _showCMPIfNeeded(); - } + _SdkStatus _sdkStatus = _SdkStatus.idle; + String? _statusMessage; + + void _initializeUsercentrics() async { + setState(() { + _sdkStatus = _SdkStatus.loading; + _statusMessage = null; + }); - void _showCMPIfNeeded() async { try { + Usercentrics.initialize( + settingsId: 'Yi9N3aXia', + loggerLevel: UsercentricsLoggerLevel.debug, + ); + final status = await Usercentrics.status; + setState(() { + _sdkStatus = _SdkStatus.ready; + _statusMessage = + 'SDK ready. shouldCollectConsent: ${status.shouldCollectConsent}'; + }); + if (status.shouldCollectConsent) { _showFirstLayer(); } else { applyConsent(status.consents); } } catch (error) { - // Handle non-localized error + setState(() { + _sdkStatus = _SdkStatus.error; + _statusMessage = error.toString(); + }); } } @@ -126,64 +123,104 @@ class HomePageState extends State { default: return const BannerSettings(/* Default Settings */); } - - // 'Activate with third-party tool' option - // final selectedVariant = WhateverTool.getABTestingVariant(); - // switch (variant) { - // case "variantA": - // return const BannerSettings(variantName: "variantA"); - // case "variantB": - // return const BannerSettings(variantName: "variantB"); - // default: - // return const BannerSettings(); - // } } @override Widget build(BuildContext context) { + final bool isSdkReady = _sdkStatus == _SdkStatus.ready; + return Scaffold( appBar: AppBar( title: const Text('Usercentrics Flutter Sample'), ), body: Padding( - padding: const EdgeInsets.all(50.0), + padding: const EdgeInsets.all(20.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Card( + color: _statusColor, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'SDK status: ${_sdkStatus.name.toUpperCase()}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + if (_statusMessage != null) ...[ + const SizedBox(height: 6), + Text( + _statusMessage!, + style: const TextStyle(fontSize: 12), + ), + ], + const SizedBox(height: 12), + ElevatedButton( + onPressed: _sdkStatus == _SdkStatus.loading + ? null + : _initializeUsercentrics, + child: _sdkStatus == _SdkStatus.loading + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Initialize SDK'), + ), + ], + ), + ), + ), + // ── END TEMPORARY ────────────────────────────────────────────── + const SizedBox(height: 16), ElevatedButton( - onPressed: () => _showFirstLayer(), + onPressed: isSdkReady ? () => _showFirstLayer() : null, child: const Text("Show First Layer"), ), ElevatedButton( - onPressed: () => _showSecondLayer(), + onPressed: isSdkReady ? () => _showSecondLayer() : null, child: const Text("Show Second Layer"), ), ElevatedButton( - onPressed: () => _showFirstLayer( - settings: bannerSettingsCustomizationExample1, - ), + onPressed: isSdkReady + ? () => _showFirstLayer( + settings: bannerSettingsCustomizationExample1, + ) + : null, child: const Text("Customization Example 1"), ), ElevatedButton( - onPressed: () => _showFirstLayer( - settings: bannerSettingsCustomizationExample2, - ), + onPressed: isSdkReady + ? () => _showFirstLayer( + settings: bannerSettingsCustomizationExample2, + ) + : null, child: const Text("Customization Example 2"), ), ElevatedButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const CustomUIPage()), - ), + onPressed: isSdkReady + ? () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CustomUIPage()), + ) + : null, child: const Text("Custom UI"), ), ElevatedButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const WebViewIntegrationPage()), - ), + onPressed: isSdkReady + ? () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const WebViewIntegrationPage()), + ) + : null, child: const Text("Webview Integration"), ), ], @@ -191,6 +228,19 @@ class HomePageState extends State { ), ); } + + Color get _statusColor { + switch (_sdkStatus) { + case _SdkStatus.idle: + return Colors.grey.shade200; + case _SdkStatus.loading: + return Colors.blue.shade50; + case _SdkStatus.ready: + return Colors.green.shade100; + case _SdkStatus.error: + return Colors.red.shade100; + } + } } void applyConsent(List? consents) { diff --git a/lib/src/internal/platform/method_channel_usercentrics.dart b/lib/src/internal/platform/method_channel_usercentrics.dart index 26badee1..01d85684 100644 --- a/lib/src/internal/platform/method_channel_usercentrics.dart +++ b/lib/src/internal/platform/method_channel_usercentrics.dart @@ -83,7 +83,12 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { isReadyCompleter = Completer(); if (ongoingInit != null) { - await ongoingInit.future; + try { + await ongoingInit.future; + } catch (error, stackTrace) { + debugPrint( + 'Usercentrics: Initialization failed, retrying. Error: $error\n$stackTrace'); + } } try { diff --git a/test/internal/bridge/fake_is_ready_bridge.dart b/test/internal/bridge/fake_is_ready_bridge.dart index 143ce388..34506ce4 100644 --- a/test/internal/bridge/fake_is_ready_bridge.dart +++ b/test/internal/bridge/fake_is_ready_bridge.dart @@ -3,10 +3,15 @@ import 'package:usercentrics_sdk/src/internal/bridge/bridge.dart'; import 'package:usercentrics_sdk/src/model/ready_status.dart'; class FakeIsReadyBridge extends IsReadyBridge { - FakeIsReadyBridge({this.invokeAnswer, this.shouldFailInitialization = false}); + FakeIsReadyBridge({ + this.invokeAnswer, + this.shouldFailInitialization = false, + this.failFirstNTimes = 0, + }); final UsercentricsReadyStatus? invokeAnswer; final bool shouldFailInitialization; + final int failFirstNTimes; var invokeCount = 0; MethodChannel? invokeChannelArgument; @@ -18,7 +23,7 @@ class FakeIsReadyBridge extends IsReadyBridge { invokeCount++; invokeChannelArgument = channel; - if (shouldFailInitialization) { + if (shouldFailInitialization || invokeCount <= failFirstNTimes) { throw PlatformException( code: 'usercentrics_flutter_isReady_error', message: 'Failed to initialize', diff --git a/test/internal/platform/method_channel_usercentrics_test.dart b/test/internal/platform/method_channel_usercentrics_test.dart index 15ec0d32..4d947c79 100644 --- a/test/internal/platform/method_channel_usercentrics_test.dart +++ b/test/internal/platform/method_channel_usercentrics_test.dart @@ -83,6 +83,42 @@ void main() { expect(instance.isReadyCompleter?.isCompleted, true); }); + test('retry succeeds after previous initialization failure', () async { + const successStatus = UsercentricsReadyStatus( + shouldCollectConsent: false, + consents: [], + geolocationRuleset: null, + location: UsercentricsLocation( + countryCode: "PT", + regionCode: "PT11", + isInEU: true, + isInUS: false, + isInCalifornia: false)); + + final initializeBridge = FakeInitializeBridge(); + final isReadyBridge = FakeIsReadyBridge( + invokeAnswer: successStatus, + failFirstNTimes: 1, + ); + final instance = MethodChannelUsercentrics( + initializeBridge: initializeBridge, + isReadyBridge: isReadyBridge, + ); + + instance.initialize(settingsId: "ABC"); + await expectLater( + instance.isReadyCompleter!.future, + throwsA(isA()), + ); + + instance.initialize(settingsId: "ABC"); + await instance.isReadyCompleter?.future; + + expect(initializeBridge.invokeCount, 2); + expect(instance.isReadyCompleter?.isCompleted, true); + expect(isReadyBridge.invokeCount, 2); + }); + test('expose stackTrace when initialization fails', () async { final initializeBridge = FakeInitializeBridge();