From 5fa1444f6c7f8a7b35fc8ef94dd56c924003517e Mon Sep 17 00:00:00 2001 From: Nicolas CHAIX Date: Mon, 3 Aug 2026 15:00:51 +0200 Subject: [PATCH 1/3] Support new showWidget and hideWidget events Native SDKs 2.47.0 introduce ShowWidgetEvent and HideWidgetEvent, emitted by the Web SDK when a widget is displayed or hidden. Bridge them to Flutter: - Android: DidomiEventListener declares showWidget/hideWidget as abstract, so the plugin did not compile against 2.47.0 until these overrides were added. - iOS: the SDK names the payload property `widgetID` while Android names it `widgetId`; the wire key is normalised to `widgetId` so a single Dart lookup works on both platforms. - Dart: onShowWidget carries a ShowWidgetEvent with nullable widgetId and layerName. Both are read with an `as String?` cast rather than toString(), because Android sends explicit nulls and iOS omits nil keys entirely, and toString() would turn either into the literal string "null". - onHideWidget is a no-arg callback since HideWidgetEvent carries no data. Widget events originate in a WebView-rendered experience and the example app's configuration serves a native notice, so the integration test covers listener registration and absence of regressions rather than real event delivery; the limitation is documented in the test. --- .gitignore | 1 + CHANGELOG.md | 1 + .../fluttersdk/DidomiEventStreamHandler.kt | 12 ++ .../integration_test/widget_events_test.dart | 106 ++++++++++++++++++ example/lib/events_helper.dart | 8 ++ .../DidomiEventStreamHandler.swift | 10 ++ lib/events/event_listener.dart | 11 ++ lib/events/events_handler.dart | 21 ++++ lib/events/show_widget_event.dart | 10 ++ 9 files changed, 180 insertions(+) create mode 100644 example/integration_test/widget_events_test.dart create mode 100644 lib/events/show_widget_event.dart diff --git a/.gitignore b/.gitignore index 26afa441..6e52746e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ CLAUDE.md .swiftpm/ **/Flutter/ephemeral/ **/xcshareddata/swiftpm/ +ios/didomi_sdk/Package.resolved diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b4deb7..395c0f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.31.0 - Update latest versions of native Android (2.47.0) and iOS (2.47.0) sdks. +- Add support for `onShowWidget` and `onHideWidget` events. ## 2.30.0 - Update latest versions of native Android (2.46.0) and iOS (2.46.0) sdks. diff --git a/android/src/main/kotlin/io/didomi/fluttersdk/DidomiEventStreamHandler.kt b/android/src/main/kotlin/io/didomi/fluttersdk/DidomiEventStreamHandler.kt index ec390f03..1502d03f 100644 --- a/android/src/main/kotlin/io/didomi/fluttersdk/DidomiEventStreamHandler.kt +++ b/android/src/main/kotlin/io/didomi/fluttersdk/DidomiEventStreamHandler.kt @@ -203,6 +203,18 @@ class DidomiEventStreamHandler : EventChannel.StreamHandler, DidomiEventListener sendEvent("preferencesClickSPIPurposeSaveChoices") } + /* + * Widgets events + */ + + override fun showWidget(event: ShowWidgetEvent) { + sendEvent("onShowWidget", mapOf("widgetId" to event.widgetId, "layerName" to event.layerName)) + } + + override fun hideWidget(event: HideWidgetEvent) { + sendEvent("onHideWidget") + } + /* * Consent events */ diff --git a/example/integration_test/widget_events_test.dart b/example/integration_test/widget_events_test.dart new file mode 100644 index 00000000..dae3e771 --- /dev/null +++ b/example/integration_test/widget_events_test.dart @@ -0,0 +1,106 @@ +import 'dart:io'; + +import 'package:didomi_sdk/didomi_sdk.dart'; +import 'package:didomi_sdk/events/event_listener.dart'; +import 'package:didomi_sdk/events/show_widget_event.dart'; +import 'package:didomi_sdk_example/testapps/sample_for_notice_tests.dart' as app; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/initialize_helper.dart'; + +/// Coverage note: +/// +/// onShowWidget / onHideWidget are emitted by the Web SDK from inside a +/// WebView-rendered experience. The notice served by this example app's +/// configuration is natively rendered and contains no widget, and there is no +/// Dart-side API to trigger a widget, so real widget event delivery cannot be +/// produced here. +/// +/// This test therefore covers what is verifiable automatically: +/// - the new listener fields exist, are assignable and are accepted by +/// addEventListener; +/// - registering them does not disturb the normal event flow (onReady still +/// fires, no error is reported); +/// - no widget event is spuriously emitted against a non-widget config, which +/// would indicate a wire-string or dispatch mistake; +/// - both native handlers still build and register — a missing Kotlin override +/// or a Swift typo regresses here. +/// +/// Asserting a real onShowWidget payload requires a Didomi configuration whose +/// experience is web-rendered and serves a widget. See the manual verification +/// steps in the pull request description. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + final initializeBtnFinder = find.byKey(Key("initializeSmall")); + final setupUIBtnFinder = find.byKey(Key("setupUI")); + final showNoticeBtnFinder = find.byKey(Key("showNotice")); + + bool isError = false; + bool isReady = false; + bool widgetWasShown = false; + bool widgetWasHidden = false; + ShowWidgetEvent? lastShowWidgetEvent; + + final listener = EventListener(); + listener.onError = (String message) { + isError = true; + }; + listener.onReady = () { + isReady = true; + }; + listener.onShowWidget = (ShowWidgetEvent event) { + widgetWasShown = true; + lastShowWidgetEvent = event; + }; + listener.onHideWidget = () { + widgetWasHidden = true; + }; + + DidomiSdk.addEventListener(listener); + + group("Widget events", () { + testWidgets("No widget event before initialization", (WidgetTester tester) async { + // Start app + app.main(); + await tester.pumpAndSettle(); + + assert(isError == false); + assert(isReady == false); + assert(widgetWasShown == false); + assert(widgetWasHidden == false); + assert(lastShowWidgetEvent == null); + }); + + testWidgets("Widget event listeners do not disrupt the event flow", (WidgetTester tester) async { + // Start app + app.main(); + await tester.pumpAndSettle(); + + await InitializeHelper.initialize(tester, initializeBtnFinder); + + assert(isError == false); + assert(isReady == true); + + if (Platform.isIOS) { + await tester.tap(setupUIBtnFinder); + await tester.pumpAndSettle(); + } + + await tester.tap(showNoticeBtnFinder); + await tester.pumpAndSettle(); + + // Events coming from the native SDK are not flushed by pumpAndSettle. + await Future.delayed(Duration(seconds: 1)); + + // The notice served by this config is native and holds no widget, + // so no widget event must have been emitted. + assert(isError == false); + assert(widgetWasShown == false); + assert(widgetWasHidden == false); + assert(lastShowWidgetEvent == null); + }); + }); +} diff --git a/example/lib/events_helper.dart b/example/lib/events_helper.dart index bb666e76..b1f3db14 100644 --- a/example/lib/events_helper.dart +++ b/example/lib/events_helper.dart @@ -148,6 +148,14 @@ class EventsHelper { onEvent("Language has not changed: $reason"); }; + // Widgets events + didomiListener.onShowWidget = (event) { + onEvent("Widget displayed (widgetId: ${event.widgetId}, layerName: ${event.layerName})"); + }; + didomiListener.onHideWidget = () { + onEvent("Widget hidden"); + }; + DidomiSdk.addEventListener(didomiListener); } diff --git a/ios/didomi_sdk/Sources/DidomiSwift/DidomiEventStreamHandler.swift b/ios/didomi_sdk/Sources/DidomiSwift/DidomiEventStreamHandler.swift index 9b4a3392..a3c75260 100644 --- a/ios/didomi_sdk/Sources/DidomiSwift/DidomiEventStreamHandler.swift +++ b/ios/didomi_sdk/Sources/DidomiSwift/DidomiEventStreamHandler.swift @@ -175,6 +175,16 @@ class DidomiEventStreamHandler: NSObject, FlutterStreamHandler { eventListener.onIntegrationError = { [weak self] event in self?.sendEvent(eventType: "onIntegrationError", arguments: ["integrationName": event.integrationName, "reason": event.reason]) } + + // Widgets events + // Note: the iOS SDK names the property `widgetID`, Android names it `widgetId`. + // The wire key must stay `widgetId` so both platforms match the Dart handler. + eventListener.onShowWidget = { [weak self] event in + self?.sendEvent(eventType: "onShowWidget", arguments: ["widgetId": event.widgetID, "layerName": event.layerName]) + } + eventListener.onHideWidget = { [weak self] event in + self?.sendEvent(eventType: "onHideWidget") + } } func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { diff --git a/lib/events/event_listener.dart b/lib/events/event_listener.dart index 47a37347..e7d69dd4 100644 --- a/lib/events/event_listener.dart +++ b/lib/events/event_listener.dart @@ -1,6 +1,7 @@ import 'package:didomi_sdk/events/sync_ready_event.dart'; import 'integration_error_event.dart'; +import 'show_widget_event.dart'; /// Listener to events sent by the Didomi SDK class EventListener { @@ -179,4 +180,14 @@ class EventListener { /// Integration with an external SDK encountered an error dynamic Function(IntegrationErrorEvent event) onIntegrationError = (event) {}; + + /* + * Widgets events + */ + + /// A widget was displayed + dynamic Function(ShowWidgetEvent event) onShowWidget = (event) {}; + + /// A widget was hidden + dynamic Function() onHideWidget = () {}; } diff --git a/lib/events/events_handler.dart b/lib/events/events_handler.dart index 374ace94..ccc9b318 100644 --- a/lib/events/events_handler.dart +++ b/lib/events/events_handler.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'event_listener.dart'; import 'integration_error_event.dart'; +import 'show_widget_event.dart'; /// Handler for events emitted by native SDK class EventsHandler { @@ -327,6 +328,26 @@ class EventsHandler { } break; + /// Widgets events + + case "onShowWidget": + // Cast instead of toString(): both fields are nullable natively, and + // toString() would turn a null into the literal string "null". + final ShowWidgetEvent showWidgetEvent = ShowWidgetEvent( + event["widgetId"] as String?, + event["layerName"] as String?, + ); + for (var listener in listeners) { + listener.onShowWidget(showWidgetEvent); + } + break; + + case "onHideWidget": + for (var listener in listeners) { + listener.onHideWidget(); + } + break; + default: print("Received invalid event: $eventType"); break; diff --git a/lib/events/show_widget_event.dart b/lib/events/show_widget_event.dart new file mode 100644 index 00000000..0195a444 --- /dev/null +++ b/lib/events/show_widget_event.dart @@ -0,0 +1,10 @@ +/** A widget was displayed. */ +class ShowWidgetEvent { + // Identifier of the widget that was displayed, as resolved by the Rules Engine + String? widgetId; + + // Name of the layer at which the widget was displayed + String? layerName; + + ShowWidgetEvent(this.widgetId, this.layerName); +} From 2215af05a91e25f6597294c36a2fd5db670adf54 Mon Sep 17 00:00:00 2001 From: Nicolas CHAIX Date: Mon, 3 Aug 2026 15:14:20 +0200 Subject: [PATCH 2/3] Update purpose name assertion after GVL change IAB GVL v169 renamed special feature 2 from "Actively scan device characteristics for identification" to "Identify devices based on information actively requested", which broke the expected purpose names in purpose_test. The purpose IDs are unchanged, so only the display name assertion needed updating. --- example/integration_test/purpose_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/integration_test/purpose_test.dart b/example/integration_test/purpose_test.dart index 0fe01357..532d1899 100644 --- a/example/integration_test/purpose_test.dart +++ b/example/integration_test/purpose_test.dart @@ -17,7 +17,7 @@ void main() { const purpose1Name = "Store and/or access information on a device"; const purpose1Description = "Cookies, device or similar onl..."; const purposeNames = "$purpose1Name, " - "Create profiles for personalised advertising, Actively scan device characteristics for identification, " + "Create profiles for personalised advertising, Identify devices based on information actively requested, " "Use precise geolocation data, Develop and improve services, Understand audiences through statistics or " "combinations of data from different sources, Measure advertising performance, " "Use limited data to select advertising, Use profiles to select personalised advertising."; From 4f23a236d6427a3f00cdbbd5c3bc1937b014507e Mon Sep 17 00:00:00 2001 From: Nicolas CHAIX Date: Mon, 3 Aug 2026 19:02:57 +0200 Subject: [PATCH 3/3] remove change log --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 395c0f9e..93b4deb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ## 2.31.0 - Update latest versions of native Android (2.47.0) and iOS (2.47.0) sdks. -- Add support for `onShowWidget` and `onHideWidget` events. ## 2.30.0 - Update latest versions of native Android (2.46.0) and iOS (2.46.0) sdks.