Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ CLAUDE.md
.swiftpm/
**/Flutter/ephemeral/
**/xcshareddata/swiftpm/
ios/didomi_sdk/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
2 changes: 1 addition & 1 deletion example/integration_test/purpose_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
106 changes: 106 additions & 0 deletions example/integration_test/widget_events_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
8 changes: 8 additions & 0 deletions example/lib/events_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () {
Comment thread
nicolas-chaix-didomi marked this conversation as resolved.
onEvent("Widget hidden");
};

DidomiSdk.addEventListener(didomiListener);
}

Expand Down
10 changes: 10 additions & 0 deletions ios/didomi_sdk/Sources/DidomiSwift/DidomiEventStreamHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand Down
11 changes: 11 additions & 0 deletions lib/events/event_listener.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 = () {};
}
21 changes: 21 additions & 0 deletions lib/events/events_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions lib/events/show_widget_event.dart
Original file line number Diff line number Diff line change
@@ -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);
}