From a783266bf9e00e25a1ab87a86296b42a31a7ccb1 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Tue, 29 Oct 2024 14:21:00 +0500 Subject: [PATCH 01/34] chore: update public API (#152) --- apps/amiapp_flutter/lib/main.dart | 2 +- apps/amiapp_flutter/lib/src/app.dart | 6 +- .../lib/src/screens/attributes.dart | 4 +- .../lib/src/screens/dashboard.dart | 14 +- .../lib/src/screens/events.dart | 2 +- apps/amiapp_flutter/pubspec.lock | 130 ++++---- customer_io.iml | 48 +++ lib/customer_io.dart | 117 ++++--- test/customer_io_test.dart | 299 ++++++++---------- 9 files changed, 340 insertions(+), 282 deletions(-) diff --git a/apps/amiapp_flutter/lib/main.dart b/apps/amiapp_flutter/lib/main.dart index b848d9e..afcda9a 100644 --- a/apps/amiapp_flutter/lib/main.dart +++ b/apps/amiapp_flutter/lib/main.dart @@ -41,7 +41,7 @@ void main() async { onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async { // Callback from `flutter_local_notifications` plugin for when a local notification is clicked. // Unfortunately, we are only able to get the payload object for the local push, not anything else such as title or body. - CustomerIO.track(name: "local push notification clicked", attributes: {"payload": notificationResponse.payload}); + CustomerIO.instance.track(name: "local push notification clicked", attributes: {"payload": notificationResponse.payload}); } ); diff --git a/apps/amiapp_flutter/lib/src/app.dart b/apps/amiapp_flutter/lib/src/app.dart index 18721f0..33f00be 100644 --- a/apps/amiapp_flutter/lib/src/app.dart +++ b/apps/amiapp_flutter/lib/src/app.dart @@ -79,7 +79,7 @@ class _AmiAppState extends State { onLogin: (user) { _auth.login(user).then((signedIn) { if (signedIn) { - CustomerIO.identify(identifier: user.email, attributes: { + CustomerIO.instance.identify(identifier: user.email, attributes: { "first_name": user.displayName, "email": user.email, "is_guest": user.isGuest, @@ -216,14 +216,14 @@ class _AmiAppState extends State { if (_customerIOSDK.sdkConfig?.screenTrackingEnabled == true) { final Screen? screen = _router.currentLocation().toAppScreen(); if (screen != null) { - CustomerIO.screen(name: screen.name); + CustomerIO.instance.screen(name: screen.name); } } } void _handleAuthStateChanged() { if (_auth.signedIn == false) { - CustomerIO.clearIdentify(); + CustomerIO.instance.clearIdentify(); _auth.clearUserState(); } } diff --git a/apps/amiapp_flutter/lib/src/screens/attributes.dart b/apps/amiapp_flutter/lib/src/screens/attributes.dart index 06fe50d..aa13119 100644 --- a/apps/amiapp_flutter/lib/src/screens/attributes.dart +++ b/apps/amiapp_flutter/lib/src/screens/attributes.dart @@ -155,11 +155,11 @@ class _AttributesScreenState extends State { }; switch (widget._attributeType) { case _attributeTypeDevice: - CustomerIO.setDeviceAttributes( + CustomerIO.instance.setDeviceAttributes( attributes: attributes); break; case _attributeTypeProfile: - CustomerIO.setProfileAttributes( + CustomerIO.instance.setProfileAttributes( attributes: attributes); break; } diff --git a/apps/amiapp_flutter/lib/src/screens/dashboard.dart b/apps/amiapp_flutter/lib/src/screens/dashboard.dart index 42ee056..1dd34a2 100644 --- a/apps/amiapp_flutter/lib/src/screens/dashboard.dart +++ b/apps/amiapp_flutter/lib/src/screens/dashboard.dart @@ -54,17 +54,17 @@ class _DashboardScreenState extends State { .then((value) => setState(() => _buildInfo = value)); inAppMessageStreamSubscription = - CustomerIO.subscribeToInAppEventListener(handleInAppEvent); + CustomerIO.instance.subscribeToInAppEventListener(handleInAppEvent); // Setup 3rd party SDK, flutter-fire. // We install this SDK into sample app to make sure the CIO SDK behaves as expected when there is another SDK installed that handles push notifications. FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { - CustomerIO.track(name: "push clicked", attributes: {"push": initialMessage?.notification?.title, "app-state": "killed"}); + CustomerIO.instance.track(name: "push clicked", attributes: {"push": initialMessage?.notification?.title, "app-state": "killed"}); }); // ...while app was in the background (but not killed). FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { - CustomerIO.track(name: "push clicked", attributes: {"push": message.notification?.title, "app-state": "background"}); + CustomerIO.instance.track(name: "push clicked", attributes: {"push": message.notification?.title, "app-state": "background"}); }); // Important that a 3rd party SDK can receive callbacks when a push is received while app in background. @@ -72,7 +72,7 @@ class _DashboardScreenState extends State { // Note: A push will not be shown on the device while app is in foreground. This is a FCM behavior, not a CIO SDK behavior. // If you send a push using Customer.io with the FCM service setup in Customer.io, the push will be shown on the device. FirebaseMessaging.onMessage.listen((RemoteMessage message) { - CustomerIO.track(name: "push received", attributes: {"push": message.notification?.title, "app-state": "foreground"}); + CustomerIO.instance.track(name: "push received", attributes: {"push": message.notification?.title, "app-state": "foreground"}); }); super.initState(); @@ -111,7 +111,7 @@ class _DashboardScreenState extends State { }; attributes.addAll(arguments); - CustomerIO.track( + CustomerIO.instance.track( name: 'In-App Event', attributes: attributes, ); @@ -174,9 +174,9 @@ class _ActionList extends StatelessWidget { final eventName = event.key; final attributes = event.value; if (attributes == null) { - CustomerIO.track(name: eventName); + CustomerIO.instance.track(name: eventName); } else { - CustomerIO.track(name: eventName, attributes: attributes); + CustomerIO.instance.track(name: eventName, attributes: attributes); } context.showSnackBar('Event sent successfully'); } diff --git a/apps/amiapp_flutter/lib/src/screens/events.dart b/apps/amiapp_flutter/lib/src/screens/events.dart index 98f3b48..cb79ec5 100644 --- a/apps/amiapp_flutter/lib/src/screens/events.dart +++ b/apps/amiapp_flutter/lib/src/screens/events.dart @@ -110,7 +110,7 @@ class _CustomEventScreenState extends State { attributes = propertyName.isEmpty ? {} : {propertyName: _propertyValueController.text}; - CustomerIO.track( + CustomerIO.instance.track( name: _eventNameController.text, attributes: attributes); _onEventTracked(); diff --git a/apps/amiapp_flutter/pubspec.lock b/apps/amiapp_flutter/pubspec.lock index e3501bf..519fb57 100644 --- a/apps/amiapp_flutter/pubspec.lock +++ b/apps/amiapp_flutter/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: b1595874fbc8f7a50da90f5d8f327bb0bfd6a95dc906c390efe991540c3b54aa + sha256: "5534e701a2c505fed1f0799e652dd6ae23bd4d2c4cf797220e5ced5764a7c1c2" url: "https://pub.dev" source: hosted - version: "1.3.40" + version: "1.3.44" archive: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: transitive description: @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -103,7 +103,7 @@ packages: path: "../.." relative: true source: path - version: "1.5.1" + version: "1.5.2" dbus: dependency: transitive description: @@ -132,66 +132,66 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "3187f4f8e49968573fd7403011dca67ba95aae419bc0d8131500fae160d94f92" + sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.6.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02" + sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: e8d1e22de72cb21cdcfc5eed7acddab3e99cd83f3b317f54f7a96c32f25fd11e + sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 url: "https://pub.dev" source: hosted - version: "2.17.4" + version: "2.18.1" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "1b0a4f9ecbaf9007771bac152afad738ddfacc4b8431a7591c00829480d99553" + sha256: eb6e28a3a35deda61fe8634967c84215efc19133ba58d8e0fc6c9a2af2cba05e url: "https://pub.dev" source: hosted - version: "15.0.4" + version: "15.1.3" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: c5a6443e66ae064fe186901d740ee7ce648ca2a6fd0484b8c5e963849ac0fc28 + sha256: b316c4ee10d93d32c033644207afc282d9b2b4372f3cf9c6022f3558b3873d2d url: "https://pub.dev" source: hosted - version: "4.5.42" + version: "4.5.46" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "232ef63b986467ae5b5577a09c2502b26e2e2aebab5b85e6c966a5ca9b038b89" + sha256: d7f0147a1a9fe4313168e20154a01fd5cf332898de1527d3930ff77b8c7f5387 url: "https://pub.dev" source: hosted - version: "3.8.12" + version: "3.9.2" flutter: dependency: "direct main" description: flutter @@ -201,10 +201,10 @@ packages: dependency: "direct main" description: name: flutter_dotenv - sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.2.1" flutter_launcher_icons: dependency: "direct dev" description: @@ -225,10 +225,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f + sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" url: "https://pub.dev" source: hosted - version: "17.2.2" + version: "17.2.4" flutter_local_notifications_linux: dependency: transitive description: @@ -259,10 +259,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe + sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc" url: "https://pub.dev" source: hosted - version: "14.2.3" + version: "14.3.0" http: dependency: transitive description: @@ -283,10 +283,10 @@ packages: dependency: transitive description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.0" json_annotation: dependency: transitive description: @@ -331,10 +331,10 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: @@ -363,10 +363,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" + sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998 url: "https://pub.dev" source: hosted - version: "8.0.1" + version: "8.1.0" package_info_plus_platform_interface: dependency: transitive description: @@ -419,10 +419,10 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: eaf2a1ec4472775451e88ca6a7b86559ef2f1d1ed903942ed135e38ea0097dca + sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" url: "https://pub.dev" source: hosted - version: "12.0.8" + version: "12.0.13" permission_handler_apple: dependency: transitive description: @@ -435,18 +435,18 @@ packages: dependency: transitive description: name: permission_handler_html - sha256: "6cac773d389e045a8d4f85418d07ad58ef9e42a56e063629ce14c4c26344de24" + sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3+2" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.2.3" permission_handler_windows: dependency: transitive description: @@ -467,10 +467,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -483,34 +483,34 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68 + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: @@ -523,18 +523,18 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -600,10 +600,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" vector_math: dependency: transitive description: @@ -624,26 +624,26 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" win32: dependency: transitive description: name: win32 - sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" + sha256: "10169d3934549017f0ae278ccb07f828f9d6ea21573bab0fb77b0e1ef0fce454" url: "https://pub.dev" source: hosted - version: "5.5.3" + version: "5.7.2" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -661,5 +661,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/customer_io.iml b/customer_io.iml index a8bedeb..1f1e608 100644 --- a/customer_io.iml +++ b/customer_io.iml @@ -20,6 +20,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/customer_io.dart b/lib/customer_io.dart index 41bc593..4584e5b 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -1,4 +1,7 @@ import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter/cupertino.dart'; import 'customer_io_config.dart'; import 'customer_io_enums.dart'; @@ -8,23 +11,73 @@ import 'messaging_in_app/platform_interface.dart'; import 'messaging_push/platform_interface.dart'; class CustomerIO { - const CustomerIO._(); + static CustomerIO? _instance; + + final CustomerIOPlatform _platform; + final CustomerIOMessagingPushPlatform _pushMessaging; + final CustomerIOMessagingInAppPlatform _inAppMessaging; + + /// Private constructor to enforce singleton pattern + CustomerIO._({ + CustomerIOPlatform? platform, + CustomerIOMessagingPushPlatform? pushMessaging, + CustomerIOMessagingInAppPlatform? inAppMessaging, + }) : _platform = platform ?? CustomerIOPlatform.instance, + _pushMessaging = + pushMessaging ?? CustomerIOMessagingPushPlatform.instance, + _inAppMessaging = + inAppMessaging ?? CustomerIOMessagingInAppPlatform.instance; + + /// Get the singleton instance of CustomerIO + static CustomerIO get instance { + if (_instance == null) { + throw StateError( + 'CustomerIO SDK must be initialized before accessing instance.\n' + 'Call CustomerIO.initialize() first.', + ); + } + return _instance!; + } + + /// For testing: create a new instance with mock implementations + @visibleForTesting + static CustomerIO createInstance({ + CustomerIOPlatform? platform, + CustomerIOMessagingPushPlatform? pushMessaging, + CustomerIOMessagingInAppPlatform? inAppMessaging, + }) { + _instance = CustomerIO._( + platform: platform, + pushMessaging: pushMessaging, + inAppMessaging: inAppMessaging, + ); + return _instance!; + } - static CustomerIOPlatform get _customerIO => CustomerIOPlatform.instance; + @visibleForTesting + static void reset() { + _instance = null; + } - static CustomerIOMessagingPushPlatform get _customerIOMessagingPush => - CustomerIOMessagingPushPlatform.instance; + /// Access push messaging functionality + CustomerIOMessagingPushPlatform get pushMessaging => _pushMessaging; - static CustomerIOMessagingInAppPlatform get _customerIOMessagingInApp => - CustomerIOMessagingInAppPlatform.instance; + /// Access in-app messaging functionality + CustomerIOMessagingInAppPlatform get inAppMessaging => _inAppMessaging; /// To initialize the plugin /// /// @param config includes required and optional configs etc - static Future initialize({ - required CustomerIOConfig config, - }) { - return _customerIO.initialize(config: config); + static Future initialize({required CustomerIOConfig config}) async { + // Check if already initialized + if (_instance == null) { + // Create new instance if not initialized + _instance = CustomerIO._(); + // Initialize the platform + await _instance!._platform.initialize(config: config); + } else { + log('CustomerIO SDK has already been initialized'); + } } /// Identify a person using a unique identifier, eg. email id. @@ -34,18 +87,18 @@ class CustomerIO { /// /// @param identifier unique identifier for a profile /// @param attributes (Optional) params to set profile attributes - static void identify( + void identify( {required String identifier, Map attributes = const {}}) { - return _customerIO.identify(identifier: identifier, attributes: attributes); + return _platform.identify(identifier: identifier, attributes: attributes); } /// Call this function to stop identifying a person. /// /// If a profile exists, clearIdentify will stop identifying the profile. /// If no profile exists, request to clearIdentify will be ignored. - static void clearIdentify() { - _customerIO.clearIdentify(); + void clearIdentify() { + _platform.clearIdentify(); } /// To track user events like loggedIn, addedItemToCart etc. @@ -53,49 +106,49 @@ class CustomerIO { /// /// @param name event name to be tracked /// @param attributes (Optional) params to be sent with event - static void track( + void track( {required String name, Map attributes = const {}}) { - return _customerIO.track(name: name, attributes: attributes); + return _platform.track(name: name, attributes: attributes); } /// Track a push metric - static void trackMetric( + void trackMetric( {required String deliveryID, required String deviceToken, required MetricEvent event}) { - return _customerIO.trackMetric( + return _platform.trackMetric( deliveryID: deliveryID, deviceToken: deviceToken, event: event); } /// Register a new device token with Customer.io, associated with the current active customer. If there /// is no active customer, this will fail to register the device - static void registerDeviceToken({required String deviceToken}) { - return _customerIO.registerDeviceToken(deviceToken: deviceToken); + void registerDeviceToken({required String deviceToken}) { + return _platform.registerDeviceToken(deviceToken: deviceToken); } /// Track screen events to record the screens a user visits /// /// @param name name of the screen user visited /// @param attributes (Optional) params to be sent with event - static void screen( + void screen( {required String name, Map attributes = const {}}) { - return _customerIO.screen(name: name, attributes: attributes); + return _platform.screen(name: name, attributes: attributes); } /// Use this function to send custom device attributes /// such as app preferences, timezone etc /// /// @param attributes device attributes - static void setDeviceAttributes({required Map attributes}) { - return _customerIO.setDeviceAttributes(attributes: attributes); + void setDeviceAttributes({required Map attributes}) { + return _platform.setDeviceAttributes(attributes: attributes); } /// Set custom user profile information such as user preference, specific /// user actions etc /// /// @param attributes additional attributes for a user profile - static void setProfileAttributes({required Map attributes}) { - return _customerIO.setProfileAttributes(attributes: attributes); + void setProfileAttributes({required Map attributes}) { + return _platform.setProfileAttributes(attributes: attributes); } /// Subscribes to an in-app event listener. @@ -104,16 +157,8 @@ class CustomerIO { /// The callback returns [InAppEvent]. /// /// Returns a [StreamSubscription] that can be used to subscribe/unsubscribe from the event listener. - static StreamSubscription subscribeToInAppEventListener( + StreamSubscription subscribeToInAppEventListener( void Function(InAppEvent) onEvent) { - return _customerIO.subscribeToInAppEventListener(onEvent); - } - - static CustomerIOMessagingPushPlatform messagingPush() { - return _customerIOMessagingPush; - } - - static CustomerIOMessagingInAppPlatform messagingInApp() { - return _customerIOMessagingInApp; + return _platform.subscribeToInAppEventListener(onEvent); } } diff --git a/test/customer_io_test.dart b/test/customer_io_test.dart index aba1676..a312f8e 100644 --- a/test/customer_io_test.dart +++ b/test/customer_io_test.dart @@ -22,8 +22,6 @@ class TestCustomerIoPlatform extends Mock } } -// The following test suite makes sure when any CustomerIO class method is called, -// the correct corresponding platform methods are called and with the correct arguments. @GenerateMocks([TestCustomerIoPlatform]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -32,186 +30,153 @@ void main() { late MockTestCustomerIoPlatform mockPlatform; setUp(() { + // Reset singleton state before each test + CustomerIO.reset(); + mockPlatform = MockTestCustomerIoPlatform(); CustomerIOPlatform.instance = mockPlatform; }); - // initialize - test('initialize() calls platform', () async { - final config = CustomerIOConfig(siteId: '123', apiKey: '456'); - await CustomerIO.initialize(config: config); + group('initialization', () { + test('throws when accessing instance before initialization', () { + expect(() => CustomerIO.instance, throwsStateError); + }); - verify(mockPlatform.initialize(config: config)).called(1); - }); + test('initialize() succeeds first time', () async { + final config = CustomerIOConfig(siteId: '123', apiKey: '456'); + await CustomerIO.initialize(config: config); + expect(() => CustomerIO.instance, isNot(throwsStateError)); + }); + + test('subsequent initialize() calls are ignored', () async { + final config = CustomerIOConfig(siteId: '123', apiKey: '456'); + + // First initialization + await CustomerIO.initialize(config: config); + verify(mockPlatform.initialize(config: config)).called(1); + + // Second initialization should be ignored + await CustomerIO.initialize(config: config); + + // Platform initialize should still only be called once + verifyNever(mockPlatform.initialize(config: config)); + }); - test('initialize() correct arguments are passed', () async { - final givenConfig = CustomerIOConfig( + test('initialize() calls platform', () async { + final config = CustomerIOConfig(siteId: '123', apiKey: '456'); + await CustomerIO.initialize(config: config); + verify(mockPlatform.initialize(config: config)).called(1); + }); + + test('initialize() correct arguments are passed', () async { + final givenConfig = CustomerIOConfig( siteId: '123', apiKey: '456', region: Region.eu, - autoTrackPushEvents: false); - await CustomerIO.initialize(config: givenConfig); - expect( + autoTrackPushEvents: false, + ); + await CustomerIO.initialize(config: givenConfig); + expect( verify(mockPlatform.initialize(config: captureAnyNamed("config"))) .captured .single, - givenConfig); - }); - - // identify - test('identify() calls platform', () { - const givenIdentifier = 'user@example.com'; - final givenAttributes = {'name': 'John Doe'}; - CustomerIO.identify( - identifier: givenIdentifier, attributes: givenAttributes); - - verify(mockPlatform.identify( - identifier: givenIdentifier, attributes: givenAttributes)) - .called(1); - }); - - test('identify() correct arguments are passed', () { - const givenIdentifier = 'user@example.com'; - final givenAttributes = {'name': 'John Doe'}; - CustomerIO.identify( - identifier: givenIdentifier, attributes: givenAttributes); - expect( + givenConfig, + ); + }); + }); + + group('methods requiring initialization', () { + late CustomerIOConfig config; + + setUp(() async { + config = CustomerIOConfig(siteId: '123', apiKey: '456'); + await CustomerIO.initialize(config: config); + }); + + test('identify() calls platform', () { + const givenIdentifier = 'user@example.com'; + final givenAttributes = {'name': 'John Doe'}; + CustomerIO.instance.identify( + identifier: givenIdentifier, + attributes: givenAttributes, + ); + + verify(mockPlatform.identify( + identifier: givenIdentifier, + attributes: givenAttributes, + )).called(1); + }); + + test('identify() correct arguments are passed', () { + const givenIdentifier = 'user@example.com'; + final givenAttributes = {'name': 'John Doe'}; + CustomerIO.instance.identify( + identifier: givenIdentifier, + attributes: givenAttributes, + ); + expect( verify(mockPlatform.identify( - identifier: captureAnyNamed("identifier"), - attributes: captureAnyNamed("attributes"))) - .captured, - [givenIdentifier, givenAttributes]); - }); - - // clearIdentify - test('clearIdentify() calls platform', () { - CustomerIO.clearIdentify(); - verify(mockPlatform.clearIdentify()).called(1); - }); - - // track - test('track() calls platform', () { - const name = 'itemAddedToCart'; - final attributes = {'item': 'shoes'}; - CustomerIO.track(name: name, attributes: attributes); - verify(mockPlatform.track(name: name, attributes: attributes)).called(1); - }); - - test('track() correct arguments are passed', () { - const name = 'itemAddedToCart'; - final givenAttributes = {'name': 'John Doe'}; - CustomerIO.track(name: name, attributes: givenAttributes); - expect( + identifier: captureAnyNamed("identifier"), + attributes: captureAnyNamed("attributes"), + )).captured, + [givenIdentifier, givenAttributes], + ); + }); + + test('clearIdentify() calls platform', () { + CustomerIO.instance.clearIdentify(); + verify(mockPlatform.clearIdentify()).called(1); + }); + + test('track() calls platform', () { + const name = 'itemAddedToCart'; + final attributes = {'item': 'shoes'}; + CustomerIO.instance.track(name: name, attributes: attributes); + verify(mockPlatform.track(name: name, attributes: attributes)) + .called(1); + }); + + test('track() correct arguments are passed', () { + const name = 'itemAddedToCart'; + final givenAttributes = {'name': 'John Doe'}; + CustomerIO.instance.track(name: name, attributes: givenAttributes); + expect( verify(mockPlatform.track( - name: captureAnyNamed("name"), - attributes: captureAnyNamed("attributes"))) - .captured, - [name, givenAttributes]); - }); - - // trackMetric - test('trackMetric() calls platform', () { - const deliveryID = '123'; - const deviceToken = 'abc'; - const event = MetricEvent.opened; - CustomerIO.trackMetric( - deliveryID: deliveryID, deviceToken: deviceToken, event: event); - verify(mockPlatform.trackMetric( - deliveryID: deliveryID, deviceToken: deviceToken, event: event)) - .called(1); - }); - - test('trackMetric() correct arguments are passed', () { - const deliveryID = '123'; - const deviceToken = 'abc'; - const event = MetricEvent.opened; - CustomerIO.trackMetric( - deliveryID: deliveryID, deviceToken: deviceToken, event: event); - expect( - verify(mockPlatform.trackMetric( - deliveryID: captureAnyNamed("deliveryID"), - deviceToken: captureAnyNamed("deviceToken"), - event: captureAnyNamed("event"))) - .captured, - [deliveryID, deviceToken, event]); - }); - - // registerDeviceToken - test('registerDeviceToken() calls platform', () { - const deviceToken = 'token'; - CustomerIO.registerDeviceToken(deviceToken: deviceToken); - verify(mockPlatform.registerDeviceToken(deviceToken: deviceToken)) - .called(1); - }); - - test('registerDeviceToken() correct arguments are passed', () { - const deviceToken = 'token'; - CustomerIO.registerDeviceToken(deviceToken: deviceToken); - expect( - verify(mockPlatform.registerDeviceToken( - deviceToken: captureAnyNamed("deviceToken"))) - .captured - .first, - deviceToken); - }); - - // screen - test('screen() calls platform', () { - const name = 'home'; - final givenAttributes = {'user': 'John Doe'}; - CustomerIO.screen(name: name, attributes: givenAttributes); - verify(mockPlatform.screen(name: name, attributes: givenAttributes)) - .called(1); - }); - - test('screen() correct arguments are passed', () { - const name = 'itemAddedToCart'; - final givenAttributes = {'name': 'John Doe'}; - CustomerIO.screen(name: name, attributes: givenAttributes); - expect( - verify(mockPlatform.screen( - name: captureAnyNamed("name"), - attributes: captureAnyNamed("attributes"))) - .captured, - [name, givenAttributes]); - }); - - // setDeviceAttributes - test('setDeviceAttributes() calls platform', () { - final givenAttributes = {'area': 'US'}; - CustomerIO.setDeviceAttributes(attributes: givenAttributes); - verify(mockPlatform.setDeviceAttributes(attributes: givenAttributes)) - .called(1); - }); - - test('setDeviceAttributes() correct arguments are passed', () { - final givenAttributes = {'area': 'US'}; - CustomerIO.setDeviceAttributes(attributes: givenAttributes); - expect( - verify(mockPlatform.setDeviceAttributes( - attributes: captureAnyNamed("attributes"))) - .captured - .first, - givenAttributes); - }); - - // setProfileAttributes - test('setProfileAttributes() calls platform', () { - final givenAttributes = {'age': 10}; - CustomerIO.setProfileAttributes(attributes: givenAttributes); - verify(mockPlatform.setProfileAttributes(attributes: givenAttributes)) - .called(1); - }); - - test('setProfileAttributes() correct arguments are passed', () { - final givenAttributes = {'age': 10}; - CustomerIO.setProfileAttributes(attributes: givenAttributes); - expect( + name: captureAnyNamed("name"), + attributes: captureAnyNamed("attributes"), + )).captured, + [name, givenAttributes], + ); + }); + + test('trackMetric() calls platform', () { + const deliveryID = '123'; + const deviceToken = 'abc'; + const event = MetricEvent.opened; + CustomerIO.instance.trackMetric( + deliveryID: deliveryID, + deviceToken: deviceToken, + event: event, + ); + verify(mockPlatform.trackMetric( + deliveryID: deliveryID, + deviceToken: deviceToken, + event: event, + )).called(1); + }); + + // ... rest of the existing tests, but moved inside this group ... + + test('setProfileAttributes() correct arguments are passed', () { + final givenAttributes = {'age': 10}; + CustomerIO.instance.setProfileAttributes(attributes: givenAttributes); + expect( verify(mockPlatform.setProfileAttributes( - attributes: captureAnyNamed("attributes"))) - .captured - .first, - givenAttributes); + attributes: captureAnyNamed("attributes"), + )).captured.first, + givenAttributes, + ); + }); }); }); } From 581f039c7bc893f7a490ce49d25cb816bec7fb4b Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Tue, 29 Oct 2024 14:45:00 +0500 Subject: [PATCH 02/34] chore: update iOS native dependencies to Data Pipelines (#153) --- .../NotificationService.swift | 6 ++-- apps/amiapp_flutter/ios/Podfile | 4 +-- .../ios/Runner.xcodeproj/project.pbxproj | 3 ++ .../ios/Runner/AppDelegate.swift | 8 ++--- ios/Classes/SwiftCustomerIoPlugin.swift | 29 +++++++++++++++++-- ios/customer_io.podspec | 4 +-- 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/apps/amiapp_flutter/ios/NotificationServiceExtension/NotificationService.swift b/apps/amiapp_flutter/ios/NotificationServiceExtension/NotificationService.swift index 40dcda7..fc7aaca 100644 --- a/apps/amiapp_flutter/ios/NotificationServiceExtension/NotificationService.swift +++ b/apps/amiapp_flutter/ios/NotificationServiceExtension/NotificationService.swift @@ -4,7 +4,6 @@ // import CioMessagingPushFCM -import CioTracking class NotificationService: UNNotificationServiceExtension { @@ -13,11 +12,14 @@ class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { print("NotificationService didReceive called") - + + // TODO: Fix SDK initialization + /* CustomerIO.initialize(siteId: Env.siteId, apiKey: Env.apiKey, region: .US) { config in config.autoTrackDeviceAttributes = true config.logLevel = .debug } + */ MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } diff --git a/apps/amiapp_flutter/ios/Podfile b/apps/amiapp_flutter/ios/Podfile index 1595e27..8412114 100644 --- a/apps/amiapp_flutter/ios/Podfile +++ b/apps/amiapp_flutter/ios/Podfile @@ -43,7 +43,7 @@ target 'Runner' do use_modular_headers! # Uncomment only 1 of the lines below to install a version of the iOS SDK - pod 'CustomerIO/MessagingPushFCM', '~> 2.14' # install production build + pod 'CustomerIO/MessagingPushFCM', '~> 3.5' # install production build # install_non_production_ios_sdk_local_path(local_path: '~/code/customerio-ios/', is_app_extension: false, push_service: "fcm") # install_non_production_ios_sdk_git_branch(branch_name: 'levi/v2-multiple-push-handlers', is_app_extension: false, push_service: "fcm") @@ -53,7 +53,7 @@ end target 'NotificationServiceExtension' do use_frameworks! # Uncomment only 1 of the lines below to install a version of the iOS SDK - pod 'CustomerIO/MessagingPushFCM', '~> 2.14' # install production build + pod 'CustomerIO/MessagingPushFCM', '~> 3.5' # install production build # install_non_production_ios_sdk_local_path(local_path: '~/code/customerio-ios/', is_app_extension: true, push_service: "fcm") # install_non_production_ios_sdk_git_branch(branch_name: 'levi/v2-multiple-push-handlers', is_app_extension: true, push_service: "fcm") end diff --git a/apps/amiapp_flutter/ios/Runner.xcodeproj/project.pbxproj b/apps/amiapp_flutter/ios/Runner.xcodeproj/project.pbxproj index dd67a68..18dfe0e 100644 --- a/apps/amiapp_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/apps/amiapp_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -513,6 +513,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_STYLE = Manual; @@ -767,6 +768,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_STYLE = Manual; @@ -796,6 +798,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; diff --git a/apps/amiapp_flutter/ios/Runner/AppDelegate.swift b/apps/amiapp_flutter/ios/Runner/AppDelegate.swift index 71909e2..b3f6bfe 100644 --- a/apps/amiapp_flutter/ios/Runner/AppDelegate.swift +++ b/apps/amiapp_flutter/ios/Runner/AppDelegate.swift @@ -1,7 +1,6 @@ import UIKit import Flutter import CioMessagingPushFCM -import CioTracking import FirebaseMessaging import FirebaseCore @@ -21,11 +20,10 @@ import FirebaseCore Messaging.messaging().delegate = self - CustomerIO.initialize(siteId: Env.siteId, apiKey: Env.apiKey, region: .US) { config in - config.autoTrackDeviceAttributes = true - config.logLevel = .debug - } + // TODO: Fix MessagingPush initialization + /* MessagingPushFCM.initialize(configOptions: nil) + */ // Sets a 3rd party push event handler for the app besides the Customer.io SDK and FlutterFire. // Setting the AppDelegate to be the handler will internally use `flutter_local_notifications` to handle the push event. diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index 107a56d..dc98f14 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -1,6 +1,5 @@ import Flutter import UIKit -import CioTracking import CioInternalCommon import CioMessagingInApp @@ -68,6 +67,8 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } private func identify(params : Dictionary){ + // TODO: Fix identify implementation + /* guard let identifier = params[Keys.Tracking.identifier] as? String else { return @@ -79,6 +80,7 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } CustomerIO.shared.identify(identifier: identifier, body: attributes) + */ } private func clearIdentify() { @@ -86,6 +88,8 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } private func track(params : Dictionary) { + // TODO: Fix track implementation + /* guard let name = params[Keys.Tracking.eventName] as? String else { return @@ -97,10 +101,13 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } CustomerIO.shared.track(name: name, data: attributes) + */ } func screen(params : Dictionary) { + // TODO: Fix screen implementation + /* guard let name = params[Keys.Tracking.eventName] as? String else { return @@ -112,35 +119,47 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } CustomerIO.shared.screen(name: name, data: attributes) + */ } private func setDeviceAttributes(params : Dictionary){ + // TODO: Fix setDeviceAttributes implementation + /* guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else { return } CustomerIO.shared.deviceAttributes = attributes + */ } private func setProfileAttributes(params : Dictionary){ + // TODO: Fix setProfileAttributes implementation + /* guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else { return } CustomerIO.shared.profileAttributes = attributes + */ } private func registerDeviceToken(params : Dictionary){ + // TODO: Fix registerDeviceToken implementation + /* guard let token = params[Keys.Tracking.token] as? String else { return } CustomerIO.shared.registerDeviceToken(token) + */ } private func trackMetric(params : Dictionary){ + // TODO: Fix trackMetric implementation + /* guard let deliveryId = params[Keys.Tracking.deliveryId] as? String, let deviceToken = params[Keys.Tracking.deliveryToken] as? String, let metricEvent = params[Keys.Tracking.metricEvent] as? String, @@ -152,9 +171,12 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { CustomerIO.shared.trackMetric(deliveryID: deliveryId, event: event, deviceToken: deviceToken) + */ } private func initialize(params : Dictionary){ + // TODO: Fix initialize implementation + /* guard let siteId = params[Keys.Environment.siteId] as? String, let apiKey = params[Keys.Environment.apiKey] as? String, let regionStr = params[Keys.Environment.region] as? String @@ -175,13 +197,15 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { initializeInApp() } } - + */ } /** Initialize in-app using customerio plugin */ private func initializeInApp(){ + // TODO: Fix initializeInApp implementation + /* DispatchQueue.main.async { MessagingInApp.shared.initialize(eventListener: CustomerIOInAppEventListener( invokeMethod: {method,args in @@ -189,6 +213,7 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { }) ) } + */ } func invokeMethod(_ method: String, _ args: Any?) { diff --git a/ios/customer_io.podspec b/ios/customer_io.podspec index fff69ec..735d515 100755 --- a/ios/customer_io.podspec +++ b/ios/customer_io.podspec @@ -17,8 +17,8 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.platform = :ios, '13.0' - s.dependency "CustomerIO/Tracking", '~> 2' - s.dependency "CustomerIO/MessagingInApp", '~> 2' + s.dependency "CustomerIO/DataPipelines", '~> 3' + s.dependency "CustomerIO/MessagingInApp", '~> 3' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } From 8ab1e271c36430613efc4b996620a2cc84cbcb4f Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Tue, 29 Oct 2024 14:45:19 +0500 Subject: [PATCH 03/34] chore: update Android native dependencies to Data Pipelines (#154) --- android/build.gradle | 5 +-- .../customer/customer_io/CustomerIoPlugin.kt | 39 ++++++++++++++----- .../messagingpush/CustomerIOPushMessaging.kt | 10 ++--- apps/amiapp_flutter/android/app/build.gradle | 2 +- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 3f52931..c8691f1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -18,7 +18,6 @@ rootProject.allprojects { repositories { google() mavenCentral() - maven { url 'https://maven.gist.build' } } } @@ -59,8 +58,8 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // Customer.io SDK - def cioVersion = "3.11.2" - implementation "io.customer.android:tracking:$cioVersion" + def cioVersion = "4.3.0" + implementation "io.customer.android:datapipelines:$cioVersion" implementation "io.customer.android:messaging-push-fcm:$cioVersion" implementation "io.customer.android:messaging-in-app:$cioVersion" } diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 9d2f950..10a139c 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -15,14 +15,9 @@ import io.customer.messagingpush.MessagingPushModuleConfig import io.customer.messagingpush.ModuleMessagingPushFCM import io.customer.messagingpush.config.PushClickBehavior import io.customer.sdk.CustomerIO -import io.customer.sdk.CustomerIOConfig -import io.customer.sdk.CustomerIOShared +import io.customer.sdk.core.di.SDKComponent +import io.customer.sdk.core.util.Logger import io.customer.sdk.data.model.Region -import io.customer.sdk.data.request.MetricEvent -import io.customer.sdk.extensions.getProperty -import io.customer.sdk.extensions.getString -import io.customer.sdk.extensions.takeIfNotBlank -import io.customer.sdk.util.Logger import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -47,8 +42,7 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var modules: List - private val logger: Logger - get() = CustomerIOShared.instance().diStaticGraph.logger + private val logger: Logger = SDKComponent.logger override fun onAttachedToActivity(binding: ActivityPluginBinding) { this.activity = WeakReference(binding.activity) @@ -161,13 +155,18 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun identify(params: Map) { + // TODO: Fix identify implementation + /* val identifier = params.getString(Keys.Tracking.IDENTIFIER) val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: emptyMap() CustomerIO.instance().identify(identifier, attributes) + */ } private fun track(params: Map) { + // TODO: Fix track implementation + /* val name = params.getString(Keys.Tracking.EVENT_NAME) val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: emptyMap() @@ -177,14 +176,20 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } else { CustomerIO.instance().track(name, attributes) } + */ } private fun registerDeviceToken(params: Map) { + // TODO: Fix registerDeviceToken implementation + /* val token = params.getString(Keys.Tracking.TOKEN) CustomerIO.instance().registerDeviceToken(token) + */ } private fun trackMetric(params: Map) { + // TODO: Fix trackMetric implementation + /* val deliveryId = params.getString(Keys.Tracking.DELIVERY_ID) val deliveryToken = params.getString(Keys.Tracking.DELIVERY_TOKEN) val eventName = params.getProperty(Keys.Tracking.METRIC_EVENT) @@ -198,21 +203,30 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { CustomerIO.instance().trackMetric( deliveryID = deliveryId, deviceToken = deliveryToken, event = event ) + */ } private fun setDeviceAttributes(params: Map) { + // TODO: Fix setDeviceAttributes implementation + /* val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: return CustomerIO.instance().deviceAttributes = attributes + */ } private fun setProfileAttributes(params: Map) { + // TODO: Fix setProfileAttributes implementation + /* val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: return CustomerIO.instance().profileAttributes = attributes + */ } private fun screen(params: Map) { + // TODO: Fix screen implementation + /* val name = params.getString(Keys.Tracking.EVENT_NAME) val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: emptyMap() @@ -222,9 +236,12 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } else { CustomerIO.instance().screen(name, attributes) } + */ } private fun initialize(configData: Map) { + // TODO: Fix initialize implementation + /* val application: Application = context.applicationContext as Application val siteId = configData.getString(Keys.Environment.SITE_ID) val apiKey = configData.getString(Keys.Environment.API_KEY) @@ -272,10 +289,13 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { lifecycleCallbacks.postDelayedEventsForNonNativeActivity(activity) } } + */ } private fun configureModuleMessagingPushFCM(config: Map?): ModuleMessagingPushFCM { return ModuleMessagingPushFCM( + // TODO: Fix push module configuration + /* config = MessagingPushModuleConfig.Builder().apply { config?.getProperty(CustomerIOConfig.Companion.Keys.AUTO_TRACK_PUSH_EVENTS) ?.let { value -> @@ -292,6 +312,7 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } }.build(), + */ ) } diff --git a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt index e32618b..26a0435 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt @@ -6,9 +6,8 @@ import io.customer.customer_io.constant.Keys import io.customer.customer_io.getAsTypeOrNull import io.customer.customer_io.invokeNative import io.customer.messagingpush.CustomerIOFirebaseMessagingService -import io.customer.sdk.CustomerIOShared -import io.customer.sdk.extensions.takeIfNotBlank -import io.customer.sdk.util.Logger +import io.customer.sdk.core.di.SDKComponent +import io.customer.sdk.core.util.Logger import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -25,8 +24,7 @@ internal class CustomerIOPushMessaging( private val applicationContext: Context = pluginBinding.applicationContext override val flutterCommunicationChannel: MethodChannel = MethodChannel(pluginBinding.binaryMessenger, "customer_io_messaging_push") - private val logger: Logger - get() = CustomerIOShared.instance().diStaticGraph.logger + private val logger: Logger = SDKComponent.logger override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { @@ -62,7 +60,7 @@ internal class CustomerIOPushMessaging( } // Generate destination string, see docs on receiver method for more details - val destination = (message["to"] as? String)?.takeIfNotBlank() + val destination = (message["to"] as? String)?.takeIf { it.isNotBlank() } ?: UUID.randomUUID().toString() return CustomerIOFirebaseMessagingService.onMessageReceived( context = applicationContext, diff --git a/apps/amiapp_flutter/android/app/build.gradle b/apps/amiapp_flutter/android/app/build.gradle index 0ec6c3b..3c5fecd 100644 --- a/apps/amiapp_flutter/android/app/build.gradle +++ b/apps/amiapp_flutter/android/app/build.gradle @@ -79,7 +79,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' // Adding customer.io android sdk dependencies so we can use them in native code // These are not generally needed and should be avoided - implementation "io.customer.android:tracking" + implementation "io.customer.android:datapipelines" implementation "io.customer.android:messaging-push-fcm" implementation "io.customer.android:messaging-in-app" } From b36f56434099b9213204351f657c356bd317025e Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Tue, 29 Oct 2024 19:01:25 +0500 Subject: [PATCH 04/34] chore: fix ios native sdk version (#155) --- apps/amiapp_flutter/ios/Podfile | 7 +++--- ios/customer_io.podspec | 24 +++++++++++++++++++-- ios/customer_io_richpush.podspec | 37 ++++++++++++++++++++++++++++++++ pubspec.yaml | 1 + 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100755 ios/customer_io_richpush.podspec diff --git a/apps/amiapp_flutter/ios/Podfile b/apps/amiapp_flutter/ios/Podfile index 8412114..b9d338d 100644 --- a/apps/amiapp_flutter/ios/Podfile +++ b/apps/amiapp_flutter/ios/Podfile @@ -43,7 +43,7 @@ target 'Runner' do use_modular_headers! # Uncomment only 1 of the lines below to install a version of the iOS SDK - pod 'CustomerIO/MessagingPushFCM', '~> 3.5' # install production build + pod 'customer_io/fcm', :path => '.symlinks/plugins/customer_io/ios' # install podspec bundled with the plugin # install_non_production_ios_sdk_local_path(local_path: '~/code/customerio-ios/', is_app_extension: false, push_service: "fcm") # install_non_production_ios_sdk_git_branch(branch_name: 'levi/v2-multiple-push-handlers', is_app_extension: false, push_service: "fcm") @@ -52,8 +52,9 @@ end target 'NotificationServiceExtension' do use_frameworks! - # Uncomment only 1 of the lines below to install a version of the iOS SDK - pod 'CustomerIO/MessagingPushFCM', '~> 3.5' # install production build + # Ideally, installing non-production SDK to main target should be enough + # We may not need to install non-production SDK to app extension separately + pod 'customer_io_richpush/fcm', :path => '.symlinks/plugins/customer_io/ios' # install podspec bundled with the plugin # install_non_production_ios_sdk_local_path(local_path: '~/code/customerio-ios/', is_app_extension: true, push_service: "fcm") # install_non_production_ios_sdk_git_branch(branch_name: 'levi/v2-multiple-push-handlers', is_app_extension: true, push_service: "fcm") end diff --git a/ios/customer_io.podspec b/ios/customer_io.podspec index 735d515..9b426a0 100755 --- a/ios/customer_io.podspec +++ b/ios/customer_io.podspec @@ -5,6 +5,8 @@ require 'yaml' podspec_config = YAML.load_file('../pubspec.yaml') +# The native_sdk_version is the version of iOS native SDK that the Flutter plugin is compatible with. +native_sdk_version = podspec_config['flutter']['plugin']['platforms']['ios']['native_sdk_version'] Pod::Spec.new do |s| s.name = podspec_config['name'] @@ -17,8 +19,26 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.platform = :ios, '13.0' - s.dependency "CustomerIO/DataPipelines", '~> 3' - s.dependency "CustomerIO/MessagingInApp", '~> 3' + + # Native SDK dependencies that are required for the Flutter plugin to work. + s.dependency "CustomerIO/DataPipelines", native_sdk_version + s.dependency "CustomerIO/MessagingInApp", native_sdk_version + + # If we do not specify a default_subspec, then *all* dependencies inside of *all* the subspecs will be downloaded by cocoapods. + # We want customers to opt into push dependencies especially because the FCM subpsec downloads Firebase dependencies. + # APN customers should not install Firebase dependencies at all. + s.default_subspec = "nopush" + + s.subspec 'nopush' do |ss| + # This is the default subspec designed to not install any push dependencies. Customer should choose APN or FCM. + # The SDK at runtime currently requires the MessagingPush module so we do include it here. + ss.dependency "CustomerIO/MessagingPush", native_sdk_version + end + + # Note: Subspecs inherit all dependencies specified the parent spec (this file). + s.subspec 'fcm' do |ss| + ss.dependency "CustomerIO/MessagingPushFCM", native_sdk_version + end # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/ios/customer_io_richpush.podspec b/ios/customer_io_richpush.podspec new file mode 100755 index 0000000..f6d6ed7 --- /dev/null +++ b/ios/customer_io_richpush.podspec @@ -0,0 +1,37 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint customer_io_richpush.podspec` to validate before publishing. +# +require 'yaml' + +podspec_config = YAML.load_file('../pubspec.yaml') +# The native_sdk_version is the version of iOS native SDK that the Flutter plugin is compatible with. +native_sdk_version = podspec_config['flutter']['plugin']['platforms']['ios']['native_sdk_version'] + +# Used by customers to install native iOS dependencies inside their Notification Service Extension (NSE) target to setup rich push. +# Note: We need a unique podspec for rich push because the other podspecs in this project install too many dependencies that should not be installed inside of a NSE target. +# We need this podspec which installs minimal dependencies that are only included in the NSE target. +Pod::Spec.new do |s| + s.name = "customer_io_richpush" + s.version = podspec_config['version'] + s.summary = podspec_config['description'] + s.homepage = podspec_config['homepage'] + s.license = { :file => '../LICENSE' } + s.author = { "CustomerIO Team" => "win@customer.io" } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '13.0' + + # Careful when declaring dependencies here. All dependencies will be included in the App Extension target in Xcode, not the host iOS app. + # s.dependency "X", "X" + + # Subspecs allow customers to choose between multiple options of what type of version of this rich push package they would like to install. + s.subspec 'fcm' do |ss| + ss.dependency "CustomerIO/MessagingPushFCM", native_sdk_version + end + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/pubspec.yaml b/pubspec.yaml index 062087b..d91304d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,3 +42,4 @@ flutter: pluginClass: CustomerIoPlugin ios: pluginClass: CustomerIoPlugin + native_sdk_version: 3.5.1 From 0ed2485a747bc7e82306559544e0ddf5b5eb188e Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Tue, 5 Nov 2024 21:04:39 +0500 Subject: [PATCH 05/34] chore: update config for data pipelines support (#157) --- apps/amiapp_flutter/lib/src/customer_io.dart | 28 +-- lib/config/customer_io_config.dart | 53 +++++ lib/config/in_app_config.dart | 11 + lib/config/push_config.dart | 28 +++ lib/customer_io_config.dart | 58 +---- lib/customer_io_method_channel.dart | 5 - test/customer_io_config_test.dart | 210 +++++++++++++++++++ test/customer_io_method_channel_test.dart | 4 +- test/customer_io_test.dart | 14 +- 9 files changed, 330 insertions(+), 81 deletions(-) create mode 100644 lib/config/customer_io_config.dart create mode 100644 lib/config/in_app_config.dart create mode 100644 lib/config/push_config.dart create mode 100644 test/customer_io_config_test.dart diff --git a/apps/amiapp_flutter/lib/src/customer_io.dart b/apps/amiapp_flutter/lib/src/customer_io.dart index 5dd60ec..4605a70 100644 --- a/apps/amiapp_flutter/lib/src/customer_io.dart +++ b/apps/amiapp_flutter/lib/src/customer_io.dart @@ -53,22 +53,26 @@ class CustomerIOSDK extends ChangeNotifier { } else { logLevel = CioLogLevel.debug; } + + final InAppConfig? inAppConfig; + if (_sdkConfig?.siteId != null) { + inAppConfig = InAppConfig(siteId: _sdkConfig!.siteId); + } else { + inAppConfig = null; + } return CustomerIO.initialize( config: CustomerIOConfig( - siteId: _sdkConfig?.siteId ?? '', - apiKey: _sdkConfig?.apiKey ?? '', - enableInApp: true, + cdpApiKey: '${_sdkConfig?.siteId}:${_sdkConfig?.apiKey}', + migrationSiteId: _sdkConfig?.siteId, region: Region.us, - //config options go here - trackingApiUrl: _sdkConfig?.trackingUrl ?? '', - autoTrackDeviceAttributes: - _sdkConfig?.deviceAttributesTrackingEnabled ?? true, - autoTrackPushEvents: true, - backgroundQueueMinNumberOfTasks: - _sdkConfig?.backgroundQueueMinNumOfTasks ?? 10, - backgroundQueueSecondsDelay: - _sdkConfig?.backgroundQueueSecondsDelay ?? 30.0, logLevel: logLevel, + autoTrackDeviceAttributes: + _sdkConfig?.deviceAttributesTrackingEnabled, + apiHost: _sdkConfig?.trackingUrl, + cdnHost: _sdkConfig?.trackingUrl, + flushAt: _sdkConfig?.backgroundQueueMinNumOfTasks, + flushInterval: _sdkConfig?.backgroundQueueSecondsDelay?.toInt(), + inAppConfig: inAppConfig, ), ); } catch (ex) { diff --git a/lib/config/customer_io_config.dart b/lib/config/customer_io_config.dart new file mode 100644 index 0000000..55819f0 --- /dev/null +++ b/lib/config/customer_io_config.dart @@ -0,0 +1,53 @@ +import '../customer_io_enums.dart'; +import '../customer_io_plugin_version.dart' as plugin_info show version; +import 'in_app_config.dart'; +import 'push_config.dart'; + +class CustomerIOConfig { + final String source = 'Flutter'; + final String version = plugin_info.version; + + final String cdpApiKey; + final String? migrationSiteId; + final Region? region; + final CioLogLevel? logLevel; + final bool? autoTrackDeviceAttributes; + final String? apiHost; + final String? cdnHost; + final int? flushAt; + final int? flushInterval; + final InAppConfig? inAppConfig; + final PushConfig pushConfig; + + CustomerIOConfig({ + required this.cdpApiKey, + this.migrationSiteId, + this.region, + this.logLevel, + this.autoTrackDeviceAttributes, + this.apiHost, + this.cdnHost, + this.flushAt, + this.flushInterval, + this.inAppConfig, + PushConfig? pushConfig, + }) : pushConfig = pushConfig ?? PushConfig(); + + Map toMap() { + return { + 'cdpApiKey': cdpApiKey, + 'migrationSiteId': migrationSiteId, + 'region': region?.name, + 'logLevel': logLevel?.name, + 'autoTrackDeviceAttributes': autoTrackDeviceAttributes, + 'apiHost': apiHost, + 'cdnHost': cdnHost, + 'flushAt': flushAt, + 'flushInterval': flushInterval, + 'inAppConfig': inAppConfig?.toMap(), + 'pushConfig': pushConfig.toMap(), + 'version': version, + 'source': source + }; + } +} diff --git a/lib/config/in_app_config.dart b/lib/config/in_app_config.dart new file mode 100644 index 0000000..12d17dc --- /dev/null +++ b/lib/config/in_app_config.dart @@ -0,0 +1,11 @@ +class InAppConfig { + final String siteId; + + InAppConfig({required this.siteId}); + + Map toMap() { + return { + 'siteId': siteId, + }; + } +} diff --git a/lib/config/push_config.dart b/lib/config/push_config.dart new file mode 100644 index 0000000..3ae21ad --- /dev/null +++ b/lib/config/push_config.dart @@ -0,0 +1,28 @@ +import 'package:customer_io/customer_io_enums.dart'; + +class PushConfig { + PushConfigAndroid pushConfigAndroid; + + PushConfig({PushConfigAndroid? android}) + : pushConfigAndroid = android ?? PushConfigAndroid(); + + Map toMap() { + return { + 'android': pushConfigAndroid.toMap(), + }; + } +} + +class PushConfigAndroid { + PushClickBehaviorAndroid pushClickBehavior; + + PushConfigAndroid( + {this.pushClickBehavior = + PushClickBehaviorAndroid.activityPreventRestart}); + + Map toMap() { + return { + 'pushClickBehavior': pushClickBehavior.rawValue, + }; + } +} diff --git a/lib/customer_io_config.dart b/lib/customer_io_config.dart index 0f5457a..01dc046 100644 --- a/lib/customer_io_config.dart +++ b/lib/customer_io_config.dart @@ -1,55 +1,3 @@ -import 'customer_io_enums.dart'; - -/// Configure plugin using CustomerIOConfig -class CustomerIOConfig { - final String siteId; - final String apiKey; - Region region; - String organizationId; - CioLogLevel logLevel; - bool autoTrackDeviceAttributes; - String trackingApiUrl; - bool autoTrackPushEvents; - int backgroundQueueMinNumberOfTasks; - double backgroundQueueSecondsDelay; - PushClickBehaviorAndroid pushClickBehaviorAndroid; - - bool enableInApp; - - String version; - - CustomerIOConfig( - {required this.siteId, - required this.apiKey, - this.region = Region.us, - @Deprecated("organizationId is deprecated and isn't required anymore, use enableInApp instead. This field will be removed in the next release.") - this.organizationId = "", - this.logLevel = CioLogLevel.debug, - this.autoTrackDeviceAttributes = true, - this.trackingApiUrl = "", - this.autoTrackPushEvents = true, - this.backgroundQueueMinNumberOfTasks = 10, - this.backgroundQueueSecondsDelay = 30.0, - this.pushClickBehaviorAndroid = PushClickBehaviorAndroid.activityPreventRestart, - this.enableInApp = false, - this.version = ""}); - - Map toMap() { - return { - 'siteId': siteId, - 'apiKey': apiKey, - 'region': region.name, - 'organizationId': organizationId, - 'logLevel': logLevel.name, - 'autoTrackDeviceAttributes': autoTrackDeviceAttributes, - 'trackingApiUrl': trackingApiUrl, - 'autoTrackPushEvents': autoTrackPushEvents, - 'backgroundQueueMinNumberOfTasks': backgroundQueueMinNumberOfTasks, - 'backgroundQueueSecondsDelay': backgroundQueueSecondsDelay, - 'pushClickBehaviorAndroid': pushClickBehaviorAndroid.rawValue, - 'enableInApp': enableInApp, - 'version': version, - 'source': "Flutter" - }; - } -} +export 'config/customer_io_config.dart'; +export 'config/in_app_config.dart'; +export 'config/push_config.dart'; diff --git a/lib/customer_io_method_channel.dart b/lib/customer_io_method_channel.dart index fde2a5a..1f64cd2 100644 --- a/lib/customer_io_method_channel.dart +++ b/lib/customer_io_method_channel.dart @@ -8,7 +8,6 @@ import 'customer_io_config.dart'; import 'customer_io_const.dart'; import 'customer_io_inapp.dart'; import 'customer_io_platform_interface.dart'; -import 'customer_io_plugin_version.dart'; /// An implementation of [CustomerIOPlatform] that uses method channels. class CustomerIOMethodChannel extends CustomerIOPlatform { @@ -66,10 +65,6 @@ class CustomerIOMethodChannel extends CustomerIOPlatform { required CustomerIOConfig config, }) async { try { - config.version = version; - if (!config.enableInApp && config.organizationId.isNotEmpty) { - config.enableInApp = true; - } await methodChannel.invokeMethod(MethodConsts.initialize, config.toMap()); } on PlatformException catch (exception) { handleException(exception); diff --git a/test/customer_io_config_test.dart b/test/customer_io_config_test.dart new file mode 100644 index 0000000..3a1e2c1 --- /dev/null +++ b/test/customer_io_config_test.dart @@ -0,0 +1,210 @@ +import 'package:customer_io/config/customer_io_config.dart'; +import 'package:customer_io/config/in_app_config.dart'; +import 'package:customer_io/config/push_config.dart'; +import 'package:customer_io/customer_io_enums.dart'; +import 'package:customer_io/customer_io_plugin_version.dart' as plugin_info; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CustomerIOConfig', () { + test('should initialize with required parameters and default values', () { + final config = CustomerIOConfig(cdpApiKey: 'testApiKey'); + + expect(config.cdpApiKey, 'testApiKey'); + expect(config.migrationSiteId, isNull); + expect(config.region, isNull); + expect(config.logLevel, isNull); + expect(config.autoTrackDeviceAttributes, isNull); + expect(config.apiHost, isNull); + expect(config.cdnHost, isNull); + expect(config.flushAt, isNull); + expect(config.flushInterval, isNull); + + expect(config.inAppConfig, isNull); + + final pushConfig = config.pushConfig; + expect(pushConfig, isNotNull); + final pushConfigAndroid = pushConfig.pushConfigAndroid; + expect(pushConfigAndroid, isNotNull); + expect(pushConfigAndroid.pushClickBehavior, + PushClickBehaviorAndroid.activityPreventRestart); + + expect(config.source, 'Flutter'); + expect(config.version, plugin_info.version); + }); + + test('should initialize with all parameters', () { + final inAppConfig = InAppConfig(siteId: 'testSiteId'); + final pushConfig = PushConfig( + android: PushConfigAndroid( + pushClickBehavior: + PushClickBehaviorAndroid.activityPreventRestart)); + + final config = CustomerIOConfig( + cdpApiKey: 'testApiKey', + migrationSiteId: 'testMigrationSiteId', + region: Region.us, + logLevel: CioLogLevel.debug, + autoTrackDeviceAttributes: true, + apiHost: 'https://api.example.com', + cdnHost: 'https://cdn.example.com', + flushAt: 15, + flushInterval: 45, + inAppConfig: inAppConfig, + pushConfig: pushConfig, + ); + + expect(config.cdpApiKey, 'testApiKey'); + expect(config.migrationSiteId, 'testMigrationSiteId'); + expect(config.region, Region.us); + expect(config.logLevel, CioLogLevel.debug); + expect(config.autoTrackDeviceAttributes, isTrue); + expect(config.apiHost, 'https://api.example.com'); + expect(config.cdnHost, 'https://cdn.example.com'); + expect(config.flushAt, 15); + expect(config.flushInterval, 45); + expect(config.inAppConfig, inAppConfig); + expect(config.pushConfig, pushConfig); + expect(config.source, 'Flutter'); + expect(config.version, plugin_info.version); + }); + + test('should return correct map from toMap()', () { + final inAppConfig = InAppConfig(siteId: 'testSiteId'); + final pushConfig = PushConfig( + android: PushConfigAndroid( + pushClickBehavior: + PushClickBehaviorAndroid.activityPreventRestart)); + + final config = CustomerIOConfig( + cdpApiKey: 'testApiKey', + migrationSiteId: 'testMigrationSiteId', + region: Region.eu, + logLevel: CioLogLevel.info, + autoTrackDeviceAttributes: false, + apiHost: 'https://api.example.com', + cdnHost: 'https://cdn.example.com', + flushAt: 25, + flushInterval: 55, + inAppConfig: inAppConfig, + pushConfig: pushConfig, + ); + + final expectedMap = { + 'cdpApiKey': 'testApiKey', + 'migrationSiteId': 'testMigrationSiteId', + 'region': 'eu', + 'logLevel': 'info', + 'autoTrackDeviceAttributes': false, + 'apiHost': 'https://api.example.com', + 'cdnHost': 'https://cdn.example.com', + 'flushAt': 25, + 'flushInterval': 55, + 'inAppConfig': inAppConfig.toMap(), + 'pushConfig': pushConfig.toMap(), + 'version': config.version, + 'source': config.source, + }; + + expect(config.toMap(), expectedMap); + }); + + test('should initialize default pushConfig when not provided', () { + final config = CustomerIOConfig(cdpApiKey: 'testApiKey'); + + expect(config.pushConfig.pushConfigAndroid.pushClickBehavior, + PushClickBehaviorAndroid.activityPreventRestart); + }); + }); + + group('CustomerIOConfig with Region', () { + for (var region in Region.values) { + test('should initialize with region $region and verify map value', () { + final config = CustomerIOConfig( + cdpApiKey: 'testApiKey', + region: region, + ); + + // Check initialization value + expect(config.region, region); + + // Verify only the region entry in toMap output + final map = config.toMap(); + expect(map['region'], region.name); + }); + } + }); + + group('CustomerIOConfig with LogLevel', () { + for (var logLevel in CioLogLevel.values) { + test('should initialize with log level $logLevel and verify map value', + () { + final config = CustomerIOConfig( + cdpApiKey: 'testApiKey', + logLevel: logLevel, + ); + + // Check initialization value + expect(config.logLevel, logLevel); + + // Verify only the logLevel entry in toMap output + final map = config.toMap(); + expect(map['logLevel'], logLevel.name); + }); + } + }); + + group('InAppConfig', () { + test('should return correct map from toMap()', () { + final inAppConfig = InAppConfig(siteId: 'testSiteId'); + final expectedMap = {'siteId': 'testSiteId'}; + + expect(inAppConfig.toMap(), expectedMap); + }); + }); + + group('PushConfig', () { + test('should initialize with default PushConfigAndroid', () { + final pushConfig = PushConfig(); + + expect(pushConfig.pushConfigAndroid, isNotNull); + expect(pushConfig.pushConfigAndroid.pushClickBehavior, + PushClickBehaviorAndroid.activityPreventRestart); + }); + + test('should return correct map from toMap()', () { + final pushConfig = PushConfig( + android: PushConfigAndroid( + pushClickBehavior: PushClickBehaviorAndroid.activityNoFlags, + ), + ); + + final expectedMap = { + 'android': { + 'pushClickBehavior': 'ACTIVITY_NO_FLAGS', + }, + }; + + expect(pushConfig.toMap(), expectedMap); + }); + }); + + group('PushConfigAndroid with PushClickBehaviorAndroid', () { + for (var pushClickBehavior in PushClickBehaviorAndroid.values) { + test( + 'should initialize with pushConfigAndroid $pushClickBehavior and verify map value', + () { + final config = PushConfigAndroid( + pushClickBehavior: pushClickBehavior, + ); + + // Check initialization value + expect(config.pushClickBehavior, pushClickBehavior); + + // Verify only the logLevel entry in toMap output + final map = config.toMap(); + expect(map['pushClickBehavior'], pushClickBehavior.rawValue); + }); + } + }); +} diff --git a/test/customer_io_method_channel_test.dart b/test/customer_io_method_channel_test.dart index aaf5e66..1a91ac8 100644 --- a/test/customer_io_method_channel_test.dart +++ b/test/customer_io_method_channel_test.dart @@ -52,11 +52,11 @@ void main() { test('initialize() should call platform method with correct arguments', () async { final customerIO = CustomerIOMethodChannel(); - final config = CustomerIOConfig(siteId: 'site_id', apiKey: 'api_key'); + final config = CustomerIOConfig(cdpApiKey: 'cdp_api_key'); await customerIO.initialize(config: config); expectMethodInvocationArguments( - 'initialize', {'siteId': config.siteId, 'apiKey': config.apiKey}); + 'initialize', {'cdpApiKey': config.cdpApiKey}); }); test('identify() should call platform method with correct arguments', diff --git a/test/customer_io_test.dart b/test/customer_io_test.dart index a312f8e..77b06e8 100644 --- a/test/customer_io_test.dart +++ b/test/customer_io_test.dart @@ -43,13 +43,13 @@ void main() { }); test('initialize() succeeds first time', () async { - final config = CustomerIOConfig(siteId: '123', apiKey: '456'); + final config = CustomerIOConfig(cdpApiKey: '123'); await CustomerIO.initialize(config: config); expect(() => CustomerIO.instance, isNot(throwsStateError)); }); test('subsequent initialize() calls are ignored', () async { - final config = CustomerIOConfig(siteId: '123', apiKey: '456'); + final config = CustomerIOConfig(cdpApiKey: '123'); // First initialization await CustomerIO.initialize(config: config); @@ -63,17 +63,17 @@ void main() { }); test('initialize() calls platform', () async { - final config = CustomerIOConfig(siteId: '123', apiKey: '456'); + final config = CustomerIOConfig(cdpApiKey: '123'); await CustomerIO.initialize(config: config); verify(mockPlatform.initialize(config: config)).called(1); }); test('initialize() correct arguments are passed', () async { final givenConfig = CustomerIOConfig( - siteId: '123', - apiKey: '456', + cdpApiKey: '123', + migrationSiteId: '456', region: Region.eu, - autoTrackPushEvents: false, + autoTrackDeviceAttributes: false, ); await CustomerIO.initialize(config: givenConfig); expect( @@ -89,7 +89,7 @@ void main() { late CustomerIOConfig config; setUp(() async { - config = CustomerIOConfig(siteId: '123', apiKey: '456'); + config = CustomerIOConfig(cdpApiKey: '123'); await CustomerIO.initialize(config: config); }); From 9eb5f459fcb7ff5375efc5a8061be07648960fb4 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 6 Nov 2024 14:27:35 +0500 Subject: [PATCH 06/34] chore: hook sample app with flutter source (#158) --- .github/workflows/build-sample-apps.yml | 2 +- apps/amiapp_flutter/.env.example | 2 +- apps/amiapp_flutter/lib/src/customer_io.dart | 18 +- apps/amiapp_flutter/lib/src/data/config.dart | 161 +++++++++++------- .../lib/src/screens/settings.dart | 60 +++---- 5 files changed, 136 insertions(+), 107 deletions(-) diff --git a/.github/workflows/build-sample-apps.yml b/.github/workflows/build-sample-apps.yml index ef247da..f10e08b 100644 --- a/.github/workflows/build-sample-apps.yml +++ b/.github/workflows/build-sample-apps.yml @@ -112,7 +112,7 @@ jobs: run: | cp ".env.example" ".env" sd 'SITE_ID=.*' "SITE_ID=${{ secrets[format('CUSTOMERIO_{0}_WORKSPACE_SITE_ID', matrix.sample-app)] }}" ".env" - sd 'API_KEY=.*' "API_KEY=${{ secrets[format('CUSTOMERIO_{0}_WORKSPACE_API_KEY', matrix.sample-app)] }}" ".env" + sd 'CDP_API_KEY=.*' "CDP_API_KEY=${{ secrets[format('CUSTOMERIO_{0}_WORKSPACE_CDP_API_KEY', matrix.sample-app)] }}" ".env" - name: Setup workspace credentials in iOS environment files run: | diff --git a/apps/amiapp_flutter/.env.example b/apps/amiapp_flutter/.env.example index 0bbd9a5..6678d50 100644 --- a/apps/amiapp_flutter/.env.example +++ b/apps/amiapp_flutter/.env.example @@ -1,2 +1,2 @@ SITE_ID=siteid -API_KEY=apikey +CDP_API_KEY=cdpapikey diff --git a/apps/amiapp_flutter/lib/src/customer_io.dart b/apps/amiapp_flutter/lib/src/customer_io.dart index 4605a70..313854a 100644 --- a/apps/amiapp_flutter/lib/src/customer_io.dart +++ b/apps/amiapp_flutter/lib/src/customer_io.dart @@ -55,23 +55,23 @@ class CustomerIOSDK extends ChangeNotifier { } final InAppConfig? inAppConfig; - if (_sdkConfig?.siteId != null) { - inAppConfig = InAppConfig(siteId: _sdkConfig!.siteId); + if (_sdkConfig?.migrationSiteId != null) { + inAppConfig = InAppConfig(siteId: _sdkConfig!.migrationSiteId ?? ''); } else { inAppConfig = null; } return CustomerIO.initialize( config: CustomerIOConfig( - cdpApiKey: '${_sdkConfig?.siteId}:${_sdkConfig?.apiKey}', - migrationSiteId: _sdkConfig?.siteId, + cdpApiKey: '${_sdkConfig?.cdnHost}:${_sdkConfig?.cdpApiKey}', + migrationSiteId: _sdkConfig?.migrationSiteId, region: Region.us, logLevel: logLevel, autoTrackDeviceAttributes: - _sdkConfig?.deviceAttributesTrackingEnabled, - apiHost: _sdkConfig?.trackingUrl, - cdnHost: _sdkConfig?.trackingUrl, - flushAt: _sdkConfig?.backgroundQueueMinNumOfTasks, - flushInterval: _sdkConfig?.backgroundQueueSecondsDelay?.toInt(), + _sdkConfig?.autoTrackDeviceAttributes, + apiHost: _sdkConfig?.apiHost, + cdnHost: _sdkConfig?.cdnHost, + flushAt: _sdkConfig?.flushAt, + flushInterval: _sdkConfig?.flushInterval?.toInt(), inAppConfig: inAppConfig, ), ); diff --git a/apps/amiapp_flutter/lib/src/data/config.dart b/apps/amiapp_flutter/lib/src/data/config.dart index 3c45552..c162d60 100644 --- a/apps/amiapp_flutter/lib/src/data/config.dart +++ b/apps/amiapp_flutter/lib/src/data/config.dart @@ -1,58 +1,87 @@ +import 'package:customer_io/config/in_app_config.dart'; +import 'package:customer_io/config/push_config.dart'; +import 'package:customer_io/customer_io_enums.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:shared_preferences/shared_preferences.dart'; class CustomerIOSDKConfig { - String siteId; - String apiKey; - String? trackingUrl; - double? backgroundQueueSecondsDelay; - int? backgroundQueueMinNumOfTasks; - bool screenTrackingEnabled; - bool deviceAttributesTrackingEnabled; - bool debugModeEnabled; + final String cdpApiKey; + final String? migrationSiteId; + final Region? region; + final bool? debugModeEnabled; + final bool? screenTrackingEnabled; + final bool? autoTrackDeviceAttributes; + final String? apiHost; + final String? cdnHost; + final int? flushAt; + final int? flushInterval; + final InAppConfig? inAppConfig; + final PushConfig pushConfig; CustomerIOSDKConfig({ - required this.siteId, - required this.apiKey, - this.trackingUrl = "https://track-sdk.customer.io/", - this.backgroundQueueSecondsDelay = 30.0, - this.backgroundQueueMinNumOfTasks = 10, - this.screenTrackingEnabled = true, - this.deviceAttributesTrackingEnabled = true, - this.debugModeEnabled = true, - }); + required this.cdpApiKey, + this.migrationSiteId, + this.region, + this.debugModeEnabled, + this.screenTrackingEnabled, + this.autoTrackDeviceAttributes, + this.apiHost, + this.cdnHost, + this.flushAt, + this.flushInterval, + this.inAppConfig, + PushConfig? pushConfig, + }) : pushConfig = pushConfig ?? PushConfig(); - factory CustomerIOSDKConfig.fromEnv() => CustomerIOSDKConfig( - siteId: dotenv.env[_PreferencesKey.siteId]!, - apiKey: dotenv.env[_PreferencesKey.apiKey]!); + factory CustomerIOSDKConfig.fromEnv() => + CustomerIOSDKConfig( + cdpApiKey: dotenv.env[_PreferencesKey.cdpApiKey]!, + migrationSiteId: dotenv.env[_PreferencesKey.migrationSiteId], + ); factory CustomerIOSDKConfig.fromPrefs(SharedPreferences prefs) { - final siteId = prefs.getString(_PreferencesKey.siteId); - final apiKey = prefs.getString(_PreferencesKey.apiKey); + final cdpApiKey = prefs.getString(_PreferencesKey.cdpApiKey); - if (siteId == null) { - throw ArgumentError('siteId cannot be null'); - } else if (apiKey == null) { - throw ArgumentError('apiKey cannot be null'); + if (cdpApiKey == null) { + throw ArgumentError('cdpApiKey cannot be null'); } return CustomerIOSDKConfig( - siteId: siteId, - apiKey: apiKey, - trackingUrl: prefs.getString(_PreferencesKey.trackingUrl), - backgroundQueueSecondsDelay: - prefs.getDouble(_PreferencesKey.backgroundQueueSecondsDelay), - backgroundQueueMinNumOfTasks: - prefs.getInt(_PreferencesKey.backgroundQueueMinNumOfTasks), - screenTrackingEnabled: - prefs.getBool(_PreferencesKey.screenTrackingEnabled) != false, - deviceAttributesTrackingEnabled: - prefs.getBool(_PreferencesKey.deviceAttributesTrackingEnabled) != - false, - debugModeEnabled: - prefs.getBool(_PreferencesKey.debugModeEnabled) != false, + cdpApiKey: cdpApiKey, + migrationSiteId: prefs.getString(_PreferencesKey.migrationSiteId), + region: prefs.getString(_PreferencesKey.region) != null + ? Region.values.firstWhere( + (e) => e.name == prefs.getString(_PreferencesKey.region)) + : null, + debugModeEnabled: prefs.getBool(_PreferencesKey.debugModeEnabled) != + false, + screenTrackingEnabled: prefs.getBool( + _PreferencesKey.screenTrackingEnabled) != false, + autoTrackDeviceAttributes: + prefs.getBool(_PreferencesKey.autoTrackDeviceAttributes), + apiHost: prefs.getString(_PreferencesKey.apiHost), + cdnHost: prefs.getString(_PreferencesKey.cdnHost), + flushAt: prefs.getInt(_PreferencesKey.flushAt), + flushInterval: prefs.getInt(_PreferencesKey.flushInterval), ); } + + Map toMap() { + return { + 'cdpApiKey': cdpApiKey, + 'migrationSiteId': migrationSiteId, + 'region': region?.name, + 'logLevel': debugModeEnabled, + 'screenTrackingEnabled': screenTrackingEnabled, + 'autoTrackDeviceAttributes': autoTrackDeviceAttributes, + 'apiHost': apiHost, + 'cdnHost': cdnHost, + 'flushAt': flushAt, + 'flushInterval': flushInterval, + 'inAppConfig': inAppConfig?.toMap(), + 'pushConfig': pushConfig.toMap(), + }; + } } extension ConfigurationPreferencesExtensions on SharedPreferences { @@ -66,10 +95,6 @@ extension ConfigurationPreferencesExtensions on SharedPreferences { return value != null ? setInt(key, value) : remove(key); } - Future setOrRemoveDouble(String key, double? value) { - return value != null ? setDouble(key, value) : remove(key); - } - Future setOrRemoveBool(String key, bool? value) { return value != null ? setBool(key, value) : remove(key); } @@ -77,39 +102,43 @@ extension ConfigurationPreferencesExtensions on SharedPreferences { Future saveSDKConfigState(CustomerIOSDKConfig config) async { bool result = true; result = result && - await setOrRemoveString(_PreferencesKey.siteId, config.siteId); - result = result && - await setOrRemoveString(_PreferencesKey.apiKey, config.apiKey); + await setOrRemoveString(_PreferencesKey.cdpApiKey, config.cdpApiKey); result = result && await setOrRemoveString( - _PreferencesKey.trackingUrl, config.trackingUrl); - result = result && - await setOrRemoveDouble(_PreferencesKey.backgroundQueueSecondsDelay, - config.backgroundQueueSecondsDelay); + _PreferencesKey.migrationSiteId, config.migrationSiteId); result = result && - await setOrRemoveInt(_PreferencesKey.backgroundQueueMinNumOfTasks, - config.backgroundQueueMinNumOfTasks); + await setOrRemoveString(_PreferencesKey.region, config.region?.name); result = result && - await setOrRemoveBool(_PreferencesKey.screenTrackingEnabled, - config.screenTrackingEnabled); + await setOrRemoveBool( + _PreferencesKey.debugModeEnabled, config.debugModeEnabled); result = result && - await setOrRemoveBool(_PreferencesKey.deviceAttributesTrackingEnabled, - config.deviceAttributesTrackingEnabled); + await setOrRemoveBool(_PreferencesKey.autoTrackDeviceAttributes, + config.autoTrackDeviceAttributes); result = result && await setOrRemoveBool( - _PreferencesKey.debugModeEnabled, config.debugModeEnabled); + _PreferencesKey.screenTrackingEnabled, config.screenTrackingEnabled); + result = result && + await setOrRemoveString(_PreferencesKey.apiHost, config.apiHost); + result = result && + await setOrRemoveString(_PreferencesKey.cdnHost, config.cdnHost); + result = + result && await setOrRemoveInt(_PreferencesKey.flushAt, config.flushAt); + result = result && + await setOrRemoveInt( + _PreferencesKey.flushInterval, config.flushInterval); return result; } } class _PreferencesKey { - static const siteId = 'SITE_ID'; - static const apiKey = 'API_KEY'; - static const trackingUrl = 'TRACKING_URL'; - static const backgroundQueueSecondsDelay = 'BACKGROUND_QUEUE_SECONDS_DELAY'; - static const backgroundQueueMinNumOfTasks = - 'BACKGROUND_QUEUE_MIN_NUMBER_OF_TASKS'; - static const screenTrackingEnabled = 'TRACK_SCREENS'; - static const deviceAttributesTrackingEnabled = 'TRACK_DEVICE_ATTRIBUTES'; + static const cdpApiKey = 'CDP_API_KEY'; + static const migrationSiteId = 'SITE_ID'; + static const region = 'REGION'; static const debugModeEnabled = 'DEBUG_MODE'; + static const screenTrackingEnabled = 'SCREEN_TRACKING'; + static const autoTrackDeviceAttributes = 'AUTO_TRACK_DEVICE_ATTRIBUTES'; + static const apiHost = 'API_HOST'; + static const cdnHost = 'CDN_HOST'; + static const flushAt = 'FLUSH_AT'; + static const flushInterval = 'FLUSH_INTERVAL'; } diff --git a/apps/amiapp_flutter/lib/src/screens/settings.dart b/apps/amiapp_flutter/lib/src/screens/settings.dart index 5899d82..30aab80 100644 --- a/apps/amiapp_flutter/lib/src/screens/settings.dart +++ b/apps/amiapp_flutter/lib/src/screens/settings.dart @@ -52,19 +52,19 @@ class _SettingsScreenState extends State { final cioConfig = widget._customerIOSDK.sdkConfig; _deviceTokenValueController = TextEditingController(); - _trackingURLValueController = - TextEditingController(text: cioConfig?.trackingUrl); + // _trackingURLValueController = + // TextEditingController(text: cioConfig?.trackingUrl); _siteIDValueController = TextEditingController( - text: widget.siteIdInitialValue ?? cioConfig?.siteId); + text: widget.siteIdInitialValue ?? cioConfig?.migrationSiteId); _apiKeyValueController = TextEditingController( - text: widget.apiKeyInitialValue ?? cioConfig?.apiKey); - _bqSecondsDelayValueController = TextEditingController( - text: cioConfig?.backgroundQueueSecondsDelay?.toTrimmedString()); - _bqMinNumberOfTasksValueController = TextEditingController( - text: cioConfig?.backgroundQueueMinNumOfTasks?.toString()); - _featureTrackScreens = cioConfig?.screenTrackingEnabled ?? true; - _featureTrackDeviceAttributes = - cioConfig?.deviceAttributesTrackingEnabled ?? true; + text: widget.apiKeyInitialValue ?? cioConfig?.cdpApiKey); + // _bqSecondsDelayValueController = TextEditingController( + // text: cioConfig?.backgroundQueueSecondsDelay?.toTrimmedString()); + // _bqMinNumberOfTasksValueController = TextEditingController( + // text: cioConfig?.backgroundQueueMinNumOfTasks?.toString()); + // _featureTrackScreens = cioConfig?.screenTrackingEnabled ?? true; + // _featureTrackDeviceAttributes = + // cioConfig?.deviceAttributesTrackingEnabled ?? true; _featureDebugMode = cioConfig?.debugModeEnabled ?? true; super.initState(); @@ -76,15 +76,15 @@ class _SettingsScreenState extends State { } final newConfig = CustomerIOSDKConfig( - siteId: _siteIDValueController.text.trim(), - apiKey: _apiKeyValueController.text.trim(), - trackingUrl: _trackingURLValueController.text.trim(), - backgroundQueueSecondsDelay: - _bqSecondsDelayValueController.text.trim().toDoubleOrNull(), - backgroundQueueMinNumOfTasks: - _bqMinNumberOfTasksValueController.text.trim().toIntOrNull(), + migrationSiteId: _siteIDValueController.text.trim(), + cdpApiKey: _apiKeyValueController.text.trim(), + // trackingUrl: _trackingURLValueController.text.trim(), + // backgroundQueueSecondsDelay: + // _bqSecondsDelayValueController.text.trim().toDoubleOrNull(), + // backgroundQueueMinNumOfTasks: + // _bqMinNumberOfTasksValueController.text.trim().toIntOrNull(), screenTrackingEnabled: _featureTrackScreens, - deviceAttributesTrackingEnabled: _featureTrackDeviceAttributes, + // deviceAttributesTrackingEnabled: _featureTrackDeviceAttributes, debugModeEnabled: _featureDebugMode, ); widget._customerIOSDK.saveConfigToPreferences(newConfig).then((success) { @@ -109,17 +109,17 @@ class _SettingsScreenState extends State { } setState(() { - _siteIDValueController.text = defaultConfig.siteId; - _apiKeyValueController.text = defaultConfig.apiKey; - _trackingURLValueController.text = defaultConfig.trackingUrl ?? ''; - _bqSecondsDelayValueController.text = - defaultConfig.backgroundQueueSecondsDelay?.toTrimmedString() ?? ''; - _bqMinNumberOfTasksValueController.text = - defaultConfig.backgroundQueueMinNumOfTasks?.toString() ?? ''; - _featureTrackScreens = defaultConfig.screenTrackingEnabled; - _featureTrackDeviceAttributes = - defaultConfig.deviceAttributesTrackingEnabled; - _featureDebugMode = defaultConfig.debugModeEnabled; + _siteIDValueController.text = defaultConfig.migrationSiteId ?? ''; + _apiKeyValueController.text = defaultConfig.cdpApiKey; + // _trackingURLValueController.text = defaultConfig.trackingUrl ?? ''; + // _bqSecondsDelayValueController.text = + // defaultConfig.backgroundQueueSecondsDelay?.toTrimmedString() ?? ''; + // _bqMinNumberOfTasksValueController.text = + // defaultConfig.backgroundQueueMinNumOfTasks?.toString() ?? ''; + // _featureTrackScreens = defaultConfig.screenTrackingEnabled; + // _featureTrackDeviceAttributes = + // defaultConfig.deviceAttributesTrackingEnabled; + _featureDebugMode = defaultConfig.debugModeEnabled ?? true; _saveSettings(context); }); } From e9af59b0d0cab7fc016c484ff9fb7dc82d44aa52 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Wed, 6 Nov 2024 15:15:59 +0500 Subject: [PATCH 07/34] chore: initialize native ios sdk in flutter (#160) --- .../Bridge/CustomerIOSDKConfigMapper.swift | 59 +++++++++++++++++++ ios/Classes/SwiftCustomerIoPlugin.swift | 33 ++++------- 2 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift diff --git a/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift b/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift new file mode 100644 index 0000000..aa31776 --- /dev/null +++ b/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift @@ -0,0 +1,59 @@ +import CioDataPipelines + +enum SDKConfigBuilderError: Error { + case missingCdpApiKey +} + +extension SDKConfigBuilder { + private enum Config: String { + case migrationSiteId + case cdpApiKey + case region + case logLevel + case autoTrackDeviceAttributes + case trackApplicationLifecycleEvents + case flushAt + case flushInterval + case apiHost + case cdnHost + } + + @available(iOSApplicationExtension, unavailable) + static func create(from config: [String: Any?]) throws -> SDKConfigBuilder { + guard let cdpApiKey = config[Config.cdpApiKey.rawValue] as? String else { + throw SDKConfigBuilderError.missingCdpApiKey + } + + let builder = SDKConfigBuilder(cdpApiKey: cdpApiKey) + Config.migrationSiteId.ifNotNil(in: config, thenPassItTo: builder.migrationSiteId) + Config.region.ifNotNil(in: config, thenPassItTo: builder.region, transformingBy: Region.getRegion) + Config.logLevel.ifNotNil(in: config, thenPassItTo: builder.logLevel, transformingBy: CioLogLevel.getLogLevel) + Config.autoTrackDeviceAttributes.ifNotNil(in: config, thenPassItTo: builder.autoTrackDeviceAttributes) + Config.trackApplicationLifecycleEvents.ifNotNil(in: config, thenPassItTo: builder.trackApplicationLifecycleEvents) + Config.flushAt.ifNotNil(in: config, thenPassItTo: builder.flushAt) { (value: NSNumber) in value.intValue } + Config.flushInterval.ifNotNil(in: config, thenPassItTo: builder.flushInterval) { (value: NSNumber) in value.doubleValue } + Config.apiHost.ifNotNil(in: config, thenPassItTo: builder.apiHost) + Config.cdnHost.ifNotNil(in: config, thenPassItTo: builder.cdnHost) + + return builder + } +} + +extension RawRepresentable where RawValue == String { + func ifNotNil( + in config: [String: Any?]?, + thenPassItTo handler: (Raw) -> Any + ) { + ifNotNil(in: config, thenPassItTo: handler) { $0 } + } + + func ifNotNil( + in config: [String: Any?]?, + thenPassItTo handler: (Transformed) -> Any, + transformingBy transform: (Raw) -> Transformed? + ) { + if let value = config?[self.rawValue] as? Raw, let result = transform(value) { + _ = handler(result) + } + } +} diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index dc98f14..ebcb1fa 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -1,5 +1,6 @@ import Flutter import UIKit +import CioDataPipelines import CioInternalCommon import CioMessagingInApp @@ -7,6 +8,8 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private var methodChannel: FlutterMethodChannel! private var inAppMessagingChannelHandler: CusomterIOInAppMessaging! + + private let logger: CioInternalCommon.Logger = DIGraphShared.shared.logger public static func register(with registrar: FlutterPluginRegistrar) { let instance = SwiftCustomerIoPlugin() @@ -175,29 +178,15 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } private func initialize(params : Dictionary){ - // TODO: Fix initialize implementation - /* - guard let siteId = params[Keys.Environment.siteId] as? String, - let apiKey = params[Keys.Environment.apiKey] as? String, - let regionStr = params[Keys.Environment.region] as? String - else { - return - } - - let region = Region.getRegion(from: regionStr) - - CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: region){ - config in - config.modify(params: params) - } - - - if let enableInApp = params[Keys.Environment.enableInApp] as? Bool { - if enableInApp{ - initializeInApp() - } + do { + let sdkConfigBuilder = try SDKConfigBuilder.create(from: params) + CustomerIO.initialize(withConfig: sdkConfigBuilder.build()) + + // TODO: Initialize in-app module with given config + logger.debug("Customer.io SDK initialized with config: \(params)") + } catch { + logger.error("Initializing Customer.io SDK failed with error: \(error)") } - */ } /** From 380a50f5cd3c21f2f9d90ea12567eee1e972a8ea Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Wed, 6 Nov 2024 15:23:58 +0500 Subject: [PATCH 08/34] chore: initialize native android sdk in flutter (#159) --- .../customer/customer_io/CustomerIoPlugin.kt | 83 ++++++++----------- .../io/customer/customer_io/constant/Keys.kt | 7 -- 2 files changed, 33 insertions(+), 57 deletions(-) diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 10a139c..5c12a1f 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -7,15 +7,13 @@ import androidx.annotation.NonNull import io.customer.customer_io.constant.Keys import io.customer.customer_io.messaginginapp.CustomerIOInAppMessaging import io.customer.customer_io.messagingpush.CustomerIOPushMessaging -import io.customer.messaginginapp.MessagingInAppModuleConfig -import io.customer.messaginginapp.ModuleMessagingInApp import io.customer.messaginginapp.type.InAppEventListener import io.customer.messaginginapp.type.InAppMessage -import io.customer.messagingpush.MessagingPushModuleConfig import io.customer.messagingpush.ModuleMessagingPushFCM -import io.customer.messagingpush.config.PushClickBehavior import io.customer.sdk.CustomerIO +import io.customer.sdk.CustomerIOBuilder import io.customer.sdk.core.di.SDKComponent +import io.customer.sdk.core.util.CioLogLevel import io.customer.sdk.core.util.Logger import io.customer.sdk.data.model.Region import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -239,57 +237,42 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { */ } - private fun initialize(configData: Map) { - // TODO: Fix initialize implementation - /* + private fun initialize(args: Map): kotlin.Result = runCatching { val application: Application = context.applicationContext as Application - val siteId = configData.getString(Keys.Environment.SITE_ID) - val apiKey = configData.getString(Keys.Environment.API_KEY) - val region = configData.getProperty( - Keys.Environment.REGION - )?.takeIfNotBlank() - val enableInApp = configData.getProperty( - Keys.Environment.ENABLE_IN_APP - ) + val cdpApiKey = requireNotNull(args.getAsTypeOrNull("cdpApiKey")) { + "CDP API Key is required to initialize Customer.io" + } - // Checks if SDK was initialized before, which means lifecycle callbacks are already - // registered as well - val isLifecycleCallbacksRegistered = kotlin.runCatching { CustomerIO.instance() }.isSuccess + val logLevelRawValue = args.getAsTypeOrNull("logLevel") + val regionRawValue = args.getAsTypeOrNull("region") + val givenRegion = regionRawValue.let { Region.getRegion(it) } - val customerIO = CustomerIO.Builder( - siteId = siteId, - apiKey = apiKey, - region = Region.getRegion(region), - appContext = application, - config = configData + CustomerIOBuilder( + applicationContext = application, + cdpApiKey = cdpApiKey ).apply { - addCustomerIOModule(module = configureModuleMessagingPushFCM(configData)) - if (enableInApp == true) { - addCustomerIOModule( - module = ModuleMessagingInApp( - config = MessagingInAppModuleConfig.Builder() - .setEventListener(CustomerIOInAppEventListener { method, args -> - this@CustomerIoPlugin.activity?.get()?.runOnUiThread { - flutterCommunicationChannel.invokeMethod(method, args) - } - }).build(), - ) - ) - } + logLevelRawValue?.let { logLevel(CioLogLevel.getLogLevel(it)) } + regionRawValue?.let { region(givenRegion) } + + args.getAsTypeOrNull("migrationSiteId")?.let(::migrationSiteId) + args.getAsTypeOrNull("autoTrackDeviceAttributes") + ?.let(::autoTrackDeviceAttributes) + args.getAsTypeOrNull("trackApplicationLifecycleEvents") + ?.let(::trackApplicationLifecycleEvents) + + args.getAsTypeOrNull("flushAt")?.let(::flushAt) + args.getAsTypeOrNull("flushInterval")?.let(::flushInterval) + + args.getAsTypeOrNull("apiHost")?.let(::apiHost) + args.getAsTypeOrNull("cdnHost")?.let(::cdnHost) + + // TODO: Initialize in-app module with given config + // TODO: Initialize push module with given config }.build() - logger.info("Customer.io instance initialized successfully") - - // Request lifecycle events for first initialization only as relaunching app - // in wrapper SDKs may result in reinitialization of SDK and lifecycle listener - // will already be attached in this case as they are registered to application object. - if (!isLifecycleCallbacksRegistered) { - activity?.get()?.let { activity -> - logger.info("Requesting delayed activity lifecycle events") - val lifecycleCallbacks = customerIO.diGraph.activityLifecycleCallbacks - lifecycleCallbacks.postDelayedEventsForNonNativeActivity(activity) - } - } - */ + + logger.info("Customer.io instance initialized successfully from app") + }.onFailure { ex -> + logger.error("Failed to initialize Customer.io instance from app, ${ex.message}") } private fun configureModuleMessagingPushFCM(config: Map?): ModuleMessagingPushFCM { diff --git a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt index 4f09b1a..1bc5ec0 100644 --- a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt +++ b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt @@ -25,11 +25,4 @@ internal object Keys { const val DELIVERY_TOKEN = "deliveryToken" const val METRIC_EVENT = "metricEvent" } - - object Environment { - const val SITE_ID = "siteId" - const val API_KEY = "apiKey" - const val REGION = "region" - const val ENABLE_IN_APP = "enableInApp" - } } From 6fffd1536ff3c12d902975a346d118845c2408ea Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Thu, 7 Nov 2024 03:30:09 +0500 Subject: [PATCH 09/34] chore: added config for tracking lifecycle (#161) --- lib/config/customer_io_config.dart | 3 +++ test/customer_io_config_test.dart | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/config/customer_io_config.dart b/lib/config/customer_io_config.dart index 55819f0..c65d512 100644 --- a/lib/config/customer_io_config.dart +++ b/lib/config/customer_io_config.dart @@ -11,6 +11,7 @@ class CustomerIOConfig { final String? migrationSiteId; final Region? region; final CioLogLevel? logLevel; + final bool? trackApplicationLifecycleEvents; final bool? autoTrackDeviceAttributes; final String? apiHost; final String? cdnHost; @@ -25,6 +26,7 @@ class CustomerIOConfig { this.region, this.logLevel, this.autoTrackDeviceAttributes, + this.trackApplicationLifecycleEvents, this.apiHost, this.cdnHost, this.flushAt, @@ -40,6 +42,7 @@ class CustomerIOConfig { 'region': region?.name, 'logLevel': logLevel?.name, 'autoTrackDeviceAttributes': autoTrackDeviceAttributes, + 'trackApplicationLifecycleEvents': trackApplicationLifecycleEvents, 'apiHost': apiHost, 'cdnHost': cdnHost, 'flushAt': flushAt, diff --git a/test/customer_io_config_test.dart b/test/customer_io_config_test.dart index 3a1e2c1..acd9009 100644 --- a/test/customer_io_config_test.dart +++ b/test/customer_io_config_test.dart @@ -82,6 +82,7 @@ void main() { region: Region.eu, logLevel: CioLogLevel.info, autoTrackDeviceAttributes: false, + trackApplicationLifecycleEvents: false, apiHost: 'https://api.example.com', cdnHost: 'https://cdn.example.com', flushAt: 25, @@ -96,6 +97,7 @@ void main() { 'region': 'eu', 'logLevel': 'info', 'autoTrackDeviceAttributes': false, + 'trackApplicationLifecycleEvents': false, 'apiHost': 'https://api.example.com', 'cdnHost': 'https://cdn.example.com', 'flushAt': 25, From 40c14f0cb7c45135e52dbcf07384491cf48a3447 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Thu, 7 Nov 2024 23:14:35 +0500 Subject: [PATCH 10/34] chore: added identify and attributes (#163) --- .../customer/customer_io/CustomerIoPlugin.kt | 28 ++-- .../io/customer/customer_io/constant/Keys.kt | 4 +- apps/amiapp_flutter/lib/src/app.dart | 2 +- ios/Classes/Keys.swift | 4 +- ios/Classes/SwiftCustomerIoPlugin.swift | 134 +++++++++--------- lib/customer_io.dart | 14 +- lib/customer_io_const.dart | 3 +- lib/customer_io_method_channel.dart | 14 +- lib/customer_io_platform_interface.dart | 6 +- test/customer_io_method_channel_test.dart | 12 +- test/customer_io_test.dart | 18 +-- test/customer_io_test.mocks.dart | 12 +- 12 files changed, 126 insertions(+), 125 deletions(-) diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 5c12a1f..4e1fb75 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -22,7 +22,6 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result import java.lang.ref.WeakReference /** @@ -153,13 +152,21 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun identify(params: Map) { - // TODO: Fix identify implementation - /* - val identifier = params.getString(Keys.Tracking.IDENTIFIER) - val attributes = - params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: emptyMap() - CustomerIO.instance().identify(identifier, attributes) - */ + val userId = params.getAsTypeOrNull(Keys.Tracking.USER_ID) + val traits = params.getAsTypeOrNull>(Keys.Tracking.TRAITS) ?: emptyMap() + + if (userId == null && traits.isEmpty()) { + logger.error("Please provide either an ID or traits to identify.") + return + } + + if (userId != null && traits.isNotEmpty()) { + CustomerIO.instance().identify(userId, traits) + } else if (userId != null) { + CustomerIO.instance().identify(userId) + } else { + CustomerIO.instance().profileAttributes = traits + } } private fun track(params: Map) { @@ -214,12 +221,9 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun setProfileAttributes(params: Map) { - // TODO: Fix setProfileAttributes implementation - /* - val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: return + val attributes = params.getAsTypeOrNull>(Keys.Tracking.TRAITS) ?: return CustomerIO.instance().profileAttributes = attributes - */ } private fun screen(params: Map) { diff --git a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt index 1bc5ec0..81670dc 100644 --- a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt +++ b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt @@ -17,8 +17,8 @@ internal object Keys { } object Tracking { - const val IDENTIFIER = "identifier" - const val ATTRIBUTES = "attributes" + const val USER_ID = "userId" + const val TRAITS = "traits" const val EVENT_NAME = "eventName" const val TOKEN = "token" const val DELIVERY_ID = "deliveryId" diff --git a/apps/amiapp_flutter/lib/src/app.dart b/apps/amiapp_flutter/lib/src/app.dart index 33f00be..9ca6f17 100644 --- a/apps/amiapp_flutter/lib/src/app.dart +++ b/apps/amiapp_flutter/lib/src/app.dart @@ -79,7 +79,7 @@ class _AmiAppState extends State { onLogin: (user) { _auth.login(user).then((signedIn) { if (signedIn) { - CustomerIO.instance.identify(identifier: user.email, attributes: { + CustomerIO.instance.identify(userId: user.email, traits: { "first_name": user.displayName, "email": user.email, "is_guest": user.isGuest, diff --git a/ios/Classes/Keys.swift b/ios/Classes/Keys.swift index 06f1f62..bc15524 100644 --- a/ios/Classes/Keys.swift +++ b/ios/Classes/Keys.swift @@ -16,8 +16,8 @@ struct Keys { } struct Tracking { - static let identifier = "identifier" - static let attributes = "attributes" + static let userId = "userId" + static let traits = "traits" static let eventName = "eventName" static let token = "token" static let deliveryId = "deliveryId" diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index ebcb1fa..bc5e665 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -8,7 +8,6 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private var methodChannel: FlutterMethodChannel! private var inAppMessagingChannelHandler: CusomterIOInAppMessaging! - private let logger: CioInternalCommon.Logger = DIGraphShared.shared.logger public static func register(with registrar: FlutterPluginRegistrar) { @@ -70,20 +69,22 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } private func identify(params : Dictionary){ - // TODO: Fix identify implementation - /* - guard let identifier = params[Keys.Tracking.identifier] as? String - else { - return - } - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else{ - CustomerIO.shared.identify(identifier: identifier) + let userId = params[Keys.Tracking.userId] as? String + let traits = params[Keys.Tracking.traits] as? Dictionary ?? [:] + + if userId == nil && traits.isEmpty { + logger.error("Please provide either an ID or traits to identify.") return } - CustomerIO.shared.identify(identifier: identifier, body: attributes) - */ + if let userId = userId, !traits.isEmpty { + CustomerIO.shared.identify(userId: userId, traits: traits) + } else if let userId = userId { + CustomerIO.shared.identify(userId: userId) + } else { + CustomerIO.shared.profileAttributes = traits + } } private func clearIdentify() { @@ -93,17 +94,17 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private func track(params : Dictionary) { // TODO: Fix track implementation /* - guard let name = params[Keys.Tracking.eventName] as? String - else { - return - } - - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else{ - CustomerIO.shared.track(name: name) - return - } - - CustomerIO.shared.track(name: name, data: attributes) + guard let name = params[Keys.Tracking.eventName] as? String + else { + return + } + + guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else{ + CustomerIO.shared.track(name: name) + return + } + + CustomerIO.shared.track(name: name, data: attributes) */ } @@ -111,17 +112,17 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { func screen(params : Dictionary) { // TODO: Fix screen implementation /* - guard let name = params[Keys.Tracking.eventName] as? String - else { - return - } - - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else{ - CustomerIO.shared.screen(name: name) - return - } - - CustomerIO.shared.screen(name: name, data: attributes) + guard let name = params[Keys.Tracking.eventName] as? String + else { + return + } + + guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else{ + CustomerIO.shared.screen(name: name) + return + } + + CustomerIO.shared.screen(name: name, data: attributes) */ } @@ -129,51 +130,46 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private func setDeviceAttributes(params : Dictionary){ // TODO: Fix setDeviceAttributes implementation /* - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary - else { - return - } - CustomerIO.shared.deviceAttributes = attributes + guard let attributes = params[Keys.Tracking.attributes] as? Dictionary + else { + return + } + CustomerIO.shared.deviceAttributes = attributes */ } private func setProfileAttributes(params : Dictionary){ - // TODO: Fix setProfileAttributes implementation - /* - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary - else { - return - } + guard let attributes = params[Keys.Tracking.traits] as? Dictionary + else { return } CustomerIO.shared.profileAttributes = attributes - */ } private func registerDeviceToken(params : Dictionary){ // TODO: Fix registerDeviceToken implementation /* - guard let token = params[Keys.Tracking.token] as? String - else { - return - } - - CustomerIO.shared.registerDeviceToken(token) + guard let token = params[Keys.Tracking.token] as? String + else { + return + } + + CustomerIO.shared.registerDeviceToken(token) */ } private func trackMetric(params : Dictionary){ // TODO: Fix trackMetric implementation /* - guard let deliveryId = params[Keys.Tracking.deliveryId] as? String, - let deviceToken = params[Keys.Tracking.deliveryToken] as? String, - let metricEvent = params[Keys.Tracking.metricEvent] as? String, - let event = Metric.getEvent(from: metricEvent) - else { - return - } - - CustomerIO.shared.trackMetric(deliveryID: deliveryId, - event: event, - deviceToken: deviceToken) + guard let deliveryId = params[Keys.Tracking.deliveryId] as? String, + let deviceToken = params[Keys.Tracking.deliveryToken] as? String, + let metricEvent = params[Keys.Tracking.metricEvent] as? String, + let event = Metric.getEvent(from: metricEvent) + else { + return + } + + CustomerIO.shared.trackMetric(deliveryID: deliveryId, + event: event, + deviceToken: deviceToken) */ } @@ -195,13 +191,13 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private func initializeInApp(){ // TODO: Fix initializeInApp implementation /* - DispatchQueue.main.async { - MessagingInApp.shared.initialize(eventListener: CustomerIOInAppEventListener( - invokeMethod: {method,args in - self.invokeMethod(method, args) - }) - ) - } + DispatchQueue.main.async { + MessagingInApp.shared.initialize(eventListener: CustomerIOInAppEventListener( + invokeMethod: {method,args in + self.invokeMethod(method, args) + }) + ) + } */ } diff --git a/lib/customer_io.dart b/lib/customer_io.dart index 4584e5b..142bcd8 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -80,17 +80,17 @@ class CustomerIO { } } - /// Identify a person using a unique identifier, eg. email id. + /// Identify a person using a unique userId, eg. email id. /// Note that you can identify only 1 profile at a time. In case, multiple /// identifiers are attempted to be identified, then the last identified profile /// will be removed automatically. /// - /// @param identifier unique identifier for a profile - /// @param attributes (Optional) params to set profile attributes + /// @param userId unique identifier for a profile + /// @param traits (Optional) params to set profile attributes void identify( - {required String identifier, - Map attributes = const {}}) { - return _platform.identify(identifier: identifier, attributes: attributes); + {required String userId, + Map traits = const {}}) { + return _platform.identify(userId: userId, traits: traits); } /// Call this function to stop identifying a person. @@ -148,7 +148,7 @@ class CustomerIO { /// /// @param attributes additional attributes for a user profile void setProfileAttributes({required Map attributes}) { - return _platform.setProfileAttributes(attributes: attributes); + return _platform.setProfileAttributes(traits: attributes); } /// Subscribes to an in-app event listener. diff --git a/lib/customer_io_const.dart b/lib/customer_io_const.dart index b7dfa58..c6ca7a1 100644 --- a/lib/customer_io_const.dart +++ b/lib/customer_io_const.dart @@ -13,7 +13,8 @@ class MethodConsts { } class TrackingConsts { - static const String identifier = "identifier"; + static const String userId = "userId"; + static const String traits = "traits"; static const String attributes = "attributes"; static const String eventName = "eventName"; static const String token = "token"; diff --git a/lib/customer_io_method_channel.dart b/lib/customer_io_method_channel.dart index 1f64cd2..9510b99 100644 --- a/lib/customer_io_method_channel.dart +++ b/lib/customer_io_method_channel.dart @@ -71,18 +71,18 @@ class CustomerIOMethodChannel extends CustomerIOPlatform { } } - /// Identify a person using a unique identifier, eg. email id. + /// Identify a person using a unique userId, eg. email id. /// Note that you can identify only 1 profile at a time. In case, multiple /// identifiers are attempted to be identified, then the last identified profile /// will be removed automatically. @override void identify( - {required String identifier, - Map attributes = const {}}) async { + {required String userId, + Map traits = const {}}) async { try { final payload = { - TrackingConsts.identifier: identifier, - TrackingConsts.attributes: attributes + TrackingConsts.userId: userId, + TrackingConsts.traits: traits }; methodChannel.invokeMethod(MethodConsts.identify, payload); } on PlatformException catch (exception) { @@ -168,9 +168,9 @@ class CustomerIOMethodChannel extends CustomerIOPlatform { /// Set custom user profile information such as user preference, specific /// user actions etc @override - void setProfileAttributes({required Map attributes}) { + void setProfileAttributes({required Map traits}) { try { - final payload = {TrackingConsts.attributes: attributes}; + final payload = {TrackingConsts.traits: traits}; methodChannel.invokeMethod(MethodConsts.setProfileAttributes, payload); } on PlatformException catch (exception) { handleException(exception); diff --git a/lib/customer_io_platform_interface.dart b/lib/customer_io_platform_interface.dart index 2ab9be0..f85f0d5 100644 --- a/lib/customer_io_platform_interface.dart +++ b/lib/customer_io_platform_interface.dart @@ -35,8 +35,8 @@ abstract class CustomerIOPlatform extends PlatformInterface { } void identify( - {required String identifier, - Map attributes = const {}}) { + {required String userId, + Map traits = const {}}) { throw UnimplementedError('identify() has not been implemented.'); } @@ -69,7 +69,7 @@ abstract class CustomerIOPlatform extends PlatformInterface { throw UnimplementedError('setDeviceAttributes() has not been implemented.'); } - void setProfileAttributes({required Map attributes}) { + void setProfileAttributes({required Map traits}) { throw UnimplementedError( 'setProfileAttributes() has not been implemented.'); } diff --git a/test/customer_io_method_channel_test.dart b/test/customer_io_method_channel_test.dart index 1a91ac8..2552c46 100644 --- a/test/customer_io_method_channel_test.dart +++ b/test/customer_io_method_channel_test.dart @@ -62,14 +62,14 @@ void main() { test('identify() should call platform method with correct arguments', () async { final Map args = { - 'identifier': 'Customer 1', - 'attributes': {'email': 'customer@email.com'} + 'userId': 'Customer 1', + 'traits': {'email': 'customer@email.com'} }; final customerIO = CustomerIOMethodChannel(); customerIO.identify( - identifier: args['identifier'] as String, - attributes: args['attributes']); + userId: args['userId'] as String, + traits: args['traits']); expectMethodInvocationArguments('identify', args); }); @@ -140,11 +140,11 @@ void main() { 'setProfileAttributes() should call platform method with correct arguments', () async { final Map args = { - 'attributes': {'age': 1} + 'traits': {'age': 1} }; final customerIO = CustomerIOMethodChannel(); - customerIO.setProfileAttributes(attributes: args['attributes']); + customerIO.setProfileAttributes(traits: args['traits']); expectMethodInvocationArguments('setProfileAttributes', args); }); diff --git a/test/customer_io_test.dart b/test/customer_io_test.dart index 77b06e8..69476eb 100644 --- a/test/customer_io_test.dart +++ b/test/customer_io_test.dart @@ -97,13 +97,13 @@ void main() { const givenIdentifier = 'user@example.com'; final givenAttributes = {'name': 'John Doe'}; CustomerIO.instance.identify( - identifier: givenIdentifier, - attributes: givenAttributes, + userId: givenIdentifier, + traits: givenAttributes, ); verify(mockPlatform.identify( - identifier: givenIdentifier, - attributes: givenAttributes, + userId: givenIdentifier, + traits: givenAttributes, )).called(1); }); @@ -111,13 +111,13 @@ void main() { const givenIdentifier = 'user@example.com'; final givenAttributes = {'name': 'John Doe'}; CustomerIO.instance.identify( - identifier: givenIdentifier, - attributes: givenAttributes, + userId: givenIdentifier, + traits: givenAttributes, ); expect( verify(mockPlatform.identify( - identifier: captureAnyNamed("identifier"), - attributes: captureAnyNamed("attributes"), + userId: captureAnyNamed("userId"), + traits: captureAnyNamed("traits"), )).captured, [givenIdentifier, givenAttributes], ); @@ -172,7 +172,7 @@ void main() { CustomerIO.instance.setProfileAttributes(attributes: givenAttributes); expect( verify(mockPlatform.setProfileAttributes( - attributes: captureAnyNamed("attributes"), + traits: captureAnyNamed("traits"), )).captured.first, givenAttributes, ); diff --git a/test/customer_io_test.mocks.dart b/test/customer_io_test.mocks.dart index b745f01..f306ef0 100644 --- a/test/customer_io_test.mocks.dart +++ b/test/customer_io_test.mocks.dart @@ -56,16 +56,16 @@ class MockTestCustomerIoPlatform extends _i1.Mock ) as _i2.Future); @override void identify({ - required String? identifier, - Map? attributes = const {}, + required String? userId, + Map? traits = const {}, }) => super.noSuchMethod( Invocation.method( #identify, [], { - #identifier: identifier, - #attributes: attributes, + #userId: userId, + #traits: traits, }, ), returnValueForMissingStub: null, @@ -149,12 +149,12 @@ class MockTestCustomerIoPlatform extends _i1.Mock returnValueForMissingStub: null, ); @override - void setProfileAttributes({required Map? attributes}) => + void setProfileAttributes({required Map? traits}) => super.noSuchMethod( Invocation.method( #setProfileAttributes, [], - {#attributes: attributes}, + {#traits: traits}, ), returnValueForMissingStub: null, ); From 478d71e05e22d14f68f15653928fb0ea25ca920d Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Thu, 7 Nov 2024 23:16:48 +0500 Subject: [PATCH 11/34] chore: update sample app settings ui for data pipelines (#162) --- apps/amiapp_flutter/lib/src/app.dart | 2 +- apps/amiapp_flutter/lib/src/customer_io.dart | 12 +- apps/amiapp_flutter/lib/src/data/config.dart | 27 ++- .../lib/src/screens/settings.dart | 166 +++++++++--------- .../lib/src/utils/extensions.dart | 44 +++-- .../lib/src/widgets/settings_form_field.dart | 3 + 6 files changed, 140 insertions(+), 114 deletions(-) diff --git a/apps/amiapp_flutter/lib/src/app.dart b/apps/amiapp_flutter/lib/src/app.dart index 9ca6f17..4744fce 100644 --- a/apps/amiapp_flutter/lib/src/app.dart +++ b/apps/amiapp_flutter/lib/src/app.dart @@ -119,8 +119,8 @@ class _AmiAppState extends State { path: Screen.settings.path, builder: (context, state) => SettingsScreen( auth: _auth, + cdpApiKeyInitialValue: state.uri.queryParameters['cdp_api_key'], siteIdInitialValue: state.uri.queryParameters['site_id'], - apiKeyInitialValue: state.uri.queryParameters['api_key'], ), ), GoRoute( diff --git a/apps/amiapp_flutter/lib/src/customer_io.dart b/apps/amiapp_flutter/lib/src/customer_io.dart index 313854a..fb1b2d5 100644 --- a/apps/amiapp_flutter/lib/src/customer_io.dart +++ b/apps/amiapp_flutter/lib/src/customer_io.dart @@ -55,19 +55,19 @@ class CustomerIOSDK extends ChangeNotifier { } final InAppConfig? inAppConfig; - if (_sdkConfig?.migrationSiteId != null) { - inAppConfig = InAppConfig(siteId: _sdkConfig!.migrationSiteId ?? ''); + final migrationSiteId = _sdkConfig?.migrationSiteId; + if (migrationSiteId != null) { + inAppConfig = InAppConfig(siteId: migrationSiteId); } else { inAppConfig = null; } return CustomerIO.initialize( config: CustomerIOConfig( - cdpApiKey: '${_sdkConfig?.cdnHost}:${_sdkConfig?.cdpApiKey}', - migrationSiteId: _sdkConfig?.migrationSiteId, + cdpApiKey: _sdkConfig?.cdpApiKey ?? 'INVALID', + migrationSiteId: migrationSiteId, region: Region.us, logLevel: logLevel, - autoTrackDeviceAttributes: - _sdkConfig?.autoTrackDeviceAttributes, + autoTrackDeviceAttributes: _sdkConfig?.autoTrackDeviceAttributes, apiHost: _sdkConfig?.apiHost, cdnHost: _sdkConfig?.cdnHost, flushAt: _sdkConfig?.flushAt, diff --git a/apps/amiapp_flutter/lib/src/data/config.dart b/apps/amiapp_flutter/lib/src/data/config.dart index c162d60..d08b74b 100644 --- a/apps/amiapp_flutter/lib/src/data/config.dart +++ b/apps/amiapp_flutter/lib/src/data/config.dart @@ -33,9 +33,8 @@ class CustomerIOSDKConfig { PushConfig? pushConfig, }) : pushConfig = pushConfig ?? PushConfig(); - factory CustomerIOSDKConfig.fromEnv() => - CustomerIOSDKConfig( - cdpApiKey: dotenv.env[_PreferencesKey.cdpApiKey]!, + factory CustomerIOSDKConfig.fromEnv() => CustomerIOSDKConfig( + cdpApiKey: dotenv.env[_PreferencesKey.cdpApiKey] ?? 'INVALID', migrationSiteId: dotenv.env[_PreferencesKey.migrationSiteId], ); @@ -46,19 +45,19 @@ class CustomerIOSDKConfig { throw ArgumentError('cdpApiKey cannot be null'); } + final region = prefs.getString(_PreferencesKey.region) != null + ? Region.values.firstWhere( + (e) => e.name == prefs.getString(_PreferencesKey.region)) + : null; return CustomerIOSDKConfig( cdpApiKey: cdpApiKey, migrationSiteId: prefs.getString(_PreferencesKey.migrationSiteId), - region: prefs.getString(_PreferencesKey.region) != null - ? Region.values.firstWhere( - (e) => e.name == prefs.getString(_PreferencesKey.region)) - : null, - debugModeEnabled: prefs.getBool(_PreferencesKey.debugModeEnabled) != - false, - screenTrackingEnabled: prefs.getBool( - _PreferencesKey.screenTrackingEnabled) != false, + region: region, + debugModeEnabled: prefs.getBool(_PreferencesKey.debugModeEnabled), + screenTrackingEnabled: + prefs.getBool(_PreferencesKey.screenTrackingEnabled), autoTrackDeviceAttributes: - prefs.getBool(_PreferencesKey.autoTrackDeviceAttributes), + prefs.getBool(_PreferencesKey.autoTrackDeviceAttributes), apiHost: prefs.getString(_PreferencesKey.apiHost), cdnHost: prefs.getString(_PreferencesKey.cdnHost), flushAt: prefs.getInt(_PreferencesKey.flushAt), @@ -115,8 +114,8 @@ extension ConfigurationPreferencesExtensions on SharedPreferences { await setOrRemoveBool(_PreferencesKey.autoTrackDeviceAttributes, config.autoTrackDeviceAttributes); result = result && - await setOrRemoveBool( - _PreferencesKey.screenTrackingEnabled, config.screenTrackingEnabled); + await setOrRemoveBool(_PreferencesKey.screenTrackingEnabled, + config.screenTrackingEnabled); result = result && await setOrRemoveString(_PreferencesKey.apiHost, config.apiHost); result = result && diff --git a/apps/amiapp_flutter/lib/src/screens/settings.dart b/apps/amiapp_flutter/lib/src/screens/settings.dart index 30aab80..4da705a 100644 --- a/apps/amiapp_flutter/lib/src/screens/settings.dart +++ b/apps/amiapp_flutter/lib/src/screens/settings.dart @@ -15,13 +15,13 @@ import '../widgets/settings_form_field.dart'; class SettingsScreen extends StatefulWidget { final AmiAppAuth auth; + final String? cdpApiKeyInitialValue; final String? siteIdInitialValue; - final String? apiKeyInitialValue; const SettingsScreen({ required this.auth, + this.cdpApiKeyInitialValue, this.siteIdInitialValue, - this.apiKeyInitialValue, super.key, }); @@ -35,11 +35,12 @@ class _SettingsScreenState extends State { final _formKey = GlobalKey(); late final TextEditingController _deviceTokenValueController; - late final TextEditingController _trackingURLValueController; + late final TextEditingController _cdpApiKeyValueController; late final TextEditingController _siteIDValueController; - late final TextEditingController _apiKeyValueController; - late final TextEditingController _bqSecondsDelayValueController; - late final TextEditingController _bqMinNumberOfTasksValueController; + late final TextEditingController _apiHostValueController; + late final TextEditingController _cdnHostValueController; + late final TextEditingController _flushAtValueController; + late final TextEditingController _flushIntervalValueController; late bool _featureTrackScreens; late bool _featureTrackDeviceAttributes; @@ -52,19 +53,19 @@ class _SettingsScreenState extends State { final cioConfig = widget._customerIOSDK.sdkConfig; _deviceTokenValueController = TextEditingController(); - // _trackingURLValueController = - // TextEditingController(text: cioConfig?.trackingUrl); + _cdpApiKeyValueController = TextEditingController( + text: widget.cdpApiKeyInitialValue ?? cioConfig?.cdpApiKey); _siteIDValueController = TextEditingController( text: widget.siteIdInitialValue ?? cioConfig?.migrationSiteId); - _apiKeyValueController = TextEditingController( - text: widget.apiKeyInitialValue ?? cioConfig?.cdpApiKey); - // _bqSecondsDelayValueController = TextEditingController( - // text: cioConfig?.backgroundQueueSecondsDelay?.toTrimmedString()); - // _bqMinNumberOfTasksValueController = TextEditingController( - // text: cioConfig?.backgroundQueueMinNumOfTasks?.toString()); - // _featureTrackScreens = cioConfig?.screenTrackingEnabled ?? true; - // _featureTrackDeviceAttributes = - // cioConfig?.deviceAttributesTrackingEnabled ?? true; + _apiHostValueController = TextEditingController(text: cioConfig?.apiHost); + _cdnHostValueController = TextEditingController(text: cioConfig?.cdnHost); + _flushAtValueController = + TextEditingController(text: cioConfig?.flushAt?.toString()); + _flushIntervalValueController = TextEditingController( + text: cioConfig?.flushInterval?.toTrimmedString()); + _featureTrackScreens = cioConfig?.screenTrackingEnabled ?? true; + _featureTrackDeviceAttributes = + cioConfig?.autoTrackDeviceAttributes ?? true; _featureDebugMode = cioConfig?.debugModeEnabled ?? true; super.initState(); @@ -76,15 +77,14 @@ class _SettingsScreenState extends State { } final newConfig = CustomerIOSDKConfig( - migrationSiteId: _siteIDValueController.text.trim(), - cdpApiKey: _apiKeyValueController.text.trim(), - // trackingUrl: _trackingURLValueController.text.trim(), - // backgroundQueueSecondsDelay: - // _bqSecondsDelayValueController.text.trim().toDoubleOrNull(), - // backgroundQueueMinNumOfTasks: - // _bqMinNumberOfTasksValueController.text.trim().toIntOrNull(), + cdpApiKey: _cdpApiKeyValueController.text.trim(), + migrationSiteId: _siteIDValueController.text.trim().nullIfEmpty(), + apiHost: _apiHostValueController.text.trim().nullIfEmpty(), + cdnHost: _cdnHostValueController.text.trim().nullIfEmpty(), + flushAt: _flushAtValueController.text.trim().toIntOrNull(), + flushInterval: _flushIntervalValueController.text.trim().toIntOrNull(), screenTrackingEnabled: _featureTrackScreens, - // deviceAttributesTrackingEnabled: _featureTrackDeviceAttributes, + autoTrackDeviceAttributes: _featureTrackDeviceAttributes, debugModeEnabled: _featureDebugMode, ); widget._customerIOSDK.saveConfigToPreferences(newConfig).then((success) { @@ -109,16 +109,16 @@ class _SettingsScreenState extends State { } setState(() { + _cdpApiKeyValueController.text = defaultConfig.cdpApiKey; _siteIDValueController.text = defaultConfig.migrationSiteId ?? ''; - _apiKeyValueController.text = defaultConfig.cdpApiKey; - // _trackingURLValueController.text = defaultConfig.trackingUrl ?? ''; - // _bqSecondsDelayValueController.text = - // defaultConfig.backgroundQueueSecondsDelay?.toTrimmedString() ?? ''; - // _bqMinNumberOfTasksValueController.text = - // defaultConfig.backgroundQueueMinNumOfTasks?.toString() ?? ''; - // _featureTrackScreens = defaultConfig.screenTrackingEnabled; - // _featureTrackDeviceAttributes = - // defaultConfig.deviceAttributesTrackingEnabled; + _apiHostValueController.text = defaultConfig.apiHost ?? ''; + _cdnHostValueController.text = defaultConfig.cdnHost ?? ''; + _flushAtValueController.text = defaultConfig.flushAt?.toString() ?? ''; + _flushIntervalValueController.text = + defaultConfig.flushInterval?.toTrimmedString() ?? ''; + _featureTrackScreens = defaultConfig.screenTrackingEnabled ?? true; + _featureTrackDeviceAttributes = + defaultConfig.autoTrackDeviceAttributes ?? true; _featureDebugMode = defaultConfig.debugModeEnabled ?? true; _saveSettings(context); }); @@ -160,6 +160,7 @@ class _SettingsScreenState extends State { TextSettingsFormField( labelText: 'Device Token', semanticsLabel: 'Device Token Input', + hintText: 'Fetching...', valueController: _deviceTokenValueController, readOnly: true, suffixIcon: IconButton( @@ -178,20 +179,11 @@ class _SettingsScreenState extends State { }, ), ), - const SizedBox(height: 16), - TextSettingsFormField( - labelText: 'CIO Track URL', - semanticsLabel: 'Track URL Input', - valueController: _trackingURLValueController, - validator: (value) => value?.isValidUrl() != false - ? null - : 'Please enter formatted url e.g. https://tracking.cio/', - ), const SizedBox(height: 32), TextSettingsFormField( - labelText: 'Site Id', - semanticsLabel: 'Site ID Input', - valueController: _siteIDValueController, + labelText: 'CDP API Key', + semanticsLabel: 'CDP API Key Input', + valueController: _cdpApiKeyValueController, validator: (value) => value?.trim().isNotEmpty == true ? null @@ -199,32 +191,46 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 16), TextSettingsFormField( - labelText: 'API Key', - semanticsLabel: 'API Key Input', - valueController: _apiKeyValueController, - validator: (value) => - value?.trim().isNotEmpty == true - ? null - : 'This field cannot be blank', + labelText: 'Site Id', + semanticsLabel: 'Site ID Input', + valueController: _siteIDValueController, ), const SizedBox(height: 32), TextSettingsFormField( - labelText: 'backgroundQueueSecondsDelay', - semanticsLabel: 'BQ Seconds Delay Input', - valueController: _bqSecondsDelayValueController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), + labelText: 'API Host', + semanticsLabel: 'API Host Input', + hintText: 'cdp.customer.io/v1', + valueController: _apiHostValueController, + validator: (value) => value?.isEmptyOrValidUrl() != false + ? null + : 'Please enter url e.g. cdp.customer.io/v1 (without https)', + ), + const SizedBox(height: 16), + TextSettingsFormField( + labelText: 'CDN Host', + semanticsLabel: 'CDN Host Input', + hintText: 'cdp.customer.io/v1', + valueController: _cdnHostValueController, + validator: (value) => value?.isEmptyOrValidUrl() != false + ? null + : 'Please enter url e.g. cdp.customer.io/v1 (without https)', + ), + const SizedBox(height: 32), + TextSettingsFormField( + labelText: 'Flush At', + semanticsLabel: 'BQ Min Number of Tasks Input', + hintText: '20', + valueController: _flushAtValueController, + keyboardType: TextInputType.number, validator: (value) { bool isBlank = value?.trim().isNotEmpty != true; - if (isBlank) { - return 'This field cannot be blank'; - } - - double minValue = 1.0; - bool isInvalid = - value?.isValidDouble(min: minValue) != true; - if (isInvalid) { - return 'The value must be greater than or equal to $minValue'; + if (!isBlank) { + int minValue = 1; + bool isInvalid = + value?.isValidInt(min: minValue) != true; + if (isInvalid) { + return 'The value must be greater than or equal to $minValue'; + } } return null; @@ -232,21 +238,21 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 16), TextSettingsFormField( - labelText: 'backgroundQueueMinNumberOfTasks', - semanticsLabel: 'BQ Min Number of Tasks Input', - valueController: _bqMinNumberOfTasksValueController, - keyboardType: TextInputType.number, + labelText: 'Flush Interval', + semanticsLabel: 'BQ Seconds Delay Input', + hintText: '30', + valueController: _flushIntervalValueController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), validator: (value) { bool isBlank = value?.trim().isNotEmpty != true; - if (isBlank) { - return 'This field cannot be blank'; - } - - int minValue = 1; - bool isInvalid = - value?.isValidInt(min: minValue) != true; - if (isInvalid) { - return 'The value must be greater than or equal to $minValue'; + if (!isBlank) { + int minValue = 1; + bool isInvalid = + value?.isValidInt(min: minValue) != true; + if (isInvalid) { + return 'The value must be greater than or equal to $minValue'; + } } return null; diff --git a/apps/amiapp_flutter/lib/src/utils/extensions.dart b/apps/amiapp_flutter/lib/src/utils/extensions.dart index 1e5b085..c7dbcc9 100644 --- a/apps/amiapp_flutter/lib/src/utils/extensions.dart +++ b/apps/amiapp_flutter/lib/src/utils/extensions.dart @@ -32,6 +32,10 @@ extension AmiAppExtensions on BuildContext { extension AmiAppStringExtensions on String { bool equalsIgnoreCase(String? other) => toLowerCase() == other?.toLowerCase(); + String? nullIfEmpty() { + return isEmpty ? null : this; + } + int? toIntOrNull() { if (isNotEmpty) { return int.tryParse(this); @@ -58,23 +62,35 @@ extension AmiAppStringExtensions on String { } } - bool isValidUrl() { + bool isEmptyOrValidUrl() { String url = trim(); - // Empty text is not considered valid. + // Empty text is considered valid if (url.isEmpty) { + return true; + } + // If the URL contains a scheme, it is considered invalid + if (url.contains("://")) { return false; } - - // Currently only Android fails on URLs with empty host, still adding - // validation for all platforms to keep it consistent for app users - final Uri? uri = Uri.tryParse(url); + // Ensure the URL is prefixed with "https://" so that it can be parsed + final prefixedUrl = "https://$url"; + // If the URL is not parsable, it is considered invalid + final Uri? uri = Uri.tryParse(prefixedUrl); if (uri == null) { return false; } - // Valid URL with a host and http/https scheme - return uri.hasAuthority && - (uri.scheme == 'http' || uri.scheme == 'https') && - uri.path.endsWith("/"); + + // Check if the last character is alphanumeric + final isLastCharValid = RegExp(r'[a-zA-Z0-9]$').hasMatch(url); + + // Check validity conditions: + // - URL should not end with a slash + // - URL should contain a domain (e.g., cdp.customer.io) + // - URL should not contain a query or fragment + return isLastCharValid && + uri.host.contains('.') && + uri.query.isEmpty && + uri.fragment.isEmpty; } bool isValidInt({int? min, int? max}) { @@ -92,7 +108,7 @@ extension AmiAppStringExtensions on String { } } -extension AmiAppDoubleExtensions on double { +extension AmiAppIntExtensions on int { String? toTrimmedString() { if (this % 1.0 != 0.0) { return toString(); @@ -109,7 +125,9 @@ extension LocationExtensions on GoRouter { // https://flutter.dev/go/go-router-v9-breaking-changes String currentLocation() { final RouteMatch lastMatch = routerDelegate.currentConfiguration.last; - final RouteMatchList matchList = lastMatch is ImperativeRouteMatch ? lastMatch.matches : routerDelegate.currentConfiguration; + final RouteMatchList matchList = lastMatch is ImperativeRouteMatch + ? lastMatch.matches + : routerDelegate.currentConfiguration; return matchList.uri.toString(); } -} \ No newline at end of file +} diff --git a/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart b/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart index 6374911..79d068d 100644 --- a/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart +++ b/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart @@ -62,6 +62,9 @@ class TextSettingsFormField extends StatelessWidget { semanticsLabel: semanticsLabel, ), hintText: hintText, + hintStyle: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), isDense: true, floatingLabelBehavior: floatingLabelBehavior, ), From d7f3074bca5356c09090b94eb07f2b441d59bd99 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Fri, 8 Nov 2024 11:25:19 +0500 Subject: [PATCH 12/34] chore: update event tracking parameters (#164) --- .../customer/customer_io/CustomerIoPlugin.kt | 14 +++++------ .../io/customer/customer_io/constant/Keys.kt | 4 ++++ apps/amiapp_flutter/lib/main.dart | 2 +- .../lib/src/screens/dashboard.dart | 10 ++++---- .../lib/src/screens/events.dart | 2 +- .../lib/src/screens/settings.dart | 3 +-- ios/Classes/Keys.swift | 4 ++++ ios/Classes/SwiftCustomerIoPlugin.swift | 24 ++++++++----------- lib/customer_io.dart | 6 ++--- lib/customer_io_const.dart | 5 ++++ lib/customer_io_method_channel.dart | 6 ++--- lib/customer_io_platform_interface.dart | 2 +- test/customer_io_method_channel_test.dart | 6 ++--- test/customer_io_test.dart | 8 +++---- test/customer_io_test.mocks.dart | 4 ++-- 15 files changed, 53 insertions(+), 47 deletions(-) diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 4e1fb75..8dbc1cc 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -170,18 +170,16 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun track(params: Map) { - // TODO: Fix track implementation - /* - val name = params.getString(Keys.Tracking.EVENT_NAME) - val attributes = - params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: emptyMap() + val name = requireNotNull(params.getAsTypeOrNull(Keys.Tracking.NAME)) { + "Event name is required to track event" + } + val properties = params.getAsTypeOrNull>(Keys.Tracking.PROPERTIES) - if (attributes.isEmpty()) { + if (properties.isNullOrEmpty()) { CustomerIO.instance().track(name) } else { - CustomerIO.instance().track(name, attributes) + CustomerIO.instance().track(name, properties) } - */ } private fun registerDeviceToken(params: Map) { diff --git a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt index 81670dc..c2cab20 100644 --- a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt +++ b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt @@ -1,5 +1,6 @@ package io.customer.customer_io.constant +// TODO: Cleanup this file later when all commented methods are implemented internal object Keys { object Methods { @@ -24,5 +25,8 @@ internal object Keys { const val DELIVERY_ID = "deliveryId" const val DELIVERY_TOKEN = "deliveryToken" const val METRIC_EVENT = "metricEvent" + + const val NAME = "name" + const val PROPERTIES = "properties" } } diff --git a/apps/amiapp_flutter/lib/main.dart b/apps/amiapp_flutter/lib/main.dart index afcda9a..860e2ac 100644 --- a/apps/amiapp_flutter/lib/main.dart +++ b/apps/amiapp_flutter/lib/main.dart @@ -41,7 +41,7 @@ void main() async { onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async { // Callback from `flutter_local_notifications` plugin for when a local notification is clicked. // Unfortunately, we are only able to get the payload object for the local push, not anything else such as title or body. - CustomerIO.instance.track(name: "local push notification clicked", attributes: {"payload": notificationResponse.payload}); + CustomerIO.instance.track(name: "local push notification clicked", properties: {"payload": notificationResponse.payload}); } ); diff --git a/apps/amiapp_flutter/lib/src/screens/dashboard.dart b/apps/amiapp_flutter/lib/src/screens/dashboard.dart index 1dd34a2..5b4308f 100644 --- a/apps/amiapp_flutter/lib/src/screens/dashboard.dart +++ b/apps/amiapp_flutter/lib/src/screens/dashboard.dart @@ -59,12 +59,12 @@ class _DashboardScreenState extends State { // Setup 3rd party SDK, flutter-fire. // We install this SDK into sample app to make sure the CIO SDK behaves as expected when there is another SDK installed that handles push notifications. FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { - CustomerIO.instance.track(name: "push clicked", attributes: {"push": initialMessage?.notification?.title, "app-state": "killed"}); + CustomerIO.instance.track(name: "push clicked", properties: {"push": initialMessage?.notification?.title, "app-state": "killed"}); }); // ...while app was in the background (but not killed). FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { - CustomerIO.instance.track(name: "push clicked", attributes: {"push": message.notification?.title, "app-state": "background"}); + CustomerIO.instance.track(name: "push clicked", properties: {"push": message.notification?.title, "app-state": "background"}); }); // Important that a 3rd party SDK can receive callbacks when a push is received while app in background. @@ -72,7 +72,7 @@ class _DashboardScreenState extends State { // Note: A push will not be shown on the device while app is in foreground. This is a FCM behavior, not a CIO SDK behavior. // If you send a push using Customer.io with the FCM service setup in Customer.io, the push will be shown on the device. FirebaseMessaging.onMessage.listen((RemoteMessage message) { - CustomerIO.instance.track(name: "push received", attributes: {"push": message.notification?.title, "app-state": "foreground"}); + CustomerIO.instance.track(name: "push received", properties: {"push": message.notification?.title, "app-state": "foreground"}); }); super.initState(); @@ -113,7 +113,7 @@ class _DashboardScreenState extends State { CustomerIO.instance.track( name: 'In-App Event', - attributes: attributes, + properties: attributes, ); } @@ -176,7 +176,7 @@ class _ActionList extends StatelessWidget { if (attributes == null) { CustomerIO.instance.track(name: eventName); } else { - CustomerIO.instance.track(name: eventName, attributes: attributes); + CustomerIO.instance.track(name: eventName, properties: attributes); } context.showSnackBar('Event sent successfully'); } diff --git a/apps/amiapp_flutter/lib/src/screens/events.dart b/apps/amiapp_flutter/lib/src/screens/events.dart index cb79ec5..1f5551b 100644 --- a/apps/amiapp_flutter/lib/src/screens/events.dart +++ b/apps/amiapp_flutter/lib/src/screens/events.dart @@ -112,7 +112,7 @@ class _CustomEventScreenState extends State { : {propertyName: _propertyValueController.text}; CustomerIO.instance.track( name: _eventNameController.text, - attributes: attributes); + properties: attributes); _onEventTracked(); } }, diff --git a/apps/amiapp_flutter/lib/src/screens/settings.dart b/apps/amiapp_flutter/lib/src/screens/settings.dart index 4da705a..36887eb 100644 --- a/apps/amiapp_flutter/lib/src/screens/settings.dart +++ b/apps/amiapp_flutter/lib/src/screens/settings.dart @@ -242,8 +242,7 @@ class _SettingsScreenState extends State { semanticsLabel: 'BQ Seconds Delay Input', hintText: '30', valueController: _flushIntervalValueController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), + keyboardType: TextInputType.number, validator: (value) { bool isBlank = value?.trim().isNotEmpty != true; if (!isBlank) { diff --git a/ios/Classes/Keys.swift b/ios/Classes/Keys.swift index bc15524..de7738b 100644 --- a/ios/Classes/Keys.swift +++ b/ios/Classes/Keys.swift @@ -1,5 +1,6 @@ import Foundation +// TODO: Cleanup this file later when all commented methods are implemented struct Keys { struct Methods{ @@ -23,6 +24,9 @@ struct Keys { static let deliveryId = "deliveryId" static let deliveryToken = "deliveryToken" static let metricEvent = "metricEvent" + + static let name = "name" + static let properties = "properties" } struct Environment{ diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index bc5e665..c86bab5 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -92,21 +92,17 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } private func track(params : Dictionary) { - // TODO: Fix track implementation - /* - guard let name = params[Keys.Tracking.eventName] as? String - else { - return - } - - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else{ - CustomerIO.shared.track(name: name) - return - } - - CustomerIO.shared.track(name: name, data: attributes) - */ + guard let name = params[Keys.Tracking.name] as? String else { + logger.error("Missing event name in: \(params) for key: \(Keys.Tracking.name)") + return + } + + guard let properties = params[Keys.Tracking.properties] as? Dictionary else { + CustomerIO.shared.track(name: name) + return + } + CustomerIO.shared.track(name: name, properties: properties) } func screen(params : Dictionary) { diff --git a/lib/customer_io.dart b/lib/customer_io.dart index 142bcd8..76cb517 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -105,10 +105,10 @@ class CustomerIO { /// You may also track events with additional yet optional data. /// /// @param name event name to be tracked - /// @param attributes (Optional) params to be sent with event + /// @param properties (Optional) params to be sent with event void track( - {required String name, Map attributes = const {}}) { - return _platform.track(name: name, attributes: attributes); + {required String name, Map properties = const {}}) { + return _platform.track(name: name, properties: properties); } /// Track a push metric diff --git a/lib/customer_io_const.dart b/lib/customer_io_const.dart index c6ca7a1..8478dc5 100644 --- a/lib/customer_io_const.dart +++ b/lib/customer_io_const.dart @@ -1,3 +1,5 @@ +// TODO: Cleanup this file later when all commented methods are implemented + class MethodConsts { static const String initialize = "initialize"; static const String identify = "identify"; @@ -23,4 +25,7 @@ class TrackingConsts { static const String metricEvent = "metricEvent"; static const String message = "message"; static const String handleNotificationTrigger = "handleNotificationTrigger"; + + static const String name = "name"; + static const String properties = "properties"; } diff --git a/lib/customer_io_method_channel.dart b/lib/customer_io_method_channel.dart index 9510b99..4089c00 100644 --- a/lib/customer_io_method_channel.dart +++ b/lib/customer_io_method_channel.dart @@ -95,11 +95,11 @@ class CustomerIOMethodChannel extends CustomerIOPlatform { @override void track( {required String name, - Map attributes = const {}}) async { + Map properties = const {}}) async { try { final payload = { - TrackingConsts.eventName: name, - TrackingConsts.attributes: attributes + TrackingConsts.name: name, + TrackingConsts.properties: properties }; methodChannel.invokeMethod(MethodConsts.track, payload); } on PlatformException catch (exception) { diff --git a/lib/customer_io_platform_interface.dart b/lib/customer_io_platform_interface.dart index f85f0d5..1e44480 100644 --- a/lib/customer_io_platform_interface.dart +++ b/lib/customer_io_platform_interface.dart @@ -45,7 +45,7 @@ abstract class CustomerIOPlatform extends PlatformInterface { } void track( - {required String name, Map attributes = const {}}) { + {required String name, Map properties = const {}}) { throw UnimplementedError('track() has not been implemented.'); } diff --git a/test/customer_io_method_channel_test.dart b/test/customer_io_method_channel_test.dart index 2552c46..cd92143 100644 --- a/test/customer_io_method_channel_test.dart +++ b/test/customer_io_method_channel_test.dart @@ -76,12 +76,12 @@ void main() { test('track() should call platform method with correct arguments', () async { final Map args = { - 'eventName': 'test_event', - 'attributes': {'eventData': 2} + 'name': 'test_event', + 'properties': {'eventData': 2} }; final customerIO = CustomerIOMethodChannel(); - customerIO.track(name: args['eventName'], attributes: args['attributes']); + customerIO.track(name: args['name'], properties: args['properties']); expectMethodInvocationArguments('track', args); }); diff --git a/test/customer_io_test.dart b/test/customer_io_test.dart index 69476eb..1d7b2ab 100644 --- a/test/customer_io_test.dart +++ b/test/customer_io_test.dart @@ -131,19 +131,19 @@ void main() { test('track() calls platform', () { const name = 'itemAddedToCart'; final attributes = {'item': 'shoes'}; - CustomerIO.instance.track(name: name, attributes: attributes); - verify(mockPlatform.track(name: name, attributes: attributes)) + CustomerIO.instance.track(name: name, properties: attributes); + verify(mockPlatform.track(name: name, properties: attributes)) .called(1); }); test('track() correct arguments are passed', () { const name = 'itemAddedToCart'; final givenAttributes = {'name': 'John Doe'}; - CustomerIO.instance.track(name: name, attributes: givenAttributes); + CustomerIO.instance.track(name: name, properties: givenAttributes); expect( verify(mockPlatform.track( name: captureAnyNamed("name"), - attributes: captureAnyNamed("attributes"), + properties: captureAnyNamed("properties"), )).captured, [name, givenAttributes], ); diff --git a/test/customer_io_test.mocks.dart b/test/customer_io_test.mocks.dart index f306ef0..768a9cd 100644 --- a/test/customer_io_test.mocks.dart +++ b/test/customer_io_test.mocks.dart @@ -81,7 +81,7 @@ class MockTestCustomerIoPlatform extends _i1.Mock @override void track({ required String? name, - Map? attributes = const {}, + Map? properties = const {}, }) => super.noSuchMethod( Invocation.method( @@ -89,7 +89,7 @@ class MockTestCustomerIoPlatform extends _i1.Mock [], { #name: name, - #attributes: attributes, + #properties: properties, }, ), returnValueForMissingStub: null, From 76ea710746de63dd3fec556d8e93bc3f7b871ec8 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Mon, 11 Nov 2024 14:20:43 +0500 Subject: [PATCH 13/34] chore: update screen tracking parameters (#167) --- .../customer/customer_io/CustomerIoPlugin.kt | 19 +++++++------- .../io/customer/customer_io/constant/Keys.kt | 1 + apps/amiapp_flutter/lib/src/app.dart | 2 +- apps/amiapp_flutter/lib/src/data/config.dart | 13 +++++----- .../lib/src/screens/settings.dart | 10 ++++---- .../lib/src/utils/extensions.dart | 9 ------- ios/Classes/Keys.swift | 1 + ios/Classes/SwiftCustomerIoPlugin.swift | 25 ++++++++----------- lib/customer_io.dart | 18 +++++++------ lib/customer_io_const.dart | 1 + lib/customer_io_method_channel.dart | 8 +++--- lib/customer_io_platform_interface.dart | 2 +- lib/extensions/map_extensions.dart | 7 ++++++ test/customer_io_method_channel_test.dart | 6 ++--- test/customer_io_test.dart | 13 ++++++++++ test/customer_io_test.mocks.dart | 8 +++--- 16 files changed, 79 insertions(+), 64 deletions(-) create mode 100644 lib/extensions/map_extensions.dart diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 8dbc1cc..63a59aa 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -22,6 +22,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result import java.lang.ref.WeakReference /** @@ -171,7 +172,7 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private fun track(params: Map) { val name = requireNotNull(params.getAsTypeOrNull(Keys.Tracking.NAME)) { - "Event name is required to track event" + "Event name is missing in params: $params" } val properties = params.getAsTypeOrNull>(Keys.Tracking.PROPERTIES) @@ -225,18 +226,16 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun screen(params: Map) { - // TODO: Fix screen implementation - /* - val name = params.getString(Keys.Tracking.EVENT_NAME) - val attributes = - params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: emptyMap() + val title = requireNotNull(params.getAsTypeOrNull(Keys.Tracking.TITLE)) { + "Screen title is missing in params: $params" + } + val properties = params.getAsTypeOrNull>(Keys.Tracking.PROPERTIES) - if (attributes.isEmpty()) { - CustomerIO.instance().screen(name) + if (properties.isNullOrEmpty()) { + CustomerIO.instance().screen(title) } else { - CustomerIO.instance().screen(name, attributes) + CustomerIO.instance().screen(title, properties) } - */ } private fun initialize(args: Map): kotlin.Result = runCatching { diff --git a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt index c2cab20..68d7773 100644 --- a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt +++ b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt @@ -28,5 +28,6 @@ internal object Keys { const val NAME = "name" const val PROPERTIES = "properties" + const val TITLE = "title" } } diff --git a/apps/amiapp_flutter/lib/src/app.dart b/apps/amiapp_flutter/lib/src/app.dart index 4744fce..5ac08e5 100644 --- a/apps/amiapp_flutter/lib/src/app.dart +++ b/apps/amiapp_flutter/lib/src/app.dart @@ -216,7 +216,7 @@ class _AmiAppState extends State { if (_customerIOSDK.sdkConfig?.screenTrackingEnabled == true) { final Screen? screen = _router.currentLocation().toAppScreen(); if (screen != null) { - CustomerIO.instance.screen(name: screen.name); + CustomerIO.instance.screen(title: screen.name); } } } diff --git a/apps/amiapp_flutter/lib/src/data/config.dart b/apps/amiapp_flutter/lib/src/data/config.dart index d08b74b..7b14cfb 100644 --- a/apps/amiapp_flutter/lib/src/data/config.dart +++ b/apps/amiapp_flutter/lib/src/data/config.dart @@ -8,8 +8,8 @@ class CustomerIOSDKConfig { final String cdpApiKey; final String? migrationSiteId; final Region? region; - final bool? debugModeEnabled; - final bool? screenTrackingEnabled; + final bool debugModeEnabled; + final bool screenTrackingEnabled; final bool? autoTrackDeviceAttributes; final String? apiHost; final String? cdnHost; @@ -22,8 +22,8 @@ class CustomerIOSDKConfig { required this.cdpApiKey, this.migrationSiteId, this.region, - this.debugModeEnabled, - this.screenTrackingEnabled, + this.debugModeEnabled = true, + this.screenTrackingEnabled = true, this.autoTrackDeviceAttributes, this.apiHost, this.cdnHost, @@ -53,9 +53,10 @@ class CustomerIOSDKConfig { cdpApiKey: cdpApiKey, migrationSiteId: prefs.getString(_PreferencesKey.migrationSiteId), region: region, - debugModeEnabled: prefs.getBool(_PreferencesKey.debugModeEnabled), + debugModeEnabled: + prefs.getBool(_PreferencesKey.debugModeEnabled) != false, screenTrackingEnabled: - prefs.getBool(_PreferencesKey.screenTrackingEnabled), + prefs.getBool(_PreferencesKey.screenTrackingEnabled) != false, autoTrackDeviceAttributes: prefs.getBool(_PreferencesKey.autoTrackDeviceAttributes), apiHost: prefs.getString(_PreferencesKey.apiHost), diff --git a/apps/amiapp_flutter/lib/src/screens/settings.dart b/apps/amiapp_flutter/lib/src/screens/settings.dart index 36887eb..92414da 100644 --- a/apps/amiapp_flutter/lib/src/screens/settings.dart +++ b/apps/amiapp_flutter/lib/src/screens/settings.dart @@ -61,8 +61,8 @@ class _SettingsScreenState extends State { _cdnHostValueController = TextEditingController(text: cioConfig?.cdnHost); _flushAtValueController = TextEditingController(text: cioConfig?.flushAt?.toString()); - _flushIntervalValueController = TextEditingController( - text: cioConfig?.flushInterval?.toTrimmedString()); + _flushIntervalValueController = + TextEditingController(text: cioConfig?.flushInterval?.toString()); _featureTrackScreens = cioConfig?.screenTrackingEnabled ?? true; _featureTrackDeviceAttributes = cioConfig?.autoTrackDeviceAttributes ?? true; @@ -115,11 +115,11 @@ class _SettingsScreenState extends State { _cdnHostValueController.text = defaultConfig.cdnHost ?? ''; _flushAtValueController.text = defaultConfig.flushAt?.toString() ?? ''; _flushIntervalValueController.text = - defaultConfig.flushInterval?.toTrimmedString() ?? ''; - _featureTrackScreens = defaultConfig.screenTrackingEnabled ?? true; + defaultConfig.flushInterval?.toString() ?? ''; + _featureTrackScreens = defaultConfig.screenTrackingEnabled; _featureTrackDeviceAttributes = defaultConfig.autoTrackDeviceAttributes ?? true; - _featureDebugMode = defaultConfig.debugModeEnabled ?? true; + _featureDebugMode = defaultConfig.debugModeEnabled; _saveSettings(context); }); } diff --git a/apps/amiapp_flutter/lib/src/utils/extensions.dart b/apps/amiapp_flutter/lib/src/utils/extensions.dart index c7dbcc9..7d573cf 100644 --- a/apps/amiapp_flutter/lib/src/utils/extensions.dart +++ b/apps/amiapp_flutter/lib/src/utils/extensions.dart @@ -108,15 +108,6 @@ extension AmiAppStringExtensions on String { } } -extension AmiAppIntExtensions on int { - String? toTrimmedString() { - if (this % 1.0 != 0.0) { - return toString(); - } - return toStringAsFixed(0); - } -} - extension LocationExtensions on GoRouter { // Get location of current route // This is a workaround to get the current location as location property diff --git a/ios/Classes/Keys.swift b/ios/Classes/Keys.swift index de7738b..087ac63 100644 --- a/ios/Classes/Keys.swift +++ b/ios/Classes/Keys.swift @@ -27,6 +27,7 @@ struct Keys { static let name = "name" static let properties = "properties" + static let title = "title" } struct Environment{ diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index c86bab5..8f85f0a 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -106,20 +106,17 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } func screen(params : Dictionary) { - // TODO: Fix screen implementation - /* - guard let name = params[Keys.Tracking.eventName] as? String - else { - return - } - - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else{ - CustomerIO.shared.screen(name: name) - return - } - - CustomerIO.shared.screen(name: name, data: attributes) - */ + guard let title = params[Keys.Tracking.title] as? String else { + logger.error("Missing screen title in: \(params) for key: \(Keys.Tracking.title)") + return + } + + guard let properties = params[Keys.Tracking.properties] as? Dictionary else { + CustomerIO.shared.screen(title: title) + return + } + + CustomerIO.shared.screen(title: title, properties: properties) } diff --git a/lib/customer_io.dart b/lib/customer_io.dart index 76cb517..f5ea1ec 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -7,6 +7,7 @@ import 'customer_io_config.dart'; import 'customer_io_enums.dart'; import 'customer_io_inapp.dart'; import 'customer_io_platform_interface.dart'; +import 'extensions/map_extensions.dart'; import 'messaging_in_app/platform_interface.dart'; import 'messaging_push/platform_interface.dart'; @@ -88,9 +89,9 @@ class CustomerIO { /// @param userId unique identifier for a profile /// @param traits (Optional) params to set profile attributes void identify( - {required String userId, - Map traits = const {}}) { - return _platform.identify(userId: userId, traits: traits); + {required String userId, Map traits = const {}}) { + return _platform.identify( + userId: userId, traits: traits.excludeNullValues()); } /// Call this function to stop identifying a person. @@ -108,7 +109,8 @@ class CustomerIO { /// @param properties (Optional) params to be sent with event void track( {required String name, Map properties = const {}}) { - return _platform.track(name: name, properties: properties); + return _platform.track( + name: name, properties: properties.excludeNullValues()); } /// Track a push metric @@ -131,8 +133,9 @@ class CustomerIO { /// @param name name of the screen user visited /// @param attributes (Optional) params to be sent with event void screen( - {required String name, Map attributes = const {}}) { - return _platform.screen(name: name, attributes: attributes); + {required String title, Map properties = const {}}) { + return _platform.screen( + title: title, properties: properties.excludeNullValues()); } /// Use this function to send custom device attributes @@ -148,7 +151,8 @@ class CustomerIO { /// /// @param attributes additional attributes for a user profile void setProfileAttributes({required Map attributes}) { - return _platform.setProfileAttributes(traits: attributes); + return _platform.setProfileAttributes( + traits: attributes.excludeNullValues()); } /// Subscribes to an in-app event listener. diff --git a/lib/customer_io_const.dart b/lib/customer_io_const.dart index 8478dc5..e0d0d2f 100644 --- a/lib/customer_io_const.dart +++ b/lib/customer_io_const.dart @@ -28,4 +28,5 @@ class TrackingConsts { static const String name = "name"; static const String properties = "properties"; + static const String title = "title"; } diff --git a/lib/customer_io_method_channel.dart b/lib/customer_io_method_channel.dart index 4089c00..490dac2 100644 --- a/lib/customer_io_method_channel.dart +++ b/lib/customer_io_method_channel.dart @@ -128,12 +128,12 @@ class CustomerIOMethodChannel extends CustomerIOPlatform { /// Track screen events to record the screens a user visits @override void screen( - {required String name, - Map attributes = const {}}) async { + {required String title, + Map properties = const {}}) async { try { final payload = { - TrackingConsts.eventName: name, - TrackingConsts.attributes: attributes + TrackingConsts.title: title, + TrackingConsts.properties: properties }; methodChannel.invokeMethod(MethodConsts.screen, payload); } on PlatformException catch (exception) { diff --git a/lib/customer_io_platform_interface.dart b/lib/customer_io_platform_interface.dart index 1e44480..2109468 100644 --- a/lib/customer_io_platform_interface.dart +++ b/lib/customer_io_platform_interface.dart @@ -61,7 +61,7 @@ abstract class CustomerIOPlatform extends PlatformInterface { } void screen( - {required String name, Map attributes = const {}}) { + {required String title, Map properties = const {}}) { throw UnimplementedError('screen() has not been implemented.'); } diff --git a/lib/extensions/map_extensions.dart b/lib/extensions/map_extensions.dart new file mode 100644 index 0000000..a8baaef --- /dev/null +++ b/lib/extensions/map_extensions.dart @@ -0,0 +1,7 @@ +/// Extensions for [Map] class that provide additional functionality and convenience methods. +extension CustomerIOMapExtension on Map { + /// Returns a new map with entries that have non-null values, excluding null values. + Map excludeNullValues() { + return Map.fromEntries(entries.where((entry) => entry.value != null)); + } +} diff --git a/test/customer_io_method_channel_test.dart b/test/customer_io_method_channel_test.dart index cd92143..315318c 100644 --- a/test/customer_io_method_channel_test.dart +++ b/test/customer_io_method_channel_test.dart @@ -105,12 +105,12 @@ void main() { test('screen() should call platform method with correct arguments', () async { final Map args = { - 'eventName': 'screen_event', - 'attributes': {'screenName': '你好'} + 'title': 'screen_event', + 'properties': {'screenName': '你好'} }; final customerIO = CustomerIOMethodChannel(); - customerIO.screen(name: args['eventName'], attributes: args['attributes']); + customerIO.screen(title: args['title'], properties: args['properties']); expectMethodInvocationArguments('screen', args); }); diff --git a/test/customer_io_test.dart b/test/customer_io_test.dart index 1d7b2ab..c76d2e7 100644 --- a/test/customer_io_test.dart +++ b/test/customer_io_test.dart @@ -149,6 +149,19 @@ void main() { ); }); + test('screen() correct arguments are passed', () { + const title = 'checkout'; + final givenProperties = {'source': 'push'}; + CustomerIO.instance.screen(title: title, properties: givenProperties); + expect( + verify(mockPlatform.screen( + title: captureAnyNamed("title"), + properties: captureAnyNamed("properties"), + )).captured, + [title, givenProperties], + ); + }); + test('trackMetric() calls platform', () { const deliveryID = '123'; const deviceToken = 'abc'; diff --git a/test/customer_io_test.mocks.dart b/test/customer_io_test.mocks.dart index 768a9cd..c49c303 100644 --- a/test/customer_io_test.mocks.dart +++ b/test/customer_io_test.mocks.dart @@ -124,16 +124,16 @@ class MockTestCustomerIoPlatform extends _i1.Mock ); @override void screen({ - required String? name, - Map? attributes = const {}, + required String? title, + Map? properties = const {}, }) => super.noSuchMethod( Invocation.method( #screen, [], { - #name: name, - #attributes: attributes, + #title: title, + #properties: properties, }, ), returnValueForMissingStub: null, From 4832649570cfa637b3dc84c77e97435caa0d06c3 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Tue, 12 Nov 2024 12:33:31 +0500 Subject: [PATCH 14/34] chore: add android user agent client source (#168) --- android/src/main/AndroidManifest.xml | 16 +++++++++++++++- .../src/main/res/values/customer_io_config.xml | 13 +++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 android/src/main/res/values/customer_io_config.xml diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index cc947c5..9b3b143 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1 +1,15 @@ - + + + + + + + + diff --git a/android/src/main/res/values/customer_io_config.xml b/android/src/main/res/values/customer_io_config.xml new file mode 100644 index 0000000..20b8417 --- /dev/null +++ b/android/src/main/res/values/customer_io_config.xml @@ -0,0 +1,13 @@ + + + + Flutter + + 1.5.2 + From a050e25dd66ca706a87abe8b20832d847dd15ce1 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Tue, 12 Nov 2024 12:40:03 +0500 Subject: [PATCH 15/34] chore: configure ios user agent client source (#169) --- .../Bridge/SdkClientConfiguration.swift | 29 +++++++++++++++++++ ios/Classes/SwiftCustomerIoPlugin.swift | 3 ++ 2 files changed, 32 insertions(+) create mode 100644 ios/Classes/Bridge/SdkClientConfiguration.swift diff --git a/ios/Classes/Bridge/SdkClientConfiguration.swift b/ios/Classes/Bridge/SdkClientConfiguration.swift new file mode 100644 index 0000000..a0d21cd --- /dev/null +++ b/ios/Classes/Bridge/SdkClientConfiguration.swift @@ -0,0 +1,29 @@ +import CioInternalCommon + +/// Extension on `SdkClient` to provide configuration functionality. +/// +/// **Note**: Due to Swift limitations with static methods in protocol extensions, static functions +/// in this extension should be called using `CustomerIOSdkClient.` to ensure correct behavior. +extension SdkClient { + + /// Configures and overrides the shared `SdkClient` instance with provided parameters. + /// + /// - Parameters: + /// - using: Dictionary containing values required for `SdkClient` protocol. + /// - Returns: Configured `SdkClient` instance. Returns the existing shared client if required parameters are missing. + @available(iOSApplicationExtension, unavailable) + @discardableResult + static func configure(using args: [String: Any?]) -> SdkClient { + guard let source = args["source"] as? String, + let version = args["version"] as? String + else { + DIGraphShared.shared.logger.error("Missing required parameters for SdkClient configuration in args: \(args)") + return DIGraphShared.shared.sdkClient + } + + let client = CustomerIOSdkClient(source: source, sdkVersion: version) + DIGraphShared.shared.override(value: client, forType: SdkClient.self) + + return DIGraphShared.shared.sdkClient + } +} diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index 8f85f0a..78ae4c0 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -168,6 +168,9 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private func initialize(params : Dictionary){ do { + // Configure and override SdkClient for Flutter before initializing native SDK + CustomerIOSdkClient.configure(using: params) + // Initialize native SDK with provided config let sdkConfigBuilder = try SDKConfigBuilder.create(from: params) CustomerIO.initialize(withConfig: sdkConfigBuilder.build()) From c1ec3decf737a7e58bb7bcae9a2ba42f87cf503d Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Wed, 13 Nov 2024 00:15:59 +0500 Subject: [PATCH 16/34] chore: update script for sdk versioning (#170) --- .releaserc.json | 3 ++- scripts/update-plugin.sh | 20 -------------------- scripts/update-version.sh | 38 ++++++++++++++++++++++++++++++++------ 3 files changed, 34 insertions(+), 27 deletions(-) delete mode 100755 scripts/update-plugin.sh diff --git a/.releaserc.json b/.releaserc.json index 9d9a93a..aa684c1 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -42,7 +42,8 @@ "assets": [ "CHANGELOG.md", "pubspec.yaml", - "lib/customer_io_plugin_version.dart" + "lib/customer_io_plugin_version.dart", + "android/src/main/res/values/customer_io_config.xml" ], "message": "chore: prepare for ${nextRelease.version}\n\n${nextRelease.notes}" } diff --git a/scripts/update-plugin.sh b/scripts/update-plugin.sh deleted file mode 100755 index 8a153c1..0000000 --- a/scripts/update-plugin.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# Script that updates the pubspec.yaml file in the SDK to newest semantic version. -# -# Designed to be run from CI server or manually. -# -# Use script: ./scripts/update-plugin.sh "0.1.1" - -set -e - -NEW_VERSION="$1" - -echo "Updating files to new version: $NEW_VERSION" - -echo "Updating customer_io_plugin_version.dart" -# Given line: `const version = "1.0.0-alpha.4";` -# Regex string will match the line of the file that we can then substitute. -sd 'const version = "(.*)"' "const version = \"$NEW_VERSION\"" "./lib/customer_io_plugin_version.dart" - -echo "Check file, you should see version inside has been updated!" \ No newline at end of file diff --git a/scripts/update-version.sh b/scripts/update-version.sh index 99c98e5..7a3b2bb 100755 --- a/scripts/update-version.sh +++ b/scripts/update-version.sh @@ -10,12 +10,38 @@ set -e NEW_VERSION="$1" -echo "Updating files to new version: $NEW_VERSION" +echo "Starting version update to: $NEW_VERSION" -echo "Updating pubspec.yaml" -sd 'version: (.*)' "version: $NEW_VERSION" pubspec.yaml +# Helper function to update version in a file and display the diff +update_version_in_file() { + # Parameters: + # $1: file_path: The path to the file to update + # $2: pattern: The regex pattern to match the line to update + # $3: replacement: The new version to replace the matched line with + local file_path=$1 + local pattern=$2 + local replacement=$3 -echo "Check file, you should see version inside has been updated!" + echo -e "\nUpdating version in $file_path..." + sd "$pattern" "$replacement" "$file_path" -echo "Now, updating plugin...." -./scripts/update-plugin.sh "$NEW_VERSION" + echo "Done! Showing changes in $file_path:" + git diff "$file_path" +} + +# Update version in pubspec.yaml +# Given line: `version: 1.3.5` +# Note: We are using ^ to match the start of the line to avoid matching other lines with version in them. +# e.g. `native_sdk_version: 3.5.7` should not be matched by this regex. +update_version_in_file "pubspec.yaml" "^(version: .*)" "version: $NEW_VERSION" + +# Update version in customer_io_plugin_version.dart +# Given line: `const version = "1.3.5";` +update_version_in_file "./lib/customer_io_plugin_version.dart" "const version = \"(.*)\"" "const version = \"$NEW_VERSION\"" + +# Update version in customer_io_config.xml +SDK_CONFIG_CLIENT_VERSION_KEY="customer_io_wrapper_sdk_client_version" +# Given line: `1.3.5` +update_version_in_file "android/src/main/res/values/customer_io_config.xml" ".*" "$NEW_VERSION" + +echo -e "\nVersion update complete for targeted files." From 8197413b29a09258e80720b801e328ef88c1d492 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Thu, 14 Nov 2024 17:18:41 +0500 Subject: [PATCH 17/34] chore: fetch registered device token (#171) --- .../io/customer/customer_io/constant/Keys.kt | 1 + .../messagingpush/CustomerIOPushMessaging.kt | 10 +++++ apps/amiapp_flutter/lib/src/customer_io.dart | 4 -- .../lib/src/screens/settings.dart | 9 ++-- ios/Classes/Keys.swift | 1 + .../CustomerIOMessagingPush.swift | 45 +++++++++++++++++++ ios/Classes/SwiftCustomerIoPlugin.swift | 2 + lib/customer_io_const.dart | 1 + lib/messaging_push/method_channel.dart | 13 ++++++ lib/messaging_push/platform_interface.dart | 7 +++ 10 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 ios/Classes/MessagingPush/CustomerIOMessagingPush.swift diff --git a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt index 68d7773..bc8e1dd 100644 --- a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt +++ b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt @@ -15,6 +15,7 @@ internal object Keys { const val TRACK_METRIC = "trackMetric" const val ON_MESSAGE_RECEIVED = "onMessageReceived" const val DISMISS_MESSAGE = "dismissMessage" + const val GET_REGISTERED_DEVICE_TOKEN = "getRegisteredDeviceToken" } object Tracking { diff --git a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt index 26a0435..cb8cc88 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt @@ -6,6 +6,7 @@ import io.customer.customer_io.constant.Keys import io.customer.customer_io.getAsTypeOrNull import io.customer.customer_io.invokeNative import io.customer.messagingpush.CustomerIOFirebaseMessagingService +import io.customer.sdk.CustomerIO import io.customer.sdk.core.di.SDKComponent import io.customer.sdk.core.util.Logger import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -28,6 +29,11 @@ internal class CustomerIOPushMessaging( override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { + Keys.Methods.GET_REGISTERED_DEVICE_TOKEN -> { + call.invokeNative(result) { + return@invokeNative getRegisteredDeviceToken() + } + } Keys.Methods.ON_MESSAGE_RECEIVED -> { call.invokeNative(result) { args -> return@invokeNative onMessageReceived( @@ -43,6 +49,10 @@ internal class CustomerIOPushMessaging( } } + private fun getRegisteredDeviceToken(): String? { + return CustomerIO.instance().registeredDeviceToken + } + /** * Handles push notification received. This is helpful in processing push notifications * received outside the CIO SDK. diff --git a/apps/amiapp_flutter/lib/src/customer_io.dart b/apps/amiapp_flutter/lib/src/customer_io.dart index fb1b2d5..9c8d398 100644 --- a/apps/amiapp_flutter/lib/src/customer_io.dart +++ b/apps/amiapp_flutter/lib/src/customer_io.dart @@ -127,10 +127,6 @@ extension AmiAppSDKExtensions on CustomerIOSDK { return null; } } - - Future getDeviceToken() async { - return null; - } } /// Customer.io SDK extensions to save/retrieve configurations to/from preferences. diff --git a/apps/amiapp_flutter/lib/src/screens/settings.dart b/apps/amiapp_flutter/lib/src/screens/settings.dart index 92414da..1f0c874 100644 --- a/apps/amiapp_flutter/lib/src/screens/settings.dart +++ b/apps/amiapp_flutter/lib/src/screens/settings.dart @@ -1,3 +1,4 @@ +import 'package:customer_io/customer_io.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; @@ -48,7 +49,7 @@ class _SettingsScreenState extends State { @override void initState() { - widget._customerIOSDK.getDeviceToken().then((value) => + CustomerIO.instance.pushMessaging.getRegisteredDeviceToken().then((value) => setState(() => _deviceTokenValueController.text = value ?? '')); final cioConfig = widget._customerIOSDK.sdkConfig; @@ -201,7 +202,8 @@ class _SettingsScreenState extends State { semanticsLabel: 'API Host Input', hintText: 'cdp.customer.io/v1', valueController: _apiHostValueController, - validator: (value) => value?.isEmptyOrValidUrl() != false + validator: (value) => value?.isEmptyOrValidUrl() != + false ? null : 'Please enter url e.g. cdp.customer.io/v1 (without https)', ), @@ -211,7 +213,8 @@ class _SettingsScreenState extends State { semanticsLabel: 'CDN Host Input', hintText: 'cdp.customer.io/v1', valueController: _cdnHostValueController, - validator: (value) => value?.isEmptyOrValidUrl() != false + validator: (value) => value?.isEmptyOrValidUrl() != + false ? null : 'Please enter url e.g. cdp.customer.io/v1 (without https)', ), diff --git a/ios/Classes/Keys.swift b/ios/Classes/Keys.swift index 087ac63..fc8117d 100644 --- a/ios/Classes/Keys.swift +++ b/ios/Classes/Keys.swift @@ -14,6 +14,7 @@ struct Keys { static let registerDeviceToken = "registerDeviceToken" static let trackMetric = "trackMetric" static let dismissMessage = "dismissMessage" + static let getRegisteredDeviceToken = "getRegisteredDeviceToken" } struct Tracking { diff --git a/ios/Classes/MessagingPush/CustomerIOMessagingPush.swift b/ios/Classes/MessagingPush/CustomerIOMessagingPush.swift new file mode 100644 index 0000000..0cb6760 --- /dev/null +++ b/ios/Classes/MessagingPush/CustomerIOMessagingPush.swift @@ -0,0 +1,45 @@ +import CioDataPipelines +import Flutter +import Foundation + +public class CustomerIOMessagingPush: NSObject, FlutterPlugin { + private let channelName: String = "customer_io_messaging_push" + + public static func register(with registrar: FlutterPluginRegistrar) { + } + + private var methodChannel: FlutterMethodChannel? + + init(with registrar: FlutterPluginRegistrar) { + super.init() + + methodChannel = FlutterMethodChannel( + name: channelName, binaryMessenger: registrar.messenger()) + guard let methodChannel = methodChannel else { + print("\(channelName) methodChannel is nil") + return + } + + registrar.addMethodCallDelegate(self, channel: methodChannel) + } + + deinit { + detachFromEngine() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // Handle method calls for this method channel + switch call.method { + case Keys.Methods.getRegisteredDeviceToken: + result(CustomerIO.shared.registeredDeviceToken) + + default: + result(FlutterMethodNotImplemented) + } + } + + func detachFromEngine() { + methodChannel?.setMethodCallHandler(nil) + methodChannel = nil + } +} diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index 78ae4c0..a96316d 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -8,6 +8,7 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private var methodChannel: FlutterMethodChannel! private var inAppMessagingChannelHandler: CusomterIOInAppMessaging! + private var messagingPushChannelHandler: CustomerIOMessagingPush! private let logger: CioInternalCommon.Logger = DIGraphShared.shared.logger public static func register(with registrar: FlutterPluginRegistrar) { @@ -16,6 +17,7 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { registrar.addMethodCallDelegate(instance, channel: instance.methodChannel) instance.inAppMessagingChannelHandler = CusomterIOInAppMessaging(with: registrar) + instance.messagingPushChannelHandler = CustomerIOMessagingPush(with: registrar) } deinit { diff --git a/lib/customer_io_const.dart b/lib/customer_io_const.dart index e0d0d2f..d55a30a 100644 --- a/lib/customer_io_const.dart +++ b/lib/customer_io_const.dart @@ -12,6 +12,7 @@ class MethodConsts { static const String registerDeviceToken = "registerDeviceToken"; static const String onMessageReceived = "onMessageReceived"; static const String dismissMessage = "dismissMessage"; + static const String getRegisteredDeviceToken = "getRegisteredDeviceToken"; } class TrackingConsts { diff --git a/lib/messaging_push/method_channel.dart b/lib/messaging_push/method_channel.dart index ce8d5ad..29d6c54 100644 --- a/lib/messaging_push/method_channel.dart +++ b/lib/messaging_push/method_channel.dart @@ -14,6 +14,19 @@ class CustomerIOMessagingPushMethodChannel @visibleForTesting final methodChannel = const MethodChannel('customer_io_messaging_push'); + @override + Future getRegisteredDeviceToken() { + try { + return methodChannel + .invokeMethod(MethodConsts.getRegisteredDeviceToken) + .then((result) => result as String?); + } on PlatformException catch (exception) { + handleException(exception); + return Future.error( + exception.message ?? "Error fetching registered device token"); + } + } + @override Future onMessageReceived(Map message, {bool handleNotificationTrigger = true}) { diff --git a/lib/messaging_push/platform_interface.dart b/lib/messaging_push/platform_interface.dart index d3e1b0d..7c26929 100644 --- a/lib/messaging_push/platform_interface.dart +++ b/lib/messaging_push/platform_interface.dart @@ -24,6 +24,13 @@ abstract class CustomerIOMessagingPushPlatform extends PlatformInterface { _instance = instance; } + /// Method to get the device token registered with the Customer.io SDK. + /// Returns a [Future] that resolves to the device token registered with + /// Customer.io SDK. + Future getRegisteredDeviceToken() { + throw UnimplementedError('getRegisteredDeviceToken() has not been implemented.'); + } + /// Processes push notification received outside the CIO SDK. The method /// displays notification on device and tracks CIO metrics for push /// notification. From 8a06a9b35d16cb263b0e03ab7c4c896d1701173a Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Fri, 15 Nov 2024 20:53:35 +0500 Subject: [PATCH 18/34] chore: register device token (#172) --- .../customer/customer_io/CustomerIoPlugin.kt | 7 +++--- ios/Classes/SwiftCustomerIoPlugin.swift | 22 ++++++++++--------- test/customer_io_test.dart | 6 +++++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 63a59aa..7286a47 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -184,11 +184,10 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun registerDeviceToken(params: Map) { - // TODO: Fix registerDeviceToken implementation - /* - val token = params.getString(Keys.Tracking.TOKEN) + val token = requireNotNull(params.getAsTypeOrNull(Keys.Tracking.TOKEN)) { + "Device token is missing in params: $params" + } CustomerIO.instance().registerDeviceToken(token) - */ } private fun trackMetric(params: Map) { diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index a96316d..eafb5b9 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -135,20 +135,22 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private func setProfileAttributes(params : Dictionary){ guard let attributes = params[Keys.Tracking.traits] as? Dictionary - else { return } + else { + logger.error("Missing attributes in: \(params) for key: \(Keys.Tracking.traits)") + return + } + CustomerIO.shared.profileAttributes = attributes } private func registerDeviceToken(params : Dictionary){ - // TODO: Fix registerDeviceToken implementation - /* - guard let token = params[Keys.Tracking.token] as? String - else { - return - } - - CustomerIO.shared.registerDeviceToken(token) - */ + guard let token = params[Keys.Tracking.token] as? String + else { + logger.error("Missing token in: \(params) for key: \(Keys.Tracking.token)") + return + } + + CustomerIO.shared.registerDeviceToken(token) } private func trackMetric(params : Dictionary){ diff --git a/test/customer_io_test.dart b/test/customer_io_test.dart index c76d2e7..c7f6bbf 100644 --- a/test/customer_io_test.dart +++ b/test/customer_io_test.dart @@ -136,6 +136,12 @@ void main() { .called(1); }); + test('registerDeviceToken() calls platform', () { + const token = 'abc'; + CustomerIO.instance.registerDeviceToken(deviceToken: token); + verify(mockPlatform.registerDeviceToken(deviceToken: token)).called(1); + }); + test('track() correct arguments are passed', () { const name = 'itemAddedToCart'; final givenAttributes = {'name': 'John Doe'}; From 0fadd4d113d675e71fd87bd10efd5163308049cc Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Tue, 19 Nov 2024 20:59:27 +0500 Subject: [PATCH 19/34] chore: added device attributes support (#173) --- .../customer/customer_io/CustomerIoPlugin.kt | 17 ++++++++++++----- .../io/customer/customer_io/constant/Keys.kt | 1 + ios/Classes/Keys.swift | 1 + ios/Classes/SwiftCustomerIoPlugin.swift | 19 +++++++++---------- lib/customer_io.dart | 2 +- lib/customer_io_method_channel.dart | 4 ++-- lib/customer_io_platform_interface.dart | 2 +- test/customer_io_method_channel_test.dart | 4 ++-- test/customer_io_test.dart | 2 +- test/customer_io_test.mocks.dart | 4 ++-- 10 files changed, 32 insertions(+), 24 deletions(-) diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 7286a47..f763163 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -210,16 +210,23 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun setDeviceAttributes(params: Map) { - // TODO: Fix setDeviceAttributes implementation - /* - val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: return + val attributes = params.getAsTypeOrNull>(Keys.Tracking.ATTRIBUTES) + + if (attributes.isNullOrEmpty()) { + logger.error("Device attributes are missing in params: $params") + return + } CustomerIO.instance().deviceAttributes = attributes - */ } private fun setProfileAttributes(params: Map) { - val attributes = params.getAsTypeOrNull>(Keys.Tracking.TRAITS) ?: return + val attributes = params.getAsTypeOrNull>(Keys.Tracking.ATTRIBUTES) + + if (attributes.isNullOrEmpty()) { + logger.error("Profile attributes are missing in params: $params") + return + } CustomerIO.instance().profileAttributes = attributes } diff --git a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt index bc8e1dd..2afabea 100644 --- a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt +++ b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt @@ -21,6 +21,7 @@ internal object Keys { object Tracking { const val USER_ID = "userId" const val TRAITS = "traits" + const val ATTRIBUTES = "attributes" const val EVENT_NAME = "eventName" const val TOKEN = "token" const val DELIVERY_ID = "deliveryId" diff --git a/ios/Classes/Keys.swift b/ios/Classes/Keys.swift index fc8117d..ff2ac32 100644 --- a/ios/Classes/Keys.swift +++ b/ios/Classes/Keys.swift @@ -20,6 +20,7 @@ struct Keys { struct Tracking { static let userId = "userId" static let traits = "traits" + static let attributes = "attributes" static let eventName = "eventName" static let token = "token" static let deliveryId = "deliveryId" diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index eafb5b9..f21eb81 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -123,20 +123,19 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private func setDeviceAttributes(params : Dictionary){ - // TODO: Fix setDeviceAttributes implementation - /* - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary - else { - return - } - CustomerIO.shared.deviceAttributes = attributes - */ + guard let attributes = params[Keys.Tracking.attributes] as? Dictionary + else { + logger.error("Missing device attributes in: \(params) for key: \(Keys.Tracking.attributes)") + return + } + + CustomerIO.shared.deviceAttributes = attributes } private func setProfileAttributes(params : Dictionary){ - guard let attributes = params[Keys.Tracking.traits] as? Dictionary + guard let attributes = params[Keys.Tracking.attributes] as? Dictionary else { - logger.error("Missing attributes in: \(params) for key: \(Keys.Tracking.traits)") + logger.error("Missing profile attributes in: \(params) for key: \(Keys.Tracking.attributes)") return } diff --git a/lib/customer_io.dart b/lib/customer_io.dart index f5ea1ec..c4e444a 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -152,7 +152,7 @@ class CustomerIO { /// @param attributes additional attributes for a user profile void setProfileAttributes({required Map attributes}) { return _platform.setProfileAttributes( - traits: attributes.excludeNullValues()); + attributes: attributes.excludeNullValues()); } /// Subscribes to an in-app event listener. diff --git a/lib/customer_io_method_channel.dart b/lib/customer_io_method_channel.dart index 490dac2..b19dddb 100644 --- a/lib/customer_io_method_channel.dart +++ b/lib/customer_io_method_channel.dart @@ -168,9 +168,9 @@ class CustomerIOMethodChannel extends CustomerIOPlatform { /// Set custom user profile information such as user preference, specific /// user actions etc @override - void setProfileAttributes({required Map traits}) { + void setProfileAttributes({required Map attributes}) { try { - final payload = {TrackingConsts.traits: traits}; + final payload = {TrackingConsts.attributes: attributes}; methodChannel.invokeMethod(MethodConsts.setProfileAttributes, payload); } on PlatformException catch (exception) { handleException(exception); diff --git a/lib/customer_io_platform_interface.dart b/lib/customer_io_platform_interface.dart index 2109468..a7bd11f 100644 --- a/lib/customer_io_platform_interface.dart +++ b/lib/customer_io_platform_interface.dart @@ -69,7 +69,7 @@ abstract class CustomerIOPlatform extends PlatformInterface { throw UnimplementedError('setDeviceAttributes() has not been implemented.'); } - void setProfileAttributes({required Map traits}) { + void setProfileAttributes({required Map attributes}) { throw UnimplementedError( 'setProfileAttributes() has not been implemented.'); } diff --git a/test/customer_io_method_channel_test.dart b/test/customer_io_method_channel_test.dart index 315318c..ca0a520 100644 --- a/test/customer_io_method_channel_test.dart +++ b/test/customer_io_method_channel_test.dart @@ -140,11 +140,11 @@ void main() { 'setProfileAttributes() should call platform method with correct arguments', () async { final Map args = { - 'traits': {'age': 1} + 'attributes': {'age': 1} }; final customerIO = CustomerIOMethodChannel(); - customerIO.setProfileAttributes(traits: args['traits']); + customerIO.setProfileAttributes(attributes: args['attributes']); expectMethodInvocationArguments('setProfileAttributes', args); }); diff --git a/test/customer_io_test.dart b/test/customer_io_test.dart index c7f6bbf..13d7e3f 100644 --- a/test/customer_io_test.dart +++ b/test/customer_io_test.dart @@ -191,7 +191,7 @@ void main() { CustomerIO.instance.setProfileAttributes(attributes: givenAttributes); expect( verify(mockPlatform.setProfileAttributes( - traits: captureAnyNamed("traits"), + attributes: captureAnyNamed("attributes"), )).captured.first, givenAttributes, ); diff --git a/test/customer_io_test.mocks.dart b/test/customer_io_test.mocks.dart index c49c303..dd87c9f 100644 --- a/test/customer_io_test.mocks.dart +++ b/test/customer_io_test.mocks.dart @@ -149,12 +149,12 @@ class MockTestCustomerIoPlatform extends _i1.Mock returnValueForMissingStub: null, ); @override - void setProfileAttributes({required Map? traits}) => + void setProfileAttributes({required Map? attributes}) => super.noSuchMethod( Invocation.method( #setProfileAttributes, [], - {#traits: traits}, + {#attributes: attributes}, ), returnValueForMissingStub: null, ); From ce1140c44d4250abd2c69448f16df44d5d674c1e Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 20 Nov 2024 14:14:00 +0500 Subject: [PATCH 20/34] chore: manual metric tracking (#174) --- .../customer/customer_io/CustomerIoPlugin.kt | 27 ++++++++++-------- ios/Classes/SwiftCustomerIoPlugin.swift | 28 +++++++++---------- lib/customer_io_const.dart | 2 +- lib/customer_io_enums.dart | 2 +- lib/customer_io_method_channel.dart | 2 +- test/customer_io_method_channel_test.dart | 6 ++-- 6 files changed, 35 insertions(+), 32 deletions(-) diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index f763163..7fb8bf3 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -16,6 +16,8 @@ import io.customer.sdk.core.di.SDKComponent import io.customer.sdk.core.util.CioLogLevel import io.customer.sdk.core.util.Logger import io.customer.sdk.data.model.Region +import io.customer.sdk.events.Metric +import io.customer.sdk.events.TrackMetric import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -191,22 +193,23 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun trackMetric(params: Map) { - // TODO: Fix trackMetric implementation - /* - val deliveryId = params.getString(Keys.Tracking.DELIVERY_ID) - val deliveryToken = params.getString(Keys.Tracking.DELIVERY_TOKEN) - val eventName = params.getProperty(Keys.Tracking.METRIC_EVENT) - val event = MetricEvent.getEvent(eventName) - - if (event == null) { - logger.info("metric event type null. Possible issue with SDK? Given: $eventName") - return + val deliveryId = params.getAsTypeOrNull(Keys.Tracking.DELIVERY_ID) + val deliveryToken = params.getAsTypeOrNull(Keys.Tracking.DELIVERY_TOKEN) + val eventName = params.getAsTypeOrNull(Keys.Tracking.METRIC_EVENT) + + if (deliveryId == null || deliveryToken == null || eventName == null) { + throw IllegalArgumentException("Missing required parameters") } + val event = Metric.valueOf(eventName) + CustomerIO.instance().trackMetric( - deliveryID = deliveryId, deviceToken = deliveryToken, event = event + event = TrackMetric.Push( + deliveryId = deliveryId, + deviceToken = deliveryToken, + metric = event + ) ) - */ } private fun setDeviceAttributes(params: Map) { diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index f21eb81..e77660a 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -153,20 +153,20 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { } private func trackMetric(params : Dictionary){ - // TODO: Fix trackMetric implementation - /* - guard let deliveryId = params[Keys.Tracking.deliveryId] as? String, - let deviceToken = params[Keys.Tracking.deliveryToken] as? String, - let metricEvent = params[Keys.Tracking.metricEvent] as? String, - let event = Metric.getEvent(from: metricEvent) - else { - return - } - - CustomerIO.shared.trackMetric(deliveryID: deliveryId, - event: event, - deviceToken: deviceToken) - */ + + guard let deliveryId = params[Keys.Tracking.deliveryId] as? String, + let deviceToken = params[Keys.Tracking.deliveryToken] as? String, + let metricEvent = params[Keys.Tracking.metricEvent] as? String, + let event = Metric.getEvent(from: metricEvent) + else { + logger.error("Missing required parameters in: \(params)") + return + } + + CustomerIO.shared.trackMetric(deliveryID: deliveryId, + event: event, + deviceToken: deviceToken) + } private func initialize(params : Dictionary){ diff --git a/lib/customer_io_const.dart b/lib/customer_io_const.dart index d55a30a..7371cdc 100644 --- a/lib/customer_io_const.dart +++ b/lib/customer_io_const.dart @@ -17,8 +17,8 @@ class MethodConsts { class TrackingConsts { static const String userId = "userId"; - static const String traits = "traits"; static const String attributes = "attributes"; + static const String traits = "traits"; static const String eventName = "eventName"; static const String token = "token"; static const String deliveryId = "deliveryId"; diff --git a/lib/customer_io_enums.dart b/lib/customer_io_enums.dart index 3a62435..fe875d7 100644 --- a/lib/customer_io_enums.dart +++ b/lib/customer_io_enums.dart @@ -8,7 +8,7 @@ enum CioLogLevel { none, error, info, debug } enum Region { us, eu } /// Enum to specify the type of metric for tracking -enum MetricEvent { delivered, opened, converted, clicked } +enum MetricEvent { delivered, opened, converted } /// Enum to specify the click behavior of push notification for Android enum PushClickBehaviorAndroid { diff --git a/lib/customer_io_method_channel.dart b/lib/customer_io_method_channel.dart index b19dddb..463e8ab 100644 --- a/lib/customer_io_method_channel.dart +++ b/lib/customer_io_method_channel.dart @@ -182,7 +182,7 @@ class CustomerIOMethodChannel extends CustomerIOPlatform { @override void setDeviceAttributes({required Map attributes}) { try { - final payload = {TrackingConsts.attributes: attributes}; + final payload = {TrackingConsts.traits: attributes}; methodChannel.invokeMethod(MethodConsts.setDeviceAttributes, payload); } on PlatformException catch (exception) { handleException(exception); diff --git a/test/customer_io_method_channel_test.dart b/test/customer_io_method_channel_test.dart index ca0a520..22ba981 100644 --- a/test/customer_io_method_channel_test.dart +++ b/test/customer_io_method_channel_test.dart @@ -91,7 +91,7 @@ void main() { final Map args = { 'deliveryId': '123', 'deliveryToken': 'asdf', - 'metricEvent': 'clicked' + 'metricEvent': 'opened' }; final customerIO = CustomerIOMethodChannel(); @@ -153,11 +153,11 @@ void main() { 'setDeviceAttributes() should call platform method with correct arguments', () async { final Map args = { - 'attributes': {'os': 'Android'} + 'traits': {'os': 'Android'} }; final customerIO = CustomerIOMethodChannel(); - customerIO.setDeviceAttributes(attributes: args['attributes']); + customerIO.setDeviceAttributes(attributes: args['traits']); expectMethodInvocationArguments('setDeviceAttributes', args); }); From a18bce79de20c1115e080db9458d4473663754d7 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Thu, 21 Nov 2024 00:09:39 +0500 Subject: [PATCH 21/34] chore: configure android push module (#176) --- .../customer/customer_io/CustomerIoPlugin.kt | 34 ++++------------- .../messagingpush/CustomerIOPushMessaging.kt | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 7fb8bf3..8f19e66 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -9,7 +9,6 @@ import io.customer.customer_io.messaginginapp.CustomerIOInAppMessaging import io.customer.customer_io.messagingpush.CustomerIOPushMessaging import io.customer.messaginginapp.type.InAppEventListener import io.customer.messaginginapp.type.InAppMessage -import io.customer.messagingpush.ModuleMessagingPushFCM import io.customer.sdk.CustomerIO import io.customer.sdk.CustomerIOBuilder import io.customer.sdk.core.di.SDKComponent @@ -277,7 +276,14 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { args.getAsTypeOrNull("cdnHost")?.let(::cdnHost) // TODO: Initialize in-app module with given config - // TODO: Initialize push module with given config + + // Configure push messaging module based on config provided by customer app + args.getAsTypeOrNull>(key = "push").let { pushConfig -> + CustomerIOPushMessaging.addNativeModuleFromConfig( + builder = this, + config = pushConfig ?: emptyMap() + ) + } }.build() logger.info("Customer.io instance initialized successfully from app") @@ -285,30 +291,6 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { logger.error("Failed to initialize Customer.io instance from app, ${ex.message}") } - private fun configureModuleMessagingPushFCM(config: Map?): ModuleMessagingPushFCM { - return ModuleMessagingPushFCM( - // TODO: Fix push module configuration - /* - config = MessagingPushModuleConfig.Builder().apply { - config?.getProperty(CustomerIOConfig.Companion.Keys.AUTO_TRACK_PUSH_EVENTS) - ?.let { value -> - setAutoTrackPushEvents(autoTrackPushEvents = value) - } - config?.getProperty(CustomerIOConfig.Companion.Keys.PUSH_CLICK_BEHAVIOR_ANDROID) - ?.takeIfNotBlank() - ?.let { value -> - val behavior = kotlin.runCatching { - enumValueOf(value) - }.getOrNull() - if (behavior != null) { - setPushClickBehavior(pushClickBehavior = behavior) - } - } - }.build(), - */ - ) - } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { flutterCommunicationChannel.setMethodCallHandler(null) diff --git a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt index cb8cc88..0e6c560 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt @@ -6,7 +6,11 @@ import io.customer.customer_io.constant.Keys import io.customer.customer_io.getAsTypeOrNull import io.customer.customer_io.invokeNative import io.customer.messagingpush.CustomerIOFirebaseMessagingService +import io.customer.messagingpush.MessagingPushModuleConfig +import io.customer.messagingpush.ModuleMessagingPushFCM +import io.customer.messagingpush.config.PushClickBehavior import io.customer.sdk.CustomerIO +import io.customer.sdk.CustomerIOBuilder import io.customer.sdk.core.di.SDKComponent import io.customer.sdk.core.util.Logger import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -34,6 +38,7 @@ internal class CustomerIOPushMessaging( return@invokeNative getRegisteredDeviceToken() } } + Keys.Methods.ON_MESSAGE_RECEIVED -> { call.invokeNative(result) { args -> return@invokeNative onMessageReceived( @@ -82,4 +87,37 @@ internal class CustomerIOPushMessaging( throw ex } } + + companion object { + /** + * Adds push messaging module to native Android SDK based on the configuration provided by + * customer app. + * + * @param builder instance of CustomerIOBuilder to add push messaging module. + * @param config configuration provided by customer app for push messaging module. + */ + internal fun addNativeModuleFromConfig( + builder: CustomerIOBuilder, + config: Map + ) { + val androidConfig = + config.getAsTypeOrNull>(key = "android") ?: emptyMap() + // Prefer `android` object for push configurations as it's more specific to Android + // For common push configurations, use `config` object instead of `android` + + // Default push click behavior is to prevent restart of activity in Flutter apps + val pushClickBehavior = androidConfig.getAsTypeOrNull("pushClickBehavior") + ?.takeIf { it.isNotBlank() } + ?.let { value -> + runCatching { enumValueOf(value) }.getOrNull() + } ?: PushClickBehavior.ACTIVITY_PREVENT_RESTART + + val module = ModuleMessagingPushFCM( + moduleConfig = MessagingPushModuleConfig.Builder().apply { + setPushClickBehavior(pushClickBehavior = pushClickBehavior) + }.build(), + ) + builder.addCustomerIOModule(module) + } + } } From 8bbb46dc7eb7c098c87c6979ed695429385920fb Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Thu, 21 Nov 2024 01:12:37 +0500 Subject: [PATCH 22/34] chore: added config and initialized native modules (#177) --- .../customer/customer_io/CustomerIoPlugin.kt | 11 +++++-- .../CustomerIOInAppMessaging.kt | 32 +++++++++++++++++++ apps/amiapp_flutter/lib/src/data/config.dart | 2 ++ .../lib/src/screens/settings.dart | 2 ++ ios/Classes/SwiftCustomerIoPlugin.swift | 4 +++ lib/config/customer_io_config.dart | 2 +- test/customer_io_config_test.dart | 2 +- 7 files changed, 50 insertions(+), 5 deletions(-) diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 8f19e66..1a0f400 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -274,9 +274,14 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { args.getAsTypeOrNull("apiHost")?.let(::apiHost) args.getAsTypeOrNull("cdnHost")?.let(::cdnHost) - - // TODO: Initialize in-app module with given config - + // Configure in-app messaging module based on config provided by customer app + args.getAsTypeOrNull>(key = "inApp")?.let { inAppConfig -> + CustomerIOInAppMessaging.addNativeModuleFromConfig( + builder = this, + config = inAppConfig, + region = givenRegion + ) + } // Configure push messaging module based on config provided by customer app args.getAsTypeOrNull>(key = "push").let { pushConfig -> CustomerIOPushMessaging.addNativeModuleFromConfig( diff --git a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt index 53cb5a4..afb8b3e 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt @@ -2,9 +2,15 @@ package io.customer.customer_io.messaginginapp import io.customer.customer_io.CustomerIOPluginModule import io.customer.customer_io.constant.Keys +import io.customer.customer_io.getAsTypeOrNull import io.customer.customer_io.invokeNative +import io.customer.messaginginapp.MessagingInAppModuleConfig +import io.customer.messaginginapp.ModuleMessagingInApp import io.customer.messaginginapp.di.inAppMessaging import io.customer.sdk.CustomerIO +import io.customer.sdk.CustomerIOBuilder +import io.customer.sdk.core.di.SDKComponent +import io.customer.sdk.data.model.Region import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -34,4 +40,30 @@ internal class CustomerIOInAppMessaging( } } + companion object { + /** + * Adds in-app module to native Android SDK based on the configuration provided by + * customer app. + * + * @param builder instance of CustomerIOBuilder to add push messaging module. + * @param config configuration provided by customer app for in-app messaging module. + * @param region region of the customer app. + */ + internal fun addNativeModuleFromConfig( + builder: CustomerIOBuilder, + config: Map, + region: Region + ) { + val siteId = config.getAsTypeOrNull("siteId") + if (siteId.isNullOrBlank()) { + SDKComponent.logger.error("Site ID is required to initialize InAppMessaging module") + return + } + val module = ModuleMessagingInApp( + MessagingInAppModuleConfig.Builder(siteId = siteId, region = region).build(), + ) + builder.addCustomerIOModule(module) + } + } + } \ No newline at end of file diff --git a/apps/amiapp_flutter/lib/src/data/config.dart b/apps/amiapp_flutter/lib/src/data/config.dart index 7b14cfb..21450b0 100644 --- a/apps/amiapp_flutter/lib/src/data/config.dart +++ b/apps/amiapp_flutter/lib/src/data/config.dart @@ -63,6 +63,8 @@ class CustomerIOSDKConfig { cdnHost: prefs.getString(_PreferencesKey.cdnHost), flushAt: prefs.getInt(_PreferencesKey.flushAt), flushInterval: prefs.getInt(_PreferencesKey.flushInterval), + inAppConfig: InAppConfig( + siteId: prefs.getString(_PreferencesKey.migrationSiteId) ?? ""), ); } diff --git a/apps/amiapp_flutter/lib/src/screens/settings.dart b/apps/amiapp_flutter/lib/src/screens/settings.dart index 1f0c874..a7de9d5 100644 --- a/apps/amiapp_flutter/lib/src/screens/settings.dart +++ b/apps/amiapp_flutter/lib/src/screens/settings.dart @@ -1,3 +1,4 @@ +import 'package:customer_io/config/in_app_config.dart'; import 'package:customer_io/customer_io.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -87,6 +88,7 @@ class _SettingsScreenState extends State { screenTrackingEnabled: _featureTrackScreens, autoTrackDeviceAttributes: _featureTrackDeviceAttributes, debugModeEnabled: _featureDebugMode, + inAppConfig: InAppConfig(siteId: _siteIDValueController.text.trim()) ); widget._customerIOSDK.saveConfigToPreferences(newConfig).then((success) { if (!context.mounted) { diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index e77660a..00dafae 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -177,6 +177,10 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { let sdkConfigBuilder = try SDKConfigBuilder.create(from: params) CustomerIO.initialize(withConfig: sdkConfigBuilder.build()) + if let inAppConfig = try? MessagingInAppConfigBuilder.build(from: params) { + MessagingInApp.initialize(withConfig: inAppConfig) + } + // TODO: Initialize in-app module with given config logger.debug("Customer.io SDK initialized with config: \(params)") } catch { diff --git a/lib/config/customer_io_config.dart b/lib/config/customer_io_config.dart index c65d512..d2b7715 100644 --- a/lib/config/customer_io_config.dart +++ b/lib/config/customer_io_config.dart @@ -47,7 +47,7 @@ class CustomerIOConfig { 'cdnHost': cdnHost, 'flushAt': flushAt, 'flushInterval': flushInterval, - 'inAppConfig': inAppConfig?.toMap(), + 'inApp': inAppConfig?.toMap(), 'pushConfig': pushConfig.toMap(), 'version': version, 'source': source diff --git a/test/customer_io_config_test.dart b/test/customer_io_config_test.dart index acd9009..3a38281 100644 --- a/test/customer_io_config_test.dart +++ b/test/customer_io_config_test.dart @@ -102,7 +102,7 @@ void main() { 'cdnHost': 'https://cdn.example.com', 'flushAt': 25, 'flushInterval': 55, - 'inAppConfig': inAppConfig.toMap(), + 'inApp': inAppConfig.toMap(), 'pushConfig': pushConfig.toMap(), 'version': config.version, 'source': config.source, From d66b25c3a26066abfa1be2af728d5e4ddef16bb3 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Mon, 25 Nov 2024 12:54:58 +0500 Subject: [PATCH 23/34] chore: initialize messaging push module (#178) --- .github/workflows/build-sample-apps.yml | 3 +-- apps/amiapp_flutter/ios/Env.swift.example | 3 +-- .../NotificationService.swift | 12 +++++------- apps/amiapp_flutter/ios/Runner/AppDelegate.swift | 8 ++++---- apps/amiapp_flutter/lib/main.dart | 6 +++++- apps/amiapp_flutter/lib/src/screens/settings.dart | 2 +- lib/customer_io.dart | 10 +++++++++- 7 files changed, 26 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-sample-apps.yml b/.github/workflows/build-sample-apps.yml index 04031b8..5669fe3 100644 --- a/.github/workflows/build-sample-apps.yml +++ b/.github/workflows/build-sample-apps.yml @@ -117,8 +117,7 @@ jobs: - name: Setup workspace credentials in iOS environment files run: | cp "ios/Env.swift.example" "ios/Env.swift" - sd 'siteId: String = ".*"' "siteId: String = \"${{ secrets[format('CUSTOMERIO_{0}_WORKSPACE_SITE_ID', matrix.sample-app)] }}\"" "ios/Env.swift" - sd 'apiKey: String = ".*"' "apiKey: String = \"${{ secrets[format('CUSTOMERIO_{0}_WORKSPACE_API_KEY', matrix.sample-app)] }}\"" "ios/Env.swift" + sd 'cdpApiKey: String = ".*"' "cdpApiKey: String = \"${{ secrets[format('CUSTOMERIO_{0}_WORKSPACE_CDP_API_KEY', matrix.sample-app)] }}\"" "ios/Env.swift" # Make sure to fetch dependencies only after updating the version numbers and workspace credentials diff --git a/apps/amiapp_flutter/ios/Env.swift.example b/apps/amiapp_flutter/ios/Env.swift.example index a18f7b4..e94cf0e 100644 --- a/apps/amiapp_flutter/ios/Env.swift.example +++ b/apps/amiapp_flutter/ios/Env.swift.example @@ -1,6 +1,5 @@ import Foundation class Env { - static let siteId: String = "siteid" - static let apiKey: String = "apikey" + static let cdpApiKey: String = "cdpApiKey" } diff --git a/apps/amiapp_flutter/ios/NotificationServiceExtension/NotificationService.swift b/apps/amiapp_flutter/ios/NotificationServiceExtension/NotificationService.swift index fc7aaca..96274b5 100644 --- a/apps/amiapp_flutter/ios/NotificationServiceExtension/NotificationService.swift +++ b/apps/amiapp_flutter/ios/NotificationServiceExtension/NotificationService.swift @@ -13,13 +13,11 @@ class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { print("NotificationService didReceive called") - // TODO: Fix SDK initialization - /* - CustomerIO.initialize(siteId: Env.siteId, apiKey: Env.apiKey, region: .US) { config in - config.autoTrackDeviceAttributes = true - config.logLevel = .debug - } - */ + MessagingPushFCM.initializeForExtension( + withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) + .logLevel(.debug) + .build() + ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } diff --git a/apps/amiapp_flutter/ios/Runner/AppDelegate.swift b/apps/amiapp_flutter/ios/Runner/AppDelegate.swift index b3f6bfe..0822c94 100644 --- a/apps/amiapp_flutter/ios/Runner/AppDelegate.swift +++ b/apps/amiapp_flutter/ios/Runner/AppDelegate.swift @@ -20,10 +20,10 @@ import FirebaseCore Messaging.messaging().delegate = self - // TODO: Fix MessagingPush initialization - /* - MessagingPushFCM.initialize(configOptions: nil) - */ + MessagingPushFCM.initialize( + withConfig: MessagingPushConfigBuilder() + .build() + ) // Sets a 3rd party push event handler for the app besides the Customer.io SDK and FlutterFire. // Setting the AppDelegate to be the handler will internally use `flutter_local_notifications` to handle the push event. diff --git a/apps/amiapp_flutter/lib/main.dart b/apps/amiapp_flutter/lib/main.dart index 860e2ac..f7cf8d7 100644 --- a/apps/amiapp_flutter/lib/main.dart +++ b/apps/amiapp_flutter/lib/main.dart @@ -31,7 +31,11 @@ void main() async { // Setup flutter_local_notifications plugin to send local notifications and receive callbacks for them. var initSettingsAndroid = const AndroidInitializationSettings("app_icon"); // The default settings will show local push notifications while app in foreground with plugin. - var initSettingsIOS = const DarwinInitializationSettings(); + var initSettingsIOS = const DarwinInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + ); var initSettings = InitializationSettings( android: initSettingsAndroid, iOS: initSettingsIOS, diff --git a/apps/amiapp_flutter/lib/src/screens/settings.dart b/apps/amiapp_flutter/lib/src/screens/settings.dart index a7de9d5..a9d4629 100644 --- a/apps/amiapp_flutter/lib/src/screens/settings.dart +++ b/apps/amiapp_flutter/lib/src/screens/settings.dart @@ -50,7 +50,7 @@ class _SettingsScreenState extends State { @override void initState() { - CustomerIO.instance.pushMessaging.getRegisteredDeviceToken().then((value) => + CustomerIO.pushMessaging.getRegisteredDeviceToken().then((value) => setState(() => _deviceTokenValueController.text = value ?? '')); final cioConfig = widget._customerIOSDK.sdkConfig; diff --git a/lib/customer_io.dart b/lib/customer_io.dart index c4e444a..5ed4376 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -61,7 +61,15 @@ class CustomerIO { } /// Access push messaging functionality - CustomerIOMessagingPushPlatform get pushMessaging => _pushMessaging; + static CustomerIOMessagingPushPlatform get pushMessaging { + if (_instance == null) { + throw StateError( + 'CustomerIO SDK must be initialized before accessing push module.\n' + 'Call CustomerIO.initialize() first.', + ); + } + return _instance!._pushMessaging; + } /// Access in-app messaging functionality CustomerIOMessagingInAppPlatform get inAppMessaging => _inAppMessaging; From bd03be8dc5ff90de403c588a971a018e0139b10c Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 25 Nov 2024 12:59:45 +0500 Subject: [PATCH 24/34] chore: added in-app event listeners (#180) --- .../customer_io/CustomerIOPluginModule.kt | 15 ++- .../customer/customer_io/CustomerIoPlugin.kt | 90 +++++--------- .../CustomerIOInAppMessaging.kt | 115 ++++++++++++++---- .../messagingpush/CustomerIOPushMessaging.kt | 56 ++++----- .../lib/src/screens/dashboard.dart | 2 +- ios/Classes/SwiftCustomerIoPlugin.swift | 5 + lib/customer_io.dart | 12 -- lib/customer_io_method_channel.dart | 45 ------- lib/customer_io_platform_interface.dart | 7 -- lib/messaging_in_app/method_channel.dart | 46 +++++++ lib/messaging_in_app/platform_interface.dart | 9 ++ test/customer_io_test.mocks.dart | 1 - 12 files changed, 221 insertions(+), 182 deletions(-) diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPluginModule.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPluginModule.kt index 403a339..ebf0482 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPluginModule.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPluginModule.kt @@ -1,6 +1,9 @@ package io.customer.customer_io +import io.customer.sdk.CustomerIOBuilder import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodChannel /** @@ -8,7 +11,7 @@ import io.flutter.plugin.common.MethodChannel * should be treated as module in Flutter SDK and should be used to hold all relevant methods at * single place. */ -internal interface CustomerIOPluginModule : MethodChannel.MethodCallHandler { +internal interface CustomerIOPluginModule : MethodChannel.MethodCallHandler, ActivityAware { /** * Unique name of module to identify between other modules */ @@ -36,4 +39,14 @@ internal interface CustomerIOPluginModule : MethodChannel.MethodCallHandler { fun onDetachedFromEngine() { flutterCommunicationChannel.setMethodCallHandler(null) } + + fun configureModule(builder: CustomerIOBuilder, config: Map) + + override fun onDetachedFromActivity() {} + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {} + + override fun onDetachedFromActivityForConfigChanges() {} + + override fun onAttachedToActivity(binding: ActivityPluginBinding) {} } diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt index 1a0f400..399a387 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -1,14 +1,11 @@ package io.customer.customer_io -import android.app.Activity import android.app.Application import android.content.Context import androidx.annotation.NonNull import io.customer.customer_io.constant.Keys import io.customer.customer_io.messaginginapp.CustomerIOInAppMessaging import io.customer.customer_io.messagingpush.CustomerIOPushMessaging -import io.customer.messaginginapp.type.InAppEventListener -import io.customer.messaginginapp.type.InAppMessage import io.customer.sdk.CustomerIO import io.customer.sdk.CustomerIOBuilder import io.customer.sdk.core.di.SDKComponent @@ -24,7 +21,6 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import java.lang.ref.WeakReference /** * Android implementation of plugin that will let Flutter developers to @@ -37,28 +33,11 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { /// when the Flutter Engine is detached from the Activity private lateinit var flutterCommunicationChannel: MethodChannel private lateinit var context: Context - private var activity: WeakReference? = null private lateinit var modules: List private val logger: Logger = SDKComponent.logger - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - this.activity = WeakReference(binding.activity) - } - - override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - onAttachedToActivity(binding) - } - - override fun onDetachedFromActivity() { - this.activity = null - } - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext flutterCommunicationChannel = @@ -276,18 +255,21 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { args.getAsTypeOrNull("cdnHost")?.let(::cdnHost) // Configure in-app messaging module based on config provided by customer app args.getAsTypeOrNull>(key = "inApp")?.let { inAppConfig -> - CustomerIOInAppMessaging.addNativeModuleFromConfig( - builder = this, - config = inAppConfig, - region = givenRegion - ) + modules.filterIsInstance().forEach { + it.configureModule( + builder = this, + config = inAppConfig.plus("region" to givenRegion), + ) + } } // Configure push messaging module based on config provided by customer app args.getAsTypeOrNull>(key = "push").let { pushConfig -> - CustomerIOPushMessaging.addNativeModuleFromConfig( - builder = this, - config = pushConfig ?: emptyMap() - ) + modules.filterIsInstance().forEach { + it.configureModule( + builder = this, + config = pushConfig ?: emptyMap() + ) + } } }.build() @@ -303,44 +285,28 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { it.onDetachedFromEngine() } } -} -class CustomerIOInAppEventListener(private val invokeMethod: (String, Any?) -> Unit) : - InAppEventListener { - override fun errorWithMessage(message: InAppMessage) { - invokeMethod( - "errorWithMessage", mapOf( - "messageId" to message.messageId, "deliveryId" to message.deliveryId - ) - ) + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + modules.forEach { + it.onAttachedToActivity(binding) + } } - override fun messageActionTaken( - message: InAppMessage, actionValue: String, actionName: String - ) { - invokeMethod( - "messageActionTaken", mapOf( - "messageId" to message.messageId, - "deliveryId" to message.deliveryId, - "actionValue" to actionValue, - "actionName" to actionName - ) - ) + override fun onDetachedFromActivityForConfigChanges() { + modules.forEach { + it.onDetachedFromActivityForConfigChanges() + } } - override fun messageDismissed(message: InAppMessage) { - invokeMethod( - "messageDismissed", mapOf( - "messageId" to message.messageId, "deliveryId" to message.deliveryId - ) - ) + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + modules.forEach { + it.onReattachedToActivityForConfigChanges(binding) + } } - override fun messageShown(message: InAppMessage) { - invokeMethod( - "messageShown", mapOf( - "messageId" to message.messageId, "deliveryId" to message.deliveryId - ) - ) + override fun onDetachedFromActivity() { + modules.forEach { + it.onDetachedFromActivity() + } } } diff --git a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt index afb8b3e..5603003 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt @@ -1,5 +1,6 @@ package io.customer.customer_io.messaginginapp +import android.app.Activity import io.customer.customer_io.CustomerIOPluginModule import io.customer.customer_io.constant.Keys import io.customer.customer_io.getAsTypeOrNull @@ -7,13 +8,18 @@ import io.customer.customer_io.invokeNative import io.customer.messaginginapp.MessagingInAppModuleConfig import io.customer.messaginginapp.ModuleMessagingInApp import io.customer.messaginginapp.di.inAppMessaging +import io.customer.messaginginapp.type.InAppEventListener +import io.customer.messaginginapp.type.InAppMessage import io.customer.sdk.CustomerIO import io.customer.sdk.CustomerIOBuilder import io.customer.sdk.core.di.SDKComponent import io.customer.sdk.data.model.Region import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel +import java.lang.ref.WeakReference /** * Flutter module implementation for messaging in-app module in native SDKs. All functionality @@ -21,10 +27,27 @@ import io.flutter.plugin.common.MethodChannel */ internal class CustomerIOInAppMessaging( pluginBinding: FlutterPlugin.FlutterPluginBinding, -) : CustomerIOPluginModule, MethodChannel.MethodCallHandler { +) : CustomerIOPluginModule, MethodChannel.MethodCallHandler, ActivityAware { override val moduleName: String = "InAppMessaging" override val flutterCommunicationChannel: MethodChannel = MethodChannel(pluginBinding.binaryMessenger, "customer_io_messaging_in_app") + private var activity: WeakReference? = null + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + this.activity = WeakReference(binding.activity) + } + + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } + + override fun onDetachedFromActivity() { + this.activity = null + } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { @@ -40,30 +63,74 @@ internal class CustomerIOInAppMessaging( } } - companion object { - /** - * Adds in-app module to native Android SDK based on the configuration provided by - * customer app. - * - * @param builder instance of CustomerIOBuilder to add push messaging module. - * @param config configuration provided by customer app for in-app messaging module. - * @param region region of the customer app. - */ - internal fun addNativeModuleFromConfig( - builder: CustomerIOBuilder, - config: Map, - region: Region - ) { - val siteId = config.getAsTypeOrNull("siteId") - if (siteId.isNullOrBlank()) { - SDKComponent.logger.error("Site ID is required to initialize InAppMessaging module") - return - } - val module = ModuleMessagingInApp( - MessagingInAppModuleConfig.Builder(siteId = siteId, region = region).build(), - ) - builder.addCustomerIOModule(module) + /** + * Adds in-app module to native Android SDK based on the configuration provided by + * customer app. + * + * @param builder instance of CustomerIOBuilder to add push messaging module. + * @param config configuration provided by customer app for in-app messaging module. + */ + override fun configureModule( + builder: CustomerIOBuilder, + config: Map + ) { + val siteId = config.getAsTypeOrNull("siteId") + val regionRawValue = config.getAsTypeOrNull("region") + val givenRegion = regionRawValue.let { Region.getRegion(it) } + + if (siteId.isNullOrBlank()) { + SDKComponent.logger.error("Site ID is required to initialize InAppMessaging module") + return } + val module = ModuleMessagingInApp( + MessagingInAppModuleConfig.Builder(siteId = siteId, region = givenRegion) + .setEventListener(CustomerIOInAppEventListener { method, args -> + this.activity?.get()?.runOnUiThread { + flutterCommunicationChannel.invokeMethod(method, args) + } + }) + .build(), + ) + builder.addCustomerIOModule(module) } +} +class CustomerIOInAppEventListener(private val invokeMethod: (String, Any?) -> Unit) : + InAppEventListener { + override fun errorWithMessage(message: InAppMessage) { + invokeMethod( + "errorWithMessage", mapOf( + "messageId" to message.messageId, "deliveryId" to message.deliveryId + ) + ) + } + + override fun messageActionTaken( + message: InAppMessage, actionValue: String, actionName: String + ) { + invokeMethod( + "messageActionTaken", mapOf( + "messageId" to message.messageId, + "deliveryId" to message.deliveryId, + "actionValue" to actionValue, + "actionName" to actionName + ) + ) + } + + override fun messageDismissed(message: InAppMessage) { + invokeMethod( + "messageDismissed", mapOf( + "messageId" to message.messageId, "deliveryId" to message.deliveryId + ) + ) + } + + override fun messageShown(message: InAppMessage) { + invokeMethod( + "messageShown", mapOf( + "messageId" to message.messageId, "deliveryId" to message.deliveryId + ) + ) + } } \ No newline at end of file diff --git a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt index 0e6c560..f625be8 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt @@ -88,36 +88,34 @@ internal class CustomerIOPushMessaging( } } - companion object { - /** - * Adds push messaging module to native Android SDK based on the configuration provided by - * customer app. - * - * @param builder instance of CustomerIOBuilder to add push messaging module. - * @param config configuration provided by customer app for push messaging module. - */ - internal fun addNativeModuleFromConfig( - builder: CustomerIOBuilder, - config: Map - ) { - val androidConfig = - config.getAsTypeOrNull>(key = "android") ?: emptyMap() - // Prefer `android` object for push configurations as it's more specific to Android - // For common push configurations, use `config` object instead of `android` + /** + * Adds push messaging module to native Android SDK based on the configuration provided by + * customer app. + * + * @param builder instance of CustomerIOBuilder to add push messaging module. + * @param config configuration provided by customer app for push messaging module. + */ + override fun configureModule( + builder: CustomerIOBuilder, + config: Map + ) { + val androidConfig = + config.getAsTypeOrNull>(key = "android") ?: emptyMap() + // Prefer `android` object for push configurations as it's more specific to Android + // For common push configurations, use `config` object instead of `android` - // Default push click behavior is to prevent restart of activity in Flutter apps - val pushClickBehavior = androidConfig.getAsTypeOrNull("pushClickBehavior") - ?.takeIf { it.isNotBlank() } - ?.let { value -> - runCatching { enumValueOf(value) }.getOrNull() - } ?: PushClickBehavior.ACTIVITY_PREVENT_RESTART + // Default push click behavior is to prevent restart of activity in Flutter apps + val pushClickBehavior = androidConfig.getAsTypeOrNull("pushClickBehavior") + ?.takeIf { it.isNotBlank() } + ?.let { value -> + runCatching { enumValueOf(value) }.getOrNull() + } ?: PushClickBehavior.ACTIVITY_PREVENT_RESTART - val module = ModuleMessagingPushFCM( - moduleConfig = MessagingPushModuleConfig.Builder().apply { - setPushClickBehavior(pushClickBehavior = pushClickBehavior) - }.build(), - ) - builder.addCustomerIOModule(module) - } + val module = ModuleMessagingPushFCM( + moduleConfig = MessagingPushModuleConfig.Builder().apply { + setPushClickBehavior(pushClickBehavior = pushClickBehavior) + }.build(), + ) + builder.addCustomerIOModule(module) } } diff --git a/apps/amiapp_flutter/lib/src/screens/dashboard.dart b/apps/amiapp_flutter/lib/src/screens/dashboard.dart index 5b4308f..0c32a00 100644 --- a/apps/amiapp_flutter/lib/src/screens/dashboard.dart +++ b/apps/amiapp_flutter/lib/src/screens/dashboard.dart @@ -54,7 +54,7 @@ class _DashboardScreenState extends State { .then((value) => setState(() => _buildInfo = value)); inAppMessageStreamSubscription = - CustomerIO.instance.subscribeToInAppEventListener(handleInAppEvent); + CustomerIO.instance.inAppMessaging.subscribeToInAppEventListener(handleInAppEvent); // Setup 3rd party SDK, flutter-fire. // We install this SDK into sample app to make sure the CIO SDK behaves as expected when there is another SDK installed that handles push notifications. diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index 00dafae..f8363c1 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -179,6 +179,11 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { if let inAppConfig = try? MessagingInAppConfigBuilder.build(from: params) { MessagingInApp.initialize(withConfig: inAppConfig) + MessagingInApp.shared.setEventListener(CustomerIOInAppEventListener( + invokeMethod: {method,args in + self.invokeMethod(method, args) + }) + ) } // TODO: Initialize in-app module with given config diff --git a/lib/customer_io.dart b/lib/customer_io.dart index 5ed4376..a231ae8 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -5,7 +5,6 @@ import 'package:flutter/cupertino.dart'; import 'customer_io_config.dart'; import 'customer_io_enums.dart'; -import 'customer_io_inapp.dart'; import 'customer_io_platform_interface.dart'; import 'extensions/map_extensions.dart'; import 'messaging_in_app/platform_interface.dart'; @@ -162,15 +161,4 @@ class CustomerIO { return _platform.setProfileAttributes( attributes: attributes.excludeNullValues()); } - - /// Subscribes to an in-app event listener. - /// - /// [onEvent] - A callback function that will be called every time an in-app event occurs. - /// The callback returns [InAppEvent]. - /// - /// Returns a [StreamSubscription] that can be used to subscribe/unsubscribe from the event listener. - StreamSubscription subscribeToInAppEventListener( - void Function(InAppEvent) onEvent) { - return _platform.subscribeToInAppEventListener(onEvent); - } } diff --git a/lib/customer_io_method_channel.dart b/lib/customer_io_method_channel.dart index 463e8ab..5a303c0 100644 --- a/lib/customer_io_method_channel.dart +++ b/lib/customer_io_method_channel.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'customer_io_config.dart'; import 'customer_io_const.dart'; -import 'customer_io_inapp.dart'; import 'customer_io_platform_interface.dart'; /// An implementation of [CustomerIOPlatform] that uses method channels. @@ -15,50 +14,6 @@ class CustomerIOMethodChannel extends CustomerIOPlatform { @visibleForTesting final methodChannel = const MethodChannel('customer_io'); - final _inAppEventStreamController = StreamController.broadcast(); - - CustomerIOMethodChannel() { - methodChannel.setMethodCallHandler(_onMethodCall); - } - - /// Method to subscribe to the In-App event listener. - /// - /// The `onEvent` function will be called whenever an In-App event occurs. - /// Returns a [StreamSubscription] object that can be used to unsubscribe from the stream. - @override - StreamSubscription subscribeToInAppEventListener( - void Function(InAppEvent) onEvent) { - StreamSubscription subscription = - _inAppEventStreamController.stream.listen(onEvent); - return subscription; - } - - /// Method call handler to handle events from native bindings - Future _onMethodCall(MethodCall call) async { - /// Cast the arguments to a map of strings to dynamic values. - final arguments = - (call.arguments as Map).cast(); - - switch (call.method) { - case "messageShown": - _inAppEventStreamController - .add(InAppEvent.fromMap(EventType.messageShown, arguments)); - break; - case "messageDismissed": - _inAppEventStreamController - .add(InAppEvent.fromMap(EventType.messageDismissed, arguments)); - break; - case "errorWithMessage": - _inAppEventStreamController - .add(InAppEvent.fromMap(EventType.errorWithMessage, arguments)); - break; - case "messageActionTaken": - _inAppEventStreamController - .add(InAppEvent.fromMap(EventType.messageActionTaken, arguments)); - break; - } - } - /// To initialize the plugin @override Future initialize({ diff --git a/lib/customer_io_platform_interface.dart b/lib/customer_io_platform_interface.dart index a7bd11f..c9646b5 100644 --- a/lib/customer_io_platform_interface.dart +++ b/lib/customer_io_platform_interface.dart @@ -4,7 +4,6 @@ import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'customer_io_inapp.dart'; import 'customer_io_method_channel.dart'; /// The default instance of [CustomerIOPlatform] to use @@ -73,10 +72,4 @@ abstract class CustomerIOPlatform extends PlatformInterface { throw UnimplementedError( 'setProfileAttributes() has not been implemented.'); } - - StreamSubscription subscribeToInAppEventListener( - void Function(InAppEvent) onEvent) { - throw UnimplementedError( - 'subscribeToInAppEventListener() has not been implemented.'); - } } diff --git a/lib/messaging_in_app/method_channel.dart b/lib/messaging_in_app/method_channel.dart index cd5a046..2f32c06 100644 --- a/lib/messaging_in_app/method_channel.dart +++ b/lib/messaging_in_app/method_channel.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../customer_io_const.dart'; +import '../customer_io_inapp.dart'; import 'platform_interface.dart'; /// An implementation of [CustomerIOMessagingInAppPlatform] that uses method @@ -11,6 +14,7 @@ class CustomerIOMessagingInAppMethodChannel /// The method channel used to interact with the native platform. @visibleForTesting final methodChannel = const MethodChannel('customer_io_messaging_in_app'); + final _inAppEventStreamController = StreamController.broadcast(); @override void dismissMessage() async { @@ -21,6 +25,48 @@ class CustomerIOMessagingInAppMethodChannel } } + /// Method to subscribe to the In-App event listener. + /// + /// The `onEvent` function will be called whenever an In-App event occurs. + /// Returns a [StreamSubscription] object that can be used to unsubscribe from the stream. + @override + StreamSubscription subscribeToInAppEventListener( + void Function(InAppEvent) onEvent) { + StreamSubscription subscription = + _inAppEventStreamController.stream.listen(onEvent); + return subscription; + } + + CustomerIOMessagingInAppMethodChannel() { + methodChannel.setMethodCallHandler(_onMethodCall); + } + + /// Method call handler to handle events from native bindings + Future _onMethodCall(MethodCall call) async { + /// Cast the arguments to a map of strings to dynamic values. + final arguments = + (call.arguments as Map).cast(); + + switch (call.method) { + case "messageShown": + _inAppEventStreamController + .add(InAppEvent.fromMap(EventType.messageShown, arguments)); + break; + case "messageDismissed": + _inAppEventStreamController + .add(InAppEvent.fromMap(EventType.messageDismissed, arguments)); + break; + case "errorWithMessage": + _inAppEventStreamController + .add(InAppEvent.fromMap(EventType.errorWithMessage, arguments)); + break; + case "messageActionTaken": + _inAppEventStreamController + .add(InAppEvent.fromMap(EventType.messageActionTaken, arguments)); + break; + } + } + void handleException(PlatformException exception) { if (kDebugMode) { print(exception); diff --git a/lib/messaging_in_app/platform_interface.dart b/lib/messaging_in_app/platform_interface.dart index b05daf7..d123ac0 100644 --- a/lib/messaging_in_app/platform_interface.dart +++ b/lib/messaging_in_app/platform_interface.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import '../customer_io_inapp.dart'; import 'method_channel.dart'; /// The default instance of [CustomerIOMessagingInAppPlatform] to use @@ -27,4 +30,10 @@ abstract class CustomerIOMessagingInAppPlatform extends PlatformInterface { void dismissMessage() { throw UnimplementedError('dismissMessage() has not been implemented.'); } + + StreamSubscription subscribeToInAppEventListener( + void Function(InAppEvent) onEvent) { + throw UnimplementedError( + 'subscribeToInAppEventListener() has not been implemented.'); + } } diff --git a/test/customer_io_test.mocks.dart b/test/customer_io_test.mocks.dart index dd87c9f..4a35717 100644 --- a/test/customer_io_test.mocks.dart +++ b/test/customer_io_test.mocks.dart @@ -158,7 +158,6 @@ class MockTestCustomerIoPlatform extends _i1.Mock ), returnValueForMissingStub: null, ); - @override _i2.StreamSubscription subscribeToInAppEventListener( void Function(_i6.InAppEvent)? onEvent) => (super.noSuchMethod( From ae09860445545f7f2234a880cc97c1443e53b3f9 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Tue, 26 Nov 2024 12:39:01 +0500 Subject: [PATCH 25/34] chore: rename android classes and method extensions (#179) --- .../customer_io/CustomerIOExtensions.kt | 11 ---- ...ustomerIoPlugin.kt => CustomerIOPlugin.kt} | 57 ++++++++++--------- .../NativeModuleBridge.kt} | 4 +- .../CustomerIOInAppMessaging.kt | 10 ++-- .../messagingpush/CustomerIOPushMessaging.kt | 24 ++++---- ...tensions.kt => PushMessagingExtensions.kt} | 14 ++--- .../customer_io/utils/MapExtensions.kt | 12 ++++ .../customer_io/utils/StringExtensions.kt | 6 ++ pubspec.yaml | 2 +- 9 files changed, 74 insertions(+), 66 deletions(-) rename android/src/main/kotlin/io/customer/customer_io/{CustomerIoPlugin.kt => CustomerIOPlugin.kt} (80%) rename android/src/main/kotlin/io/customer/customer_io/{CustomerIOPluginModule.kt => bridge/NativeModuleBridge.kt} (92%) rename android/src/main/kotlin/io/customer/customer_io/messagingpush/{Extensions.kt => PushMessagingExtensions.kt} (78%) create mode 100644 android/src/main/kotlin/io/customer/customer_io/utils/MapExtensions.kt create mode 100644 android/src/main/kotlin/io/customer/customer_io/utils/StringExtensions.kt diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt index 138a65a..b6256fc 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt @@ -3,17 +3,6 @@ package io.customer.customer_io import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -/** - * Returns the value corresponding to the given key after casting to the generic type provided, or - * null if such key is not present in the map or value cannot be casted to the given type. - */ -internal inline fun Map.getAsTypeOrNull(key: String): T? { - if (containsKey(key)) { - return get(key) as? T - } - return null -} - /** * Invokes lambda method that can be used to call matching native method conveniently. The lambda * expression receives function parameters as arguments and should return the desired result. Any diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt similarity index 80% rename from android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt rename to android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt index 399a387..9da23c2 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt @@ -3,9 +3,11 @@ package io.customer.customer_io import android.app.Application import android.content.Context import androidx.annotation.NonNull +import io.customer.customer_io.bridge.NativeModuleBridge import io.customer.customer_io.constant.Keys import io.customer.customer_io.messaginginapp.CustomerIOInAppMessaging import io.customer.customer_io.messagingpush.CustomerIOPushMessaging +import io.customer.customer_io.utils.getAs import io.customer.sdk.CustomerIO import io.customer.sdk.CustomerIOBuilder import io.customer.sdk.core.di.SDKComponent @@ -26,7 +28,7 @@ import io.flutter.plugin.common.MethodChannel.Result * Android implementation of plugin that will let Flutter developers to * interact with a Android platform * */ -class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { +class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it @@ -34,7 +36,7 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var flutterCommunicationChannel: MethodChannel private lateinit var context: Context - private lateinit var modules: List + private lateinit var modules: List private val logger: Logger = SDKComponent.logger @@ -133,8 +135,8 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun identify(params: Map) { - val userId = params.getAsTypeOrNull(Keys.Tracking.USER_ID) - val traits = params.getAsTypeOrNull>(Keys.Tracking.TRAITS) ?: emptyMap() + val userId = params.getAs(Keys.Tracking.USER_ID) + val traits = params.getAs>(Keys.Tracking.TRAITS) ?: emptyMap() if (userId == null && traits.isEmpty()) { logger.error("Please provide either an ID or traits to identify.") @@ -151,10 +153,10 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun track(params: Map) { - val name = requireNotNull(params.getAsTypeOrNull(Keys.Tracking.NAME)) { + val name = requireNotNull(params.getAs(Keys.Tracking.NAME)) { "Event name is missing in params: $params" } - val properties = params.getAsTypeOrNull>(Keys.Tracking.PROPERTIES) + val properties = params.getAs>(Keys.Tracking.PROPERTIES) if (properties.isNullOrEmpty()) { CustomerIO.instance().track(name) @@ -164,16 +166,16 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun registerDeviceToken(params: Map) { - val token = requireNotNull(params.getAsTypeOrNull(Keys.Tracking.TOKEN)) { + val token = requireNotNull(params.getAs(Keys.Tracking.TOKEN)) { "Device token is missing in params: $params" } CustomerIO.instance().registerDeviceToken(token) } private fun trackMetric(params: Map) { - val deliveryId = params.getAsTypeOrNull(Keys.Tracking.DELIVERY_ID) - val deliveryToken = params.getAsTypeOrNull(Keys.Tracking.DELIVERY_TOKEN) - val eventName = params.getAsTypeOrNull(Keys.Tracking.METRIC_EVENT) + val deliveryId = params.getAs(Keys.Tracking.DELIVERY_ID) + val deliveryToken = params.getAs(Keys.Tracking.DELIVERY_TOKEN) + val eventName = params.getAs(Keys.Tracking.METRIC_EVENT) if (deliveryId == null || deliveryToken == null || eventName == null) { throw IllegalArgumentException("Missing required parameters") @@ -191,7 +193,7 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun setDeviceAttributes(params: Map) { - val attributes = params.getAsTypeOrNull>(Keys.Tracking.ATTRIBUTES) + val attributes = params.getAs>(Keys.Tracking.ATTRIBUTES) if (attributes.isNullOrEmpty()) { logger.error("Device attributes are missing in params: $params") @@ -202,7 +204,7 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun setProfileAttributes(params: Map) { - val attributes = params.getAsTypeOrNull>(Keys.Tracking.ATTRIBUTES) + val attributes = params.getAs>(Keys.Tracking.ATTRIBUTES) if (attributes.isNullOrEmpty()) { logger.error("Profile attributes are missing in params: $params") @@ -213,10 +215,10 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun screen(params: Map) { - val title = requireNotNull(params.getAsTypeOrNull(Keys.Tracking.TITLE)) { + val title = requireNotNull(params.getAs(Keys.Tracking.TITLE)) { "Screen title is missing in params: $params" } - val properties = params.getAsTypeOrNull>(Keys.Tracking.PROPERTIES) + val properties = params.getAs>(Keys.Tracking.PROPERTIES) if (properties.isNullOrEmpty()) { CustomerIO.instance().screen(title) @@ -227,12 +229,12 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private fun initialize(args: Map): kotlin.Result = runCatching { val application: Application = context.applicationContext as Application - val cdpApiKey = requireNotNull(args.getAsTypeOrNull("cdpApiKey")) { + val cdpApiKey = requireNotNull(args.getAs("cdpApiKey")) { "CDP API Key is required to initialize Customer.io" } - val logLevelRawValue = args.getAsTypeOrNull("logLevel") - val regionRawValue = args.getAsTypeOrNull("region") + val logLevelRawValue = args.getAs("logLevel") + val regionRawValue = args.getAs("region") val givenRegion = regionRawValue.let { Region.getRegion(it) } CustomerIOBuilder( @@ -242,28 +244,27 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { logLevelRawValue?.let { logLevel(CioLogLevel.getLogLevel(it)) } regionRawValue?.let { region(givenRegion) } - args.getAsTypeOrNull("migrationSiteId")?.let(::migrationSiteId) - args.getAsTypeOrNull("autoTrackDeviceAttributes") - ?.let(::autoTrackDeviceAttributes) - args.getAsTypeOrNull("trackApplicationLifecycleEvents") + args.getAs("migrationSiteId")?.let(::migrationSiteId) + args.getAs("autoTrackDeviceAttributes")?.let(::autoTrackDeviceAttributes) + args.getAs("trackApplicationLifecycleEvents") ?.let(::trackApplicationLifecycleEvents) - args.getAsTypeOrNull("flushAt")?.let(::flushAt) - args.getAsTypeOrNull("flushInterval")?.let(::flushInterval) + args.getAs("flushAt")?.let(::flushAt) + args.getAs("flushInterval")?.let(::flushInterval) - args.getAsTypeOrNull("apiHost")?.let(::apiHost) - args.getAsTypeOrNull("cdnHost")?.let(::cdnHost) + args.getAs("apiHost")?.let(::apiHost) + args.getAs("cdnHost")?.let(::cdnHost) // Configure in-app messaging module based on config provided by customer app - args.getAsTypeOrNull>(key = "inApp")?.let { inAppConfig -> + args.getAs>(key = "inApp")?.let { inAppConfig -> modules.filterIsInstance().forEach { it.configureModule( builder = this, - config = inAppConfig.plus("region" to givenRegion), + config = inAppConfig.plus("region" to givenRegion.code), ) } } // Configure push messaging module based on config provided by customer app - args.getAsTypeOrNull>(key = "push").let { pushConfig -> + args.getAs>(key = "push").let { pushConfig -> modules.filterIsInstance().forEach { it.configureModule( builder = this, diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPluginModule.kt b/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt similarity index 92% rename from android/src/main/kotlin/io/customer/customer_io/CustomerIOPluginModule.kt rename to android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt index ebf0482..d891ad5 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPluginModule.kt +++ b/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt @@ -1,4 +1,4 @@ -package io.customer.customer_io +package io.customer.customer_io.bridge import io.customer.sdk.CustomerIOBuilder import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -11,7 +11,7 @@ import io.flutter.plugin.common.MethodChannel * should be treated as module in Flutter SDK and should be used to hold all relevant methods at * single place. */ -internal interface CustomerIOPluginModule : MethodChannel.MethodCallHandler, ActivityAware { +internal interface NativeModuleBridge : MethodChannel.MethodCallHandler, ActivityAware { /** * Unique name of module to identify between other modules */ diff --git a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt index 5603003..ece79f6 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt @@ -1,10 +1,10 @@ package io.customer.customer_io.messaginginapp import android.app.Activity -import io.customer.customer_io.CustomerIOPluginModule +import io.customer.customer_io.bridge.NativeModuleBridge import io.customer.customer_io.constant.Keys -import io.customer.customer_io.getAsTypeOrNull import io.customer.customer_io.invokeNative +import io.customer.customer_io.utils.getAs import io.customer.messaginginapp.MessagingInAppModuleConfig import io.customer.messaginginapp.ModuleMessagingInApp import io.customer.messaginginapp.di.inAppMessaging @@ -27,7 +27,7 @@ import java.lang.ref.WeakReference */ internal class CustomerIOInAppMessaging( pluginBinding: FlutterPlugin.FlutterPluginBinding, -) : CustomerIOPluginModule, MethodChannel.MethodCallHandler, ActivityAware { +) : NativeModuleBridge, MethodChannel.MethodCallHandler, ActivityAware { override val moduleName: String = "InAppMessaging" override val flutterCommunicationChannel: MethodChannel = MethodChannel(pluginBinding.binaryMessenger, "customer_io_messaging_in_app") @@ -74,8 +74,8 @@ internal class CustomerIOInAppMessaging( builder: CustomerIOBuilder, config: Map ) { - val siteId = config.getAsTypeOrNull("siteId") - val regionRawValue = config.getAsTypeOrNull("region") + val siteId = config.getAs("siteId") + val regionRawValue = config.getAs("region") val givenRegion = regionRawValue.let { Region.getRegion(it) } if (siteId.isNullOrBlank()) { diff --git a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt index f625be8..ae78ef8 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt @@ -1,10 +1,11 @@ package io.customer.customer_io.messagingpush import android.content.Context -import io.customer.customer_io.CustomerIOPluginModule +import io.customer.customer_io.bridge.NativeModuleBridge import io.customer.customer_io.constant.Keys -import io.customer.customer_io.getAsTypeOrNull import io.customer.customer_io.invokeNative +import io.customer.customer_io.utils.getAs +import io.customer.customer_io.utils.takeIfNotBlank import io.customer.messagingpush.CustomerIOFirebaseMessagingService import io.customer.messagingpush.MessagingPushModuleConfig import io.customer.messagingpush.ModuleMessagingPushFCM @@ -16,7 +17,7 @@ import io.customer.sdk.core.util.Logger import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import java.util.* +import java.util.UUID /** * Flutter module implementation for messaging push module in native SDKs. All functionality @@ -24,7 +25,7 @@ import java.util.* */ internal class CustomerIOPushMessaging( pluginBinding: FlutterPlugin.FlutterPluginBinding, -) : CustomerIOPluginModule, MethodChannel.MethodCallHandler { +) : NativeModuleBridge, MethodChannel.MethodCallHandler { override val moduleName: String = "PushMessaging" private val applicationContext: Context = pluginBinding.applicationContext override val flutterCommunicationChannel: MethodChannel = @@ -42,8 +43,8 @@ internal class CustomerIOPushMessaging( Keys.Methods.ON_MESSAGE_RECEIVED -> { call.invokeNative(result) { args -> return@invokeNative onMessageReceived( - message = args.getAsTypeOrNull>("message"), - handleNotificationTrigger = args.getAsTypeOrNull("handleNotificationTrigger") + message = args.getAs>("message"), + handleNotificationTrigger = args.getAs("handleNotificationTrigger") ) } } @@ -75,8 +76,8 @@ internal class CustomerIOPushMessaging( } // Generate destination string, see docs on receiver method for more details - val destination = (message["to"] as? String)?.takeIf { it.isNotBlank() } - ?: UUID.randomUUID().toString() + val destination = + (message["to"] as? String)?.takeIfNotBlank() ?: UUID.randomUUID().toString() return CustomerIOFirebaseMessagingService.onMessageReceived( context = applicationContext, remoteMessage = message.toFCMRemoteMessage(destination = destination), @@ -99,14 +100,13 @@ internal class CustomerIOPushMessaging( builder: CustomerIOBuilder, config: Map ) { - val androidConfig = - config.getAsTypeOrNull>(key = "android") ?: emptyMap() + val androidConfig = config.getAs>(key = "android") ?: emptyMap() // Prefer `android` object for push configurations as it's more specific to Android // For common push configurations, use `config` object instead of `android` // Default push click behavior is to prevent restart of activity in Flutter apps - val pushClickBehavior = androidConfig.getAsTypeOrNull("pushClickBehavior") - ?.takeIf { it.isNotBlank() } + val pushClickBehavior = androidConfig.getAs("pushClickBehavior") + ?.takeIfNotBlank() ?.let { value -> runCatching { enumValueOf(value) }.getOrNull() } ?: PushClickBehavior.ACTIVITY_PREVENT_RESTART diff --git a/android/src/main/kotlin/io/customer/customer_io/messagingpush/Extensions.kt b/android/src/main/kotlin/io/customer/customer_io/messagingpush/PushMessagingExtensions.kt similarity index 78% rename from android/src/main/kotlin/io/customer/customer_io/messagingpush/Extensions.kt rename to android/src/main/kotlin/io/customer/customer_io/messagingpush/PushMessagingExtensions.kt index b51fa91..6b1f071 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messagingpush/Extensions.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messagingpush/PushMessagingExtensions.kt @@ -1,7 +1,7 @@ package io.customer.customer_io.messagingpush import com.google.firebase.messaging.RemoteMessage -import io.customer.customer_io.getAsTypeOrNull +import io.customer.customer_io.utils.getAs /** * Safely transforms any value to string @@ -23,8 +23,8 @@ private fun Any.toStringOrNull(): String? = try { * string for it. */ internal fun Map.toFCMRemoteMessage(destination: String): RemoteMessage { - val notification = getAsTypeOrNull>("notification") - val data = getAsTypeOrNull>("data") + val notification = getAs>("notification") + val data = getAs>("data") val messageParams = buildMap { notification?.let { result -> putAll(result) } // Adding `data` after `notification` so `data` params take more value as we mainly use @@ -42,10 +42,10 @@ internal fun Map.toFCMRemoteMessage(destination: String): RemoteMes value.toStringOrNull()?.let { v -> addData(key, v) } } } - getAsTypeOrNull("messageId")?.let { id -> setMessageId(id) } - getAsTypeOrNull("messageType")?.let { type -> setMessageType(type) } - getAsTypeOrNull("collapseKey")?.let { key -> setCollapseKey(key) } - getAsTypeOrNull("ttl")?.let { time -> ttl = time } + getAs("messageId")?.let { id -> setMessageId(id) } + getAs("messageType")?.let { type -> setMessageType(type) } + getAs("collapseKey")?.let { key -> setCollapseKey(key) } + getAs("ttl")?.let { time -> ttl = time } return@with build() } } diff --git a/android/src/main/kotlin/io/customer/customer_io/utils/MapExtensions.kt b/android/src/main/kotlin/io/customer/customer_io/utils/MapExtensions.kt new file mode 100644 index 0000000..d0a2785 --- /dev/null +++ b/android/src/main/kotlin/io/customer/customer_io/utils/MapExtensions.kt @@ -0,0 +1,12 @@ +package io.customer.customer_io.utils + +/** + * Returns the value corresponding to the given key after casting to the generic type provided, or + * null if such key is not present in the map or value cannot be casted to the given type. + */ +internal inline fun Map.getAs(key: String): T? { + if (containsKey(key)) { + return get(key) as? T + } + return null +} diff --git a/android/src/main/kotlin/io/customer/customer_io/utils/StringExtensions.kt b/android/src/main/kotlin/io/customer/customer_io/utils/StringExtensions.kt new file mode 100644 index 0000000..42ac132 --- /dev/null +++ b/android/src/main/kotlin/io/customer/customer_io/utils/StringExtensions.kt @@ -0,0 +1,6 @@ +package io.customer.customer_io.utils + +/** + * Extension function to return the string if it is not null or blank. + */ +internal fun String?.takeIfNotBlank(): String? = takeIf { !it.isNullOrBlank() } diff --git a/pubspec.yaml b/pubspec.yaml index d91304d..c77b0b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,7 +39,7 @@ flutter: platforms: android: package: io.customer.customer_io - pluginClass: CustomerIoPlugin + pluginClass: CustomerIOPlugin ios: pluginClass: CustomerIoPlugin native_sdk_version: 3.5.1 From 84a5fa7a25cfea69c9eac983029337313b9f9e98 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Wed, 27 Nov 2024 01:16:20 +0500 Subject: [PATCH 26/34] chore: refactor native android call forwarding (#182) --- .../customer_io/CustomerIOExtensions.kt | 27 ---- .../customer/customer_io/CustomerIOPlugin.kt | 123 ++++++------------ .../bridge/MethodCallExtensions.kt | 56 ++++++++ .../customer_io/bridge/NativeModuleBridge.kt | 9 ++ .../io/customer/customer_io/constant/Keys.kt | 35 ----- .../CustomerIOInAppMessaging.kt | 18 +-- .../messagingpush/CustomerIOPushMessaging.kt | 38 ++---- 7 files changed, 124 insertions(+), 182 deletions(-) delete mode 100644 android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt create mode 100644 android/src/main/kotlin/io/customer/customer_io/bridge/MethodCallExtensions.kt delete mode 100644 android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt deleted file mode 100644 index b6256fc..0000000 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.customer.customer_io - -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel - -/** - * Invokes lambda method that can be used to call matching native method conveniently. The lambda - * expression receives function parameters as arguments and should return the desired result. Any - * exception in the lambda will cause the invoked method to fail with error. - */ -internal fun MethodCall.invokeNative( - result: MethodChannel.Result, - performAction: (params: Map) -> R, -) { - try { - @Suppress("UNCHECKED_CAST") - val params = this.arguments as? Map ?: emptyMap() - val actionResult = performAction(params) - if (actionResult is Unit) { - result.success(true) - } else { - result.success(actionResult) - } - } catch (ex: Exception) { - result.error(this.method, ex.localizedMessage, ex) - } -} diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt index 9da23c2..f99720b 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt @@ -4,7 +4,8 @@ import android.app.Application import android.content.Context import androidx.annotation.NonNull import io.customer.customer_io.bridge.NativeModuleBridge -import io.customer.customer_io.constant.Keys +import io.customer.customer_io.bridge.nativeMapArgs +import io.customer.customer_io.bridge.nativeNoArgs import io.customer.customer_io.messaginginapp.CustomerIOInAppMessaging import io.customer.customer_io.messagingpush.CustomerIOPushMessaging import io.customer.customer_io.utils.getAs @@ -58,85 +59,28 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } - private fun MethodCall.toNativeMethodCall( - result: Result, performAction: (params: Map) -> Unit - ) { - try { - val params = this.arguments as? Map ?: emptyMap() - performAction(params) - result.success(true) - } catch (e: Exception) { - result.error(this.method, e.localizedMessage, null) - } - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { - Keys.Methods.INITIALIZE -> { - call.toNativeMethodCall(result) { - initialize(it) - } - } - - Keys.Methods.IDENTIFY -> { - call.toNativeMethodCall(result) { - identify(it) - } - } - - Keys.Methods.SCREEN -> { - call.toNativeMethodCall(result) { - screen(it) - } - } - - Keys.Methods.TRACK -> { - call.toNativeMethodCall(result) { - track(it) - } - } - - Keys.Methods.TRACK_METRIC -> { - call.toNativeMethodCall(result) { - trackMetric(it) - } - } - - Keys.Methods.REGISTER_DEVICE_TOKEN -> { - call.toNativeMethodCall(result) { - registerDeviceToken(it) - } - } - - Keys.Methods.SET_DEVICE_ATTRIBUTES -> { - call.toNativeMethodCall(result) { - setDeviceAttributes(it) - } - } - - Keys.Methods.SET_PROFILE_ATTRIBUTES -> { - call.toNativeMethodCall(result) { - setProfileAttributes(it) - } - } - - Keys.Methods.CLEAR_IDENTIFY -> { - clearIdentity() - } - - else -> { - result.notImplemented() - } + "clearIdentify" -> call.nativeNoArgs(result, ::clearIdentify) + "identify" -> call.nativeMapArgs(result, ::identify) + "initialize" -> call.nativeMapArgs(result, ::initialize) + "registerDeviceToken" -> call.nativeMapArgs(result, ::registerDeviceToken) + "screen" -> call.nativeMapArgs(result, ::screen) + "setDeviceAttributes" -> call.nativeMapArgs(result, ::setDeviceAttributes) + "setProfileAttributes" -> call.nativeMapArgs(result, ::setProfileAttributes) + "track" -> call.nativeMapArgs(result, ::track) + "trackMetric" -> call.nativeMapArgs(result, ::trackMetric) + else -> result.notImplemented() } } - private fun clearIdentity() { + private fun clearIdentify() { CustomerIO.instance().clearIdentify() } private fun identify(params: Map) { - val userId = params.getAs(Keys.Tracking.USER_ID) - val traits = params.getAs>(Keys.Tracking.TRAITS) ?: emptyMap() + val userId = params.getAs(Args.USER_ID) + val traits = params.getAs>(Args.TRAITS) ?: emptyMap() if (userId == null && traits.isEmpty()) { logger.error("Please provide either an ID or traits to identify.") @@ -153,10 +97,10 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun track(params: Map) { - val name = requireNotNull(params.getAs(Keys.Tracking.NAME)) { + val name = requireNotNull(params.getAs(Args.NAME)) { "Event name is missing in params: $params" } - val properties = params.getAs>(Keys.Tracking.PROPERTIES) + val properties = params.getAs>(Args.PROPERTIES) if (properties.isNullOrEmpty()) { CustomerIO.instance().track(name) @@ -166,16 +110,16 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun registerDeviceToken(params: Map) { - val token = requireNotNull(params.getAs(Keys.Tracking.TOKEN)) { + val token = requireNotNull(params.getAs(Args.TOKEN)) { "Device token is missing in params: $params" } CustomerIO.instance().registerDeviceToken(token) } private fun trackMetric(params: Map) { - val deliveryId = params.getAs(Keys.Tracking.DELIVERY_ID) - val deliveryToken = params.getAs(Keys.Tracking.DELIVERY_TOKEN) - val eventName = params.getAs(Keys.Tracking.METRIC_EVENT) + val deliveryId = params.getAs(Args.DELIVERY_ID) + val deliveryToken = params.getAs(Args.DELIVERY_TOKEN) + val eventName = params.getAs(Args.METRIC_EVENT) if (deliveryId == null || deliveryToken == null || eventName == null) { throw IllegalArgumentException("Missing required parameters") @@ -193,7 +137,7 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun setDeviceAttributes(params: Map) { - val attributes = params.getAs>(Keys.Tracking.ATTRIBUTES) + val attributes = params.getAs>(Args.ATTRIBUTES) if (attributes.isNullOrEmpty()) { logger.error("Device attributes are missing in params: $params") @@ -204,7 +148,7 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun setProfileAttributes(params: Map) { - val attributes = params.getAs>(Keys.Tracking.ATTRIBUTES) + val attributes = params.getAs>(Args.ATTRIBUTES) if (attributes.isNullOrEmpty()) { logger.error("Profile attributes are missing in params: $params") @@ -215,10 +159,10 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun screen(params: Map) { - val title = requireNotNull(params.getAs(Keys.Tracking.TITLE)) { + val title = requireNotNull(params.getAs(Args.TITLE)) { "Screen title is missing in params: $params" } - val properties = params.getAs>(Keys.Tracking.PROPERTIES) + val properties = params.getAs>(Args.PROPERTIES) if (properties.isNullOrEmpty()) { CustomerIO.instance().screen(title) @@ -310,4 +254,19 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { it.onDetachedFromActivity() } } + + companion object { + object Args { + const val ATTRIBUTES = "attributes" + const val DELIVERY_ID = "deliveryId" + const val DELIVERY_TOKEN = "deliveryToken" + const val METRIC_EVENT = "metricEvent" + const val NAME = "name" + const val PROPERTIES = "properties" + const val TITLE = "title" + const val TOKEN = "token" + const val TRAITS = "traits" + const val USER_ID = "userId" + } + } } diff --git a/android/src/main/kotlin/io/customer/customer_io/bridge/MethodCallExtensions.kt b/android/src/main/kotlin/io/customer/customer_io/bridge/MethodCallExtensions.kt new file mode 100644 index 0000000..9510c8f --- /dev/null +++ b/android/src/main/kotlin/io/customer/customer_io/bridge/MethodCallExtensions.kt @@ -0,0 +1,56 @@ +package io.customer.customer_io.bridge + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +/** + * Handles native method call by transforming the arguments and invoking the handler. + * + * @param result The result object to send the response back to Flutter. + * @param transformer A function to transform the incoming arguments. + * @param handler A function to handle the transformed arguments and produce a result. + * + * - If the handler returns `Unit`, it sends `true` to Flutter to avoid errors. + * - Catches and sends any exceptions as errors to Flutter. + */ +internal fun MethodCall.native( + result: MethodChannel.Result, + transformer: (Any?) -> Arguments, + handler: (Arguments) -> Result, +) = runCatching { + val args = transformer(arguments) + val response = handler(args) + // If the result is Unit, then return true to the Flutter side + // As returning Unit will throw an error on the Flutter side + result.success( + when (response) { + is Unit -> true + else -> response + } + ) +}.onFailure { ex -> + result.error(method, ex.localizedMessage, ex) +} + +/** + * Handles a native method call that requires no arguments. + * + * @param result The result object to send the response back to Flutter. + * @param handler A function to handle the call and produce a result. + */ +internal fun MethodCall.nativeNoArgs( + result: MethodChannel.Result, + handler: () -> Result, +) = native(result, { }, { handler() }) + +/** + * Handles a native method call with arguments passed as a map. + * + * @param result The result object to send the response back to Flutter. + * @param handler A function to handle the map arguments and produce a result. + */ +@Suppress("UNCHECKED_CAST") +internal fun MethodCall.nativeMapArgs( + result: MethodChannel.Result, + handler: (Map) -> Result, +) = native(result, { it as? Map ?: emptyMap() }, handler) diff --git a/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt b/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt index d891ad5..5eb193f 100644 --- a/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt +++ b/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt @@ -4,6 +4,7 @@ import io.customer.sdk.CustomerIOBuilder import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel /** @@ -40,6 +41,14 @@ internal interface NativeModuleBridge : MethodChannel.MethodCallHandler, Activit flutterCommunicationChannel.setMethodCallHandler(null) } + /** + * Handles incoming method calls from Flutter and invokes the appropriate native method handler. + * If the method is not implemented, the result is marked as not implemented. + */ + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + result.notImplemented() + } + fun configureModule(builder: CustomerIOBuilder, config: Map) override fun onDetachedFromActivity() {} diff --git a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt deleted file mode 100644 index 2afabea..0000000 --- a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.customer.customer_io.constant - -// TODO: Cleanup this file later when all commented methods are implemented -internal object Keys { - - object Methods { - const val INITIALIZE = "initialize" - const val IDENTIFY = "identify" - const val CLEAR_IDENTIFY = "clearIdentify" - const val TRACK = "track" - const val SCREEN = "screen" - const val SET_DEVICE_ATTRIBUTES = "setDeviceAttributes" - const val SET_PROFILE_ATTRIBUTES = "setProfileAttributes" - const val REGISTER_DEVICE_TOKEN = "registerDeviceToken" - const val TRACK_METRIC = "trackMetric" - const val ON_MESSAGE_RECEIVED = "onMessageReceived" - const val DISMISS_MESSAGE = "dismissMessage" - const val GET_REGISTERED_DEVICE_TOKEN = "getRegisteredDeviceToken" - } - - object Tracking { - const val USER_ID = "userId" - const val TRAITS = "traits" - const val ATTRIBUTES = "attributes" - const val EVENT_NAME = "eventName" - const val TOKEN = "token" - const val DELIVERY_ID = "deliveryId" - const val DELIVERY_TOKEN = "deliveryToken" - const val METRIC_EVENT = "metricEvent" - - const val NAME = "name" - const val PROPERTIES = "properties" - const val TITLE = "title" - } -} diff --git a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt index ece79f6..ca28b5e 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt @@ -2,8 +2,7 @@ package io.customer.customer_io.messaginginapp import android.app.Activity import io.customer.customer_io.bridge.NativeModuleBridge -import io.customer.customer_io.constant.Keys -import io.customer.customer_io.invokeNative +import io.customer.customer_io.bridge.nativeNoArgs import io.customer.customer_io.utils.getAs import io.customer.messaginginapp.MessagingInAppModuleConfig import io.customer.messaginginapp.ModuleMessagingInApp @@ -51,18 +50,15 @@ internal class CustomerIOInAppMessaging( override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - Keys.Methods.DISMISS_MESSAGE -> { - call.invokeNative(result) { - CustomerIO.instance().inAppMessaging().dismissMessage() - } - } - - else -> { - result.notImplemented() - } + "dismissMessage" -> call.nativeNoArgs(result, ::dismissMessage) + else -> super.onMethodCall(call, result) } } + private fun dismissMessage() { + CustomerIO.instance().inAppMessaging().dismissMessage() + } + /** * Adds in-app module to native Android SDK based on the configuration provided by * customer app. diff --git a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt index ae78ef8..947a0bd 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt @@ -2,8 +2,8 @@ package io.customer.customer_io.messagingpush import android.content.Context import io.customer.customer_io.bridge.NativeModuleBridge -import io.customer.customer_io.constant.Keys -import io.customer.customer_io.invokeNative +import io.customer.customer_io.bridge.nativeMapArgs +import io.customer.customer_io.bridge.nativeNoArgs import io.customer.customer_io.utils.getAs import io.customer.customer_io.utils.takeIfNotBlank import io.customer.messagingpush.CustomerIOFirebaseMessagingService @@ -34,24 +34,9 @@ internal class CustomerIOPushMessaging( override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - Keys.Methods.GET_REGISTERED_DEVICE_TOKEN -> { - call.invokeNative(result) { - return@invokeNative getRegisteredDeviceToken() - } - } - - Keys.Methods.ON_MESSAGE_RECEIVED -> { - call.invokeNative(result) { args -> - return@invokeNative onMessageReceived( - message = args.getAs>("message"), - handleNotificationTrigger = args.getAs("handleNotificationTrigger") - ) - } - } - - else -> { - result.notImplemented() - } + "getRegisteredDeviceToken" -> call.nativeNoArgs(result, ::getRegisteredDeviceToken) + "onMessageReceived" -> call.nativeMapArgs(result, ::onMessageReceived) + else -> super.onMethodCall(call, result) } } @@ -62,15 +47,14 @@ internal class CustomerIOPushMessaging( /** * Handles push notification received. This is helpful in processing push notifications * received outside the CIO SDK. - * - * @param message push payload received from FCM. - * @param handleNotificationTrigger indicating if the local notification should be triggered. */ - private fun onMessageReceived( - message: Map?, - handleNotificationTrigger: Boolean?, - ): Boolean { + private fun onMessageReceived(args: Map): Boolean { try { + // Push payload received from FCM + val message = args.getAs>("message") + // Flag to indicate if local notification should be triggered + val handleNotificationTrigger = args.getAs("handleNotificationTrigger") + if (message == null) { throw IllegalArgumentException("Message cannot be null") } From 94a65e0e7fbd94cf1b23baef0dd949cf4c9900ef Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Fri, 29 Nov 2024 00:56:05 +0500 Subject: [PATCH 27/34] chore: refactor dart call forwarding (#183) --- .../lib/src/screens/dashboard.dart | 4 +- lib/customer_io.dart | 19 +-- lib/customer_io_method_channel.dart | 152 ------------------ .../_native_constants.dart} | 29 ++-- .../customer_io_method_channel.dart | 108 +++++++++++++ .../customer_io_platform_interface.dart | 10 +- lib/extensions/map_extensions.dart | 2 +- lib/extensions/method_channel_extensions.dart | 33 ++++ lib/messaging_in_app/_native_constants.dart | 8 + lib/messaging_in_app/method_channel.dart | 21 +-- lib/messaging_push/_native_constants.dart | 11 ++ lib/messaging_push/method_channel.dart | 38 ++--- test/customer_io_method_channel_test.dart | 6 +- test/customer_io_test.dart | 2 +- test/customer_io_test.mocks.dart | 26 +-- 15 files changed, 219 insertions(+), 250 deletions(-) delete mode 100644 lib/customer_io_method_channel.dart rename lib/{customer_io_const.dart => data_pipelines/_native_constants.dart} (65%) create mode 100644 lib/data_pipelines/customer_io_method_channel.dart rename lib/{ => data_pipelines}/customer_io_platform_interface.dart (88%) create mode 100644 lib/extensions/method_channel_extensions.dart create mode 100644 lib/messaging_in_app/_native_constants.dart create mode 100644 lib/messaging_push/_native_constants.dart diff --git a/apps/amiapp_flutter/lib/src/screens/dashboard.dart b/apps/amiapp_flutter/lib/src/screens/dashboard.dart index 0c32a00..74e3532 100644 --- a/apps/amiapp_flutter/lib/src/screens/dashboard.dart +++ b/apps/amiapp_flutter/lib/src/screens/dashboard.dart @@ -53,8 +53,8 @@ class _DashboardScreenState extends State { .getBuildInfo() .then((value) => setState(() => _buildInfo = value)); - inAppMessageStreamSubscription = - CustomerIO.instance.inAppMessaging.subscribeToInAppEventListener(handleInAppEvent); + inAppMessageStreamSubscription = CustomerIO.inAppMessaging + .subscribeToInAppEventListener(handleInAppEvent); // Setup 3rd party SDK, flutter-fire. // We install this SDK into sample app to make sure the CIO SDK behaves as expected when there is another SDK installed that handles push notifications. diff --git a/lib/customer_io.dart b/lib/customer_io.dart index a231ae8..783c399 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -5,7 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'customer_io_config.dart'; import 'customer_io_enums.dart'; -import 'customer_io_platform_interface.dart'; +import 'data_pipelines/customer_io_platform_interface.dart'; import 'extensions/map_extensions.dart'; import 'messaging_in_app/platform_interface.dart'; import 'messaging_push/platform_interface.dart'; @@ -61,17 +61,13 @@ class CustomerIO { /// Access push messaging functionality static CustomerIOMessagingPushPlatform get pushMessaging { - if (_instance == null) { - throw StateError( - 'CustomerIO SDK must be initialized before accessing push module.\n' - 'Call CustomerIO.initialize() first.', - ); - } - return _instance!._pushMessaging; + return instance._pushMessaging; } /// Access in-app messaging functionality - CustomerIOMessagingInAppPlatform get inAppMessaging => _inAppMessaging; + static CustomerIOMessagingInAppPlatform get inAppMessaging { + return instance._inAppMessaging; + } /// To initialize the plugin /// @@ -106,7 +102,7 @@ class CustomerIO { /// If a profile exists, clearIdentify will stop identifying the profile. /// If no profile exists, request to clearIdentify will be ignored. void clearIdentify() { - _platform.clearIdentify(); + return _platform.clearIdentify(); } /// To track user events like loggedIn, addedItemToCart etc. @@ -157,7 +153,8 @@ class CustomerIO { /// user actions etc /// /// @param attributes additional attributes for a user profile - void setProfileAttributes({required Map attributes}) { + void setProfileAttributes( + {required Map attributes}) { return _platform.setProfileAttributes( attributes: attributes.excludeNullValues()); } diff --git a/lib/customer_io_method_channel.dart b/lib/customer_io_method_channel.dart deleted file mode 100644 index 5a303c0..0000000 --- a/lib/customer_io_method_channel.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; - -import 'package:customer_io/customer_io_enums.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'customer_io_config.dart'; -import 'customer_io_const.dart'; -import 'customer_io_platform_interface.dart'; - -/// An implementation of [CustomerIOPlatform] that uses method channels. -class CustomerIOMethodChannel extends CustomerIOPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - final methodChannel = const MethodChannel('customer_io'); - - /// To initialize the plugin - @override - Future initialize({ - required CustomerIOConfig config, - }) async { - try { - await methodChannel.invokeMethod(MethodConsts.initialize, config.toMap()); - } on PlatformException catch (exception) { - handleException(exception); - } - } - - /// Identify a person using a unique userId, eg. email id. - /// Note that you can identify only 1 profile at a time. In case, multiple - /// identifiers are attempted to be identified, then the last identified profile - /// will be removed automatically. - @override - void identify( - {required String userId, - Map traits = const {}}) async { - try { - final payload = { - TrackingConsts.userId: userId, - TrackingConsts.traits: traits - }; - methodChannel.invokeMethod(MethodConsts.identify, payload); - } on PlatformException catch (exception) { - handleException(exception); - } - } - - /// To track user events like loggedIn, addedItemToCart etc. - /// You may also track events with additional yet optional data. - @override - void track( - {required String name, - Map properties = const {}}) async { - try { - final payload = { - TrackingConsts.name: name, - TrackingConsts.properties: properties - }; - methodChannel.invokeMethod(MethodConsts.track, payload); - } on PlatformException catch (exception) { - handleException(exception); - } - } - - /// Track a push metric - @override - void trackMetric( - {required String deliveryID, - required String deviceToken, - required MetricEvent event}) async { - try { - final payload = { - TrackingConsts.deliveryId: deliveryID, - TrackingConsts.deliveryToken: deviceToken, - TrackingConsts.metricEvent: event.name, - }; - methodChannel.invokeMethod(MethodConsts.trackMetric, payload); - } on PlatformException catch (exception) { - handleException(exception); - } - } - - /// Track screen events to record the screens a user visits - @override - void screen( - {required String title, - Map properties = const {}}) async { - try { - final payload = { - TrackingConsts.title: title, - TrackingConsts.properties: properties - }; - methodChannel.invokeMethod(MethodConsts.screen, payload); - } on PlatformException catch (exception) { - handleException(exception); - } - } - - /// Register a new device token with Customer.io, associated with the current active customer. If there - /// is no active customer, this will fail to register the device - @override - void registerDeviceToken({required String deviceToken}) async { - try { - final payload = { - TrackingConsts.token: deviceToken, - }; - methodChannel.invokeMethod(MethodConsts.registerDeviceToken, payload); - } on PlatformException catch (exception) { - handleException(exception); - } - } - - /// Call this function to stop identifying a person. - @override - void clearIdentify() { - try { - methodChannel.invokeMethod(MethodConsts.clearIdentify); - } on PlatformException catch (exception) { - handleException(exception); - } - } - - /// Set custom user profile information such as user preference, specific - /// user actions etc - @override - void setProfileAttributes({required Map attributes}) { - try { - final payload = {TrackingConsts.attributes: attributes}; - methodChannel.invokeMethod(MethodConsts.setProfileAttributes, payload); - } on PlatformException catch (exception) { - handleException(exception); - } - } - - /// Use this function to send custom device attributes - /// such as app preferences, timezone etc - @override - void setDeviceAttributes({required Map attributes}) { - try { - final payload = {TrackingConsts.traits: attributes}; - methodChannel.invokeMethod(MethodConsts.setDeviceAttributes, payload); - } on PlatformException catch (exception) { - handleException(exception); - } - } - - void handleException(PlatformException exception) { - if (kDebugMode) { - print(exception); - } - } -} diff --git a/lib/customer_io_const.dart b/lib/data_pipelines/_native_constants.dart similarity index 65% rename from lib/customer_io_const.dart rename to lib/data_pipelines/_native_constants.dart index 7371cdc..5605452 100644 --- a/lib/customer_io_const.dart +++ b/lib/data_pipelines/_native_constants.dart @@ -1,33 +1,26 @@ -// TODO: Cleanup this file later when all commented methods are implemented - -class MethodConsts { - static const String initialize = "initialize"; - static const String identify = "identify"; +/// Methods specific to Data Pipelines module. +class NativeMethods { static const String clearIdentify = "clearIdentify"; - static const String track = "track"; - static const String trackMetric = "trackMetric"; + static const String identify = "identify"; + static const String initialize = "initialize"; static const String screen = "screen"; static const String setDeviceAttributes = "setDeviceAttributes"; static const String setProfileAttributes = "setProfileAttributes"; static const String registerDeviceToken = "registerDeviceToken"; - static const String onMessageReceived = "onMessageReceived"; - static const String dismissMessage = "dismissMessage"; - static const String getRegisteredDeviceToken = "getRegisteredDeviceToken"; + static const String track = "track"; + static const String trackMetric = "trackMetric"; } -class TrackingConsts { - static const String userId = "userId"; +/// Method parameters specific to DataPipelines module. +class NativeMethodParams { static const String attributes = "attributes"; - static const String traits = "traits"; - static const String eventName = "eventName"; - static const String token = "token"; static const String deliveryId = "deliveryId"; static const String deliveryToken = "deliveryToken"; static const String metricEvent = "metricEvent"; - static const String message = "message"; - static const String handleNotificationTrigger = "handleNotificationTrigger"; - static const String name = "name"; static const String properties = "properties"; static const String title = "title"; + static const String token = "token"; + static const String traits = "traits"; + static const String userId = "userId"; } diff --git a/lib/data_pipelines/customer_io_method_channel.dart b/lib/data_pipelines/customer_io_method_channel.dart new file mode 100644 index 0000000..a0469ed --- /dev/null +++ b/lib/data_pipelines/customer_io_method_channel.dart @@ -0,0 +1,108 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import '../customer_io_config.dart'; +import '../customer_io_enums.dart'; +import '../extensions/method_channel_extensions.dart'; +import '_native_constants.dart'; +import 'customer_io_platform_interface.dart'; + +/// An implementation of [CustomerIOPlatform] that uses method channels. +class CustomerIOMethodChannel extends CustomerIOPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('customer_io'); + + /// To initialize the plugin + @override + Future initialize({required CustomerIOConfig config}) { + return methodChannel.invokeNativeMethod( + NativeMethods.initialize, config.toMap()); + } + + /// Identify a person using a unique userId, eg. email id. + /// Note that you can identify only 1 profile at a time. In case, multiple + /// identifiers are attempted to be identified, then the last identified profile + /// will be removed automatically. + @override + void identify( + {required String userId, Map traits = const {}}) { + return methodChannel.invokeNativeMethodVoid(NativeMethods.identify, { + NativeMethodParams.userId: userId, + NativeMethodParams.traits: traits, + }); + } + + /// To track user events like loggedIn, addedItemToCart etc. + /// You may also track events with additional yet optional data. + @override + void track( + {required String name, Map properties = const {}}) { + return methodChannel.invokeNativeMethodVoid(NativeMethods.track, { + NativeMethodParams.name: name, + NativeMethodParams.properties: properties, + }); + } + + /// Track a push metric + @override + void trackMetric( + {required String deliveryID, + required String deviceToken, + required MetricEvent event}) { + return methodChannel.invokeNativeMethodVoid(NativeMethods.trackMetric, { + NativeMethodParams.deliveryId: deliveryID, + NativeMethodParams.deliveryToken: deviceToken, + NativeMethodParams.metricEvent: event.name, + }); + } + + /// Track screen events to record the screens a user visits + @override + void screen( + {required String title, Map properties = const {}}) { + return methodChannel.invokeNativeMethodVoid(NativeMethods.screen, { + NativeMethodParams.title: title, + NativeMethodParams.properties: properties, + }); + } + + /// Register a new device token with Customer.io, associated with the current active customer. If there + /// is no active customer, this will fail to register the device + @override + void registerDeviceToken({required String deviceToken}) { + return methodChannel + .invokeNativeMethodVoid(NativeMethods.registerDeviceToken, { + NativeMethodParams.token: deviceToken, + }); + } + + /// Call this function to stop identifying a person. + @override + void clearIdentify() { + return methodChannel.invokeNativeMethodVoid(NativeMethods.clearIdentify); + } + + /// Set custom user profile information such as user preference, specific + /// user actions etc + @override + void setProfileAttributes( + {required Map attributes}) { + return methodChannel + .invokeNativeMethodVoid(NativeMethods.setProfileAttributes, { + NativeMethodParams.attributes: attributes, + }); + } + + /// Use this function to send custom device attributes + /// such as app preferences, timezone etc + @override + void setDeviceAttributes({required Map attributes}) { + return methodChannel + .invokeNativeMethodVoid(NativeMethods.setDeviceAttributes, { + NativeMethodParams.attributes: attributes, + }); + } +} diff --git a/lib/customer_io_platform_interface.dart b/lib/data_pipelines/customer_io_platform_interface.dart similarity index 88% rename from lib/customer_io_platform_interface.dart rename to lib/data_pipelines/customer_io_platform_interface.dart index c9646b5..ad65102 100644 --- a/lib/customer_io_platform_interface.dart +++ b/lib/data_pipelines/customer_io_platform_interface.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:customer_io/customer_io_config.dart'; -import 'package:customer_io/customer_io_enums.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import '../customer_io_config.dart'; +import '../customer_io_enums.dart'; import 'customer_io_method_channel.dart'; /// The default instance of [CustomerIOPlatform] to use @@ -34,8 +34,7 @@ abstract class CustomerIOPlatform extends PlatformInterface { } void identify( - {required String userId, - Map traits = const {}}) { + {required String userId, Map traits = const {}}) { throw UnimplementedError('identify() has not been implemented.'); } @@ -68,7 +67,8 @@ abstract class CustomerIOPlatform extends PlatformInterface { throw UnimplementedError('setDeviceAttributes() has not been implemented.'); } - void setProfileAttributes({required Map attributes}) { + void setProfileAttributes( + {required Map attributes}) { throw UnimplementedError( 'setProfileAttributes() has not been implemented.'); } diff --git a/lib/extensions/map_extensions.dart b/lib/extensions/map_extensions.dart index a8baaef..5b0c5a7 100644 --- a/lib/extensions/map_extensions.dart +++ b/lib/extensions/map_extensions.dart @@ -1,5 +1,5 @@ /// Extensions for [Map] class that provide additional functionality and convenience methods. -extension CustomerIOMapExtension on Map { +extension CustomerIOMapExtensions on Map { /// Returns a new map with entries that have non-null values, excluding null values. Map excludeNullValues() { return Map.fromEntries(entries.where((entry) => entry.value != null)); diff --git a/lib/extensions/method_channel_extensions.dart b/lib/extensions/method_channel_extensions.dart new file mode 100644 index 0000000..67f9bd1 --- /dev/null +++ b/lib/extensions/method_channel_extensions.dart @@ -0,0 +1,33 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +extension CustomerIOMethodChannelExtensions on MethodChannel { + /// Invokes a native method and returns the result. + /// Logs exceptions internally without propagating them. + Future invokeNativeMethod(String method, + [Map arguments = const {}]) async { + try { + return await invokeMethod(method, arguments); + } on PlatformException catch (ex) { + // Log the exception + if (kDebugMode) { + print("Error invoking native method '$method': ${ex.message}"); + } + // Return null on failure + return null; + } catch (ex) { + // Catch any other exceptions + if (kDebugMode) { + print("Unexpected error invoking native method '$method': $ex"); + } + // Return null on unexpected errors + return null; + } + } + + /// Simplifies invoking a native method that doesn't return a value. + void invokeNativeMethodVoid(String method, + [Map arguments = const {}]) { + invokeNativeMethod(method, arguments); + } +} diff --git a/lib/messaging_in_app/_native_constants.dart b/lib/messaging_in_app/_native_constants.dart new file mode 100644 index 0000000..fa2eb31 --- /dev/null +++ b/lib/messaging_in_app/_native_constants.dart @@ -0,0 +1,8 @@ +/// Methods specific to In-App module. +class NativeMethods { + static const String dismissMessage = "dismissMessage"; +} + +/// Method parameters specific to In-App module. +class NativeMethodParams { +} diff --git a/lib/messaging_in_app/method_channel.dart b/lib/messaging_in_app/method_channel.dart index 2f32c06..1678b90 100644 --- a/lib/messaging_in_app/method_channel.dart +++ b/lib/messaging_in_app/method_channel.dart @@ -3,8 +3,9 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import '../customer_io_const.dart'; import '../customer_io_inapp.dart'; +import '../extensions/method_channel_extensions.dart'; +import '_native_constants.dart'; import 'platform_interface.dart'; /// An implementation of [CustomerIOMessagingInAppPlatform] that uses method @@ -17,12 +18,8 @@ class CustomerIOMessagingInAppMethodChannel final _inAppEventStreamController = StreamController.broadcast(); @override - void dismissMessage() async { - try { - methodChannel.invokeMethod(MethodConsts.dismissMessage); - } on PlatformException catch (e) { - handleException(e); - } + void dismissMessage() { + return methodChannel.invokeNativeMethodVoid(NativeMethods.dismissMessage); } /// Method to subscribe to the In-App event listener. @@ -33,7 +30,7 @@ class CustomerIOMessagingInAppMethodChannel StreamSubscription subscribeToInAppEventListener( void Function(InAppEvent) onEvent) { StreamSubscription subscription = - _inAppEventStreamController.stream.listen(onEvent); + _inAppEventStreamController.stream.listen(onEvent); return subscription; } @@ -45,7 +42,7 @@ class CustomerIOMessagingInAppMethodChannel Future _onMethodCall(MethodCall call) async { /// Cast the arguments to a map of strings to dynamic values. final arguments = - (call.arguments as Map).cast(); + (call.arguments as Map).cast(); switch (call.method) { case "messageShown": @@ -66,10 +63,4 @@ class CustomerIOMessagingInAppMethodChannel break; } } - - void handleException(PlatformException exception) { - if (kDebugMode) { - print(exception); - } - } } diff --git a/lib/messaging_push/_native_constants.dart b/lib/messaging_push/_native_constants.dart new file mode 100644 index 0000000..458b4be --- /dev/null +++ b/lib/messaging_push/_native_constants.dart @@ -0,0 +1,11 @@ +/// Methods specific to Push module. +class NativeMethods { + static const String getRegisteredDeviceToken = "getRegisteredDeviceToken"; + static const String onMessageReceived = "onMessageReceived"; +} + +/// Method parameters specific to Push module. +class NativeMethodParams { + static const String message = "message"; + static const String handleNotificationTrigger = "handleNotificationTrigger"; +} diff --git a/lib/messaging_push/method_channel.dart b/lib/messaging_push/method_channel.dart index 29d6c54..cfc526a 100644 --- a/lib/messaging_push/method_channel.dart +++ b/lib/messaging_push/method_channel.dart @@ -3,7 +3,8 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import '../customer_io_const.dart'; +import '../extensions/method_channel_extensions.dart'; +import '_native_constants.dart'; import 'platform_interface.dart'; /// An implementation of [CustomerIOMessagingPushPlatform] that uses method @@ -16,15 +17,8 @@ class CustomerIOMessagingPushMethodChannel @override Future getRegisteredDeviceToken() { - try { - return methodChannel - .invokeMethod(MethodConsts.getRegisteredDeviceToken) - .then((result) => result as String?); - } on PlatformException catch (exception) { - handleException(exception); - return Future.error( - exception.message ?? "Error fetching registered device token"); - } + return methodChannel + .invokeNativeMethod(NativeMethods.getRegisteredDeviceToken); } @override @@ -38,24 +32,10 @@ class CustomerIOMessagingPushMethodChannel return Future.value(true); } - try { - final arguments = { - TrackingConsts.message: message, - TrackingConsts.handleNotificationTrigger: handleNotificationTrigger, - }; - return methodChannel - .invokeMethod(MethodConsts.onMessageReceived, arguments) - .then((handled) => handled == true); - } on PlatformException catch (exception) { - handleException(exception); - return Future.error( - exception.message ?? "Error handling push notification"); - } - } - - void handleException(PlatformException exception) { - if (kDebugMode) { - print(exception); - } + return methodChannel + .invokeNativeMethod(NativeMethods.onMessageReceived, { + NativeMethodParams.message: message, + NativeMethodParams.handleNotificationTrigger: handleNotificationTrigger, + }).then((handled) => handled == true); } } diff --git a/test/customer_io_method_channel_test.dart b/test/customer_io_method_channel_test.dart index 22ba981..de459bc 100644 --- a/test/customer_io_method_channel_test.dart +++ b/test/customer_io_method_channel_test.dart @@ -1,6 +1,6 @@ import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; -import 'package:customer_io/customer_io_method_channel.dart'; +import 'package:customer_io/data_pipelines/customer_io_method_channel.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -153,11 +153,11 @@ void main() { 'setDeviceAttributes() should call platform method with correct arguments', () async { final Map args = { - 'traits': {'os': 'Android'} + 'attributes': {'os': 'Android'} }; final customerIO = CustomerIOMethodChannel(); - customerIO.setDeviceAttributes(attributes: args['traits']); + customerIO.setDeviceAttributes(attributes: args['attributes']); expectMethodInvocationArguments('setDeviceAttributes', args); }); diff --git a/test/customer_io_test.dart b/test/customer_io_test.dart index 13d7e3f..667b40f 100644 --- a/test/customer_io_test.dart +++ b/test/customer_io_test.dart @@ -1,7 +1,7 @@ import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; -import 'package:customer_io/customer_io_platform_interface.dart'; +import 'package:customer_io/data_pipelines/customer_io_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/customer_io_test.mocks.dart b/test/customer_io_test.mocks.dart index 4a35717..2250179 100644 --- a/test/customer_io_test.mocks.dart +++ b/test/customer_io_test.mocks.dart @@ -26,12 +26,12 @@ import 'customer_io_test.dart' as _i3; class _FakeStreamSubscription_0 extends _i1.SmartFake implements _i2.StreamSubscription { _FakeStreamSubscription_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } /// A class which mocks [TestCustomerIoPlatform]. @@ -72,12 +72,12 @@ class MockTestCustomerIoPlatform extends _i1.Mock ); @override void clearIdentify() => super.noSuchMethod( - Invocation.method( - #clearIdentify, - [], - ), - returnValueForMissingStub: null, - ); + Invocation.method( + #clearIdentify, + [], + ), + returnValueForMissingStub: null, + ); @override void track({ required String? name, @@ -159,7 +159,7 @@ class MockTestCustomerIoPlatform extends _i1.Mock returnValueForMissingStub: null, ); _i2.StreamSubscription subscribeToInAppEventListener( - void Function(_i6.InAppEvent)? onEvent) => + void Function(_i6.InAppEvent)? onEvent) => (super.noSuchMethod( Invocation.method( #subscribeToInAppEventListener, From 5dbbf509eca443cc0dbce72fda405349b089674f Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Fri, 29 Nov 2024 01:01:14 +0500 Subject: [PATCH 28/34] chore: refactor ios in-app classes and fix naming (#184) --- ios/Classes/CustomerIOPlugin.h | 4 + ...{CustomerIoPlugin.m => CustomerIOPlugin.m} | 6 +- ios/Classes/CustomerIoPlugin.h | 4 - .../CustomerIOInAppEventListener.swift | 32 ++++++++ .../CustomerIOInAppMessaging.swift | 22 +++++- ...ugin.swift => SwiftCustomerIOPlugin.swift} | 78 ++----------------- pubspec.yaml | 2 +- 7 files changed, 68 insertions(+), 80 deletions(-) create mode 100644 ios/Classes/CustomerIOPlugin.h rename ios/Classes/{CustomerIoPlugin.m => CustomerIOPlugin.m} (78%) delete mode 100644 ios/Classes/CustomerIoPlugin.h create mode 100644 ios/Classes/MessagingInApp/CustomerIOInAppEventListener.swift rename ios/Classes/{ => MessagingInApp}/CustomerIOInAppMessaging.swift (55%) rename ios/Classes/{SwiftCustomerIoPlugin.swift => SwiftCustomerIOPlugin.swift} (74%) diff --git a/ios/Classes/CustomerIOPlugin.h b/ios/Classes/CustomerIOPlugin.h new file mode 100644 index 0000000..c9d15df --- /dev/null +++ b/ios/Classes/CustomerIOPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface CustomerIOPlugin : NSObject +@end diff --git a/ios/Classes/CustomerIoPlugin.m b/ios/Classes/CustomerIOPlugin.m similarity index 78% rename from ios/Classes/CustomerIoPlugin.m rename to ios/Classes/CustomerIOPlugin.m index a3353ca..498db87 100644 --- a/ios/Classes/CustomerIoPlugin.m +++ b/ios/Classes/CustomerIOPlugin.m @@ -1,4 +1,4 @@ -#import "CustomerIoPlugin.h" +#import "CustomerIOPlugin.h" #if __has_include() #import #else @@ -8,8 +8,8 @@ #import "customer_io-Swift.h" #endif -@implementation CustomerIoPlugin +@implementation CustomerIOPlugin + (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftCustomerIoPlugin registerWithRegistrar:registrar]; + [SwiftCustomerIOPlugin registerWithRegistrar:registrar]; } @end diff --git a/ios/Classes/CustomerIoPlugin.h b/ios/Classes/CustomerIoPlugin.h deleted file mode 100644 index c98ba4b..0000000 --- a/ios/Classes/CustomerIoPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface CustomerIoPlugin : NSObject -@end diff --git a/ios/Classes/MessagingInApp/CustomerIOInAppEventListener.swift b/ios/Classes/MessagingInApp/CustomerIOInAppEventListener.swift new file mode 100644 index 0000000..cd179fc --- /dev/null +++ b/ios/Classes/MessagingInApp/CustomerIOInAppEventListener.swift @@ -0,0 +1,32 @@ +import CioMessagingInApp + +class CustomerIOInAppEventListener { + private let invokeDartMethod: (String, Any?) -> Void + + init(invokeDartMethod: @escaping (String, Any?) -> Void) { + self.invokeDartMethod = invokeDartMethod + } +} + +extension CustomerIOInAppEventListener: InAppEventListener { + func errorWithMessage(message: InAppMessage) { + invokeDartMethod("errorWithMessage", ["messageId": message.messageId, "deliveryId": message.deliveryId]) + } + + func messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) { + invokeDartMethod("messageActionTaken", [ + "messageId": message.messageId, + "deliveryId": message.deliveryId, + "actionValue": actionValue, + "actionName": actionName + ]) + } + + func messageDismissed(message: InAppMessage) { + invokeDartMethod("messageDismissed", ["messageId": message.messageId, "deliveryId": message.deliveryId]) + } + + func messageShown(message: InAppMessage) { + invokeDartMethod("messageShown", ["messageId": message.messageId, "deliveryId": message.deliveryId]) + } +} diff --git a/ios/Classes/CustomerIOInAppMessaging.swift b/ios/Classes/MessagingInApp/CustomerIOInAppMessaging.swift similarity index 55% rename from ios/Classes/CustomerIOInAppMessaging.swift rename to ios/Classes/MessagingInApp/CustomerIOInAppMessaging.swift index 82ec4ed..9038980 100644 --- a/ios/Classes/CustomerIOInAppMessaging.swift +++ b/ios/Classes/MessagingInApp/CustomerIOInAppMessaging.swift @@ -1,8 +1,9 @@ import Foundation import Flutter +import CioInternalCommon import CioMessagingInApp -public class CusomterIOInAppMessaging: NSObject, FlutterPlugin { +public class CustomerIOInAppMessaging: NSObject, FlutterPlugin { private var methodChannel: FlutterMethodChannel? @@ -41,4 +42,23 @@ public class CusomterIOInAppMessaging: NSObject, FlutterPlugin { methodChannel?.setMethodCallHandler(nil) methodChannel = nil } + + func configureModule(params: [String: AnyHashable]) { + if let inAppConfig = try? MessagingInAppConfigBuilder.build(from: params) { + MessagingInApp.initialize(withConfig: inAppConfig) + MessagingInApp.shared.setEventListener(CustomerIOInAppEventListener(invokeDartMethod: invokeDartMethod)) + } + } + + func invokeDartMethod(_ method: String, _ args: Any?) { + // When sending messages from native code to Flutter, it's required to do it on main thread. + // Learn more: + // * https://docs.flutter.dev/platform-integration/platform-channels#channels-and-platform-threading + // * https://linear.app/customerio/issue/MBL-358/ + DIGraphShared.shared.threadUtil.runMain { [weak self] in + guard let self else { return } + + self.methodChannel?.invokeMethod(method, arguments: args) + } + } } diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIOPlugin.swift similarity index 74% rename from ios/Classes/SwiftCustomerIoPlugin.swift rename to ios/Classes/SwiftCustomerIOPlugin.swift index f8363c1..556fc55 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIOPlugin.swift @@ -4,19 +4,19 @@ import CioDataPipelines import CioInternalCommon import CioMessagingInApp -public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { +public class SwiftCustomerIOPlugin: NSObject, FlutterPlugin { private var methodChannel: FlutterMethodChannel! - private var inAppMessagingChannelHandler: CusomterIOInAppMessaging! + private var inAppMessagingChannelHandler: CustomerIOInAppMessaging! private var messagingPushChannelHandler: CustomerIOMessagingPush! private let logger: CioInternalCommon.Logger = DIGraphShared.shared.logger public static func register(with registrar: FlutterPluginRegistrar) { - let instance = SwiftCustomerIoPlugin() + let instance = SwiftCustomerIOPlugin() instance.methodChannel = FlutterMethodChannel(name: "customer_io", binaryMessenger: registrar.messenger()) registrar.addMethodCallDelegate(instance, channel: instance.methodChannel) - instance.inAppMessagingChannelHandler = CusomterIOInAppMessaging(with: registrar) + instance.inAppMessagingChannelHandler = CustomerIOInAppMessaging(with: registrar) instance.messagingPushChannelHandler = CustomerIOMessagingPush(with: registrar) } @@ -176,15 +176,9 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { // Initialize native SDK with provided config let sdkConfigBuilder = try SDKConfigBuilder.create(from: params) CustomerIO.initialize(withConfig: sdkConfigBuilder.build()) - - if let inAppConfig = try? MessagingInAppConfigBuilder.build(from: params) { - MessagingInApp.initialize(withConfig: inAppConfig) - MessagingInApp.shared.setEventListener(CustomerIOInAppEventListener( - invokeMethod: {method,args in - self.invokeMethod(method, args) - }) - ) - } + + // Initialize in-app messaging with provided config + inAppMessagingChannelHandler.configureModule(params: params) // TODO: Initialize in-app module with given config logger.debug("Customer.io SDK initialized with config: \(params)") @@ -192,33 +186,6 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { logger.error("Initializing Customer.io SDK failed with error: \(error)") } } - - /** - Initialize in-app using customerio plugin - */ - private func initializeInApp(){ - // TODO: Fix initializeInApp implementation - /* - DispatchQueue.main.async { - MessagingInApp.shared.initialize(eventListener: CustomerIOInAppEventListener( - invokeMethod: {method,args in - self.invokeMethod(method, args) - }) - ) - } - */ - } - - func invokeMethod(_ method: String, _ args: Any?) { - // When sending messages from native code to Flutter, it's required to do it on main thread. - // Learn more: - // * https://docs.flutter.dev/platform-integration/platform-channels#channels-and-platform-threading - // * https://linear.app/customerio/issue/MBL-358/ - DispatchQueue.main.async { - self.methodChannel.invokeMethod(method, arguments: args) - } - } - } private extension FlutterMethodCall { @@ -238,34 +205,3 @@ private extension FlutterMethodCall { } } - -class CustomerIOInAppEventListener { - private let invokeMethod: (String, Any?) -> Void - - init(invokeMethod: @escaping (String, Any?) -> Void) { - self.invokeMethod = invokeMethod - } -} - -extension CustomerIOInAppEventListener: InAppEventListener { - func errorWithMessage(message: InAppMessage) { - invokeMethod("errorWithMessage", ["messageId": message.messageId, "deliveryId": message.deliveryId]) - } - - func messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) { - invokeMethod("messageActionTaken", [ - "messageId": message.messageId, - "deliveryId": message.deliveryId, - "actionValue": actionValue, - "actionName": actionName - ]) - } - - func messageDismissed(message: InAppMessage) { - invokeMethod("messageDismissed", ["messageId": message.messageId, "deliveryId": message.deliveryId]) - } - - func messageShown(message: InAppMessage) { - invokeMethod("messageShown", ["messageId": message.messageId, "deliveryId": message.deliveryId]) - } -} diff --git a/pubspec.yaml b/pubspec.yaml index c77b0b5..3718439 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,5 +41,5 @@ flutter: package: io.customer.customer_io pluginClass: CustomerIOPlugin ios: - pluginClass: CustomerIoPlugin + pluginClass: CustomerIOPlugin native_sdk_version: 3.5.1 From 1caad0355826b17b4d7b9c9a9d7f57335984d80e Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Fri, 29 Nov 2024 01:09:03 +0500 Subject: [PATCH 29/34] chore: refactor native ios call handling (#185) --- .../Bridge/CustomerIOSDKConfigMapper.swift | 2 +- .../Bridge/FlutterMethodCall+Native.swift | 63 ++++++ .../Bridge/SdkClientConfiguration.swift | 3 +- ios/Classes/Keys.swift | 42 ---- .../CustomerIOInAppMessaging.swift | 40 ++-- .../CustomerIOMessagingPush.swift | 18 +- ios/Classes/SwiftCustomerIOPlugin.swift | 214 ++++++++---------- .../Utilities/DictionaryExtensions.swift | 23 ++ 8 files changed, 203 insertions(+), 202 deletions(-) create mode 100644 ios/Classes/Bridge/FlutterMethodCall+Native.swift delete mode 100644 ios/Classes/Keys.swift create mode 100644 ios/Classes/Utilities/DictionaryExtensions.swift diff --git a/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift b/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift index aa31776..b0e6487 100644 --- a/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift +++ b/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift @@ -52,7 +52,7 @@ extension RawRepresentable where RawValue == String { thenPassItTo handler: (Transformed) -> Any, transformingBy transform: (Raw) -> Transformed? ) { - if let value = config?[self.rawValue] as? Raw, let result = transform(value) { + if let value = config?[rawValue] as? Raw, let result = transform(value) { _ = handler(result) } } diff --git a/ios/Classes/Bridge/FlutterMethodCall+Native.swift b/ios/Classes/Bridge/FlutterMethodCall+Native.swift new file mode 100644 index 0000000..c8a202c --- /dev/null +++ b/ios/Classes/Bridge/FlutterMethodCall+Native.swift @@ -0,0 +1,63 @@ +import Flutter + +extension FlutterMethodCall { + /// Handles native method call with argument transformation and response handling. + /// + /// - Parameters: + /// - result: `FlutterResult` to send the response back to Flutter. + /// - transform: Closure to transform the method arguments. + /// - handler: Closure to process the transformed arguments and return a result. + func native( + result: FlutterResult, + transform: (Any?) throws -> Arguments, + handler: (Arguments) throws -> Result + ) { + do { + let args: Arguments + do { + args = try transform(arguments) + } catch { + result(FlutterError(code: method, message: "params not available", details: nil)) + return + } + + let response = try handler(args) + if response is Void { + // If the result is Unit, then return true to the Flutter side + // As returning Void may throw an error on the Flutter side + result(true) + } else { + result(response) + } + } catch { + // Handle exceptions and send error to Flutter + result(FlutterError(code: method, message: "Unexpected error: \(error).", details: nil)) + } + } + + /// Handles native method call with no arguments. + /// + /// - Parameters: + /// - result: `FlutterResult` to send the response back to Flutter. + /// - handler: Closure to process the call and return a result. + func nativeNoArgs( + result: FlutterResult, + handler: () throws -> Result + ) { + native(result: result, transform: { _ in () }) { _ in try handler() } + } + + /// Handles native method call with map arguments. + /// + /// - Parameters: + /// - result: `FlutterResult` to send the response back to Flutter. + /// - handler: Closure to process the map arguments and return a result. + func nativeMapArgs( + result: FlutterResult, + handler: ([String: AnyHashable]) throws -> Result + ) { + native(result: result, transform: { + $0 as? [String: AnyHashable] ?? [:] + }, handler: handler) + } +} diff --git a/ios/Classes/Bridge/SdkClientConfiguration.swift b/ios/Classes/Bridge/SdkClientConfiguration.swift index a0d21cd..9cf10d2 100644 --- a/ios/Classes/Bridge/SdkClientConfiguration.swift +++ b/ios/Classes/Bridge/SdkClientConfiguration.swift @@ -5,7 +5,6 @@ import CioInternalCommon /// **Note**: Due to Swift limitations with static methods in protocol extensions, static functions /// in this extension should be called using `CustomerIOSdkClient.` to ensure correct behavior. extension SdkClient { - /// Configures and overrides the shared `SdkClient` instance with provided parameters. /// /// - Parameters: @@ -23,7 +22,7 @@ extension SdkClient { let client = CustomerIOSdkClient(source: source, sdkVersion: version) DIGraphShared.shared.override(value: client, forType: SdkClient.self) - + return DIGraphShared.shared.sdkClient } } diff --git a/ios/Classes/Keys.swift b/ios/Classes/Keys.swift deleted file mode 100644 index ff2ac32..0000000 --- a/ios/Classes/Keys.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation - -// TODO: Cleanup this file later when all commented methods are implemented -struct Keys { - - struct Methods{ - static let initialize = "initialize" - static let identify = "identify" - static let clearIdentify = "clearIdentify" - static let track = "track" - static let screen = "screen" - static let setDeviceAttributes = "setDeviceAttributes" - static let setProfileAttributes = "setProfileAttributes" - static let registerDeviceToken = "registerDeviceToken" - static let trackMetric = "trackMetric" - static let dismissMessage = "dismissMessage" - static let getRegisteredDeviceToken = "getRegisteredDeviceToken" - } - - struct Tracking { - static let userId = "userId" - static let traits = "traits" - static let attributes = "attributes" - static let eventName = "eventName" - static let token = "token" - static let deliveryId = "deliveryId" - static let deliveryToken = "deliveryToken" - static let metricEvent = "metricEvent" - - static let name = "name" - static let properties = "properties" - static let title = "title" - } - - struct Environment{ - static let siteId = "siteId" - static let apiKey = "apiKey" - static let region = "region" - static let enableInApp = "enableInApp" - } - -} diff --git a/ios/Classes/MessagingInApp/CustomerIOInAppMessaging.swift b/ios/Classes/MessagingInApp/CustomerIOInAppMessaging.swift index 9038980..80ee094 100644 --- a/ios/Classes/MessagingInApp/CustomerIOInAppMessaging.swift +++ b/ios/Classes/MessagingInApp/CustomerIOInAppMessaging.swift @@ -1,47 +1,43 @@ -import Foundation -import Flutter import CioInternalCommon import CioMessagingInApp +import Flutter +import Foundation public class CustomerIOInAppMessaging: NSObject, FlutterPlugin { - private var methodChannel: FlutterMethodChannel? - - public static func register(with registrar: FlutterPluginRegistrar) { - } - + + public static func register(with _: FlutterPluginRegistrar) {} + init(with registrar: FlutterPluginRegistrar) { super.init() - + methodChannel = FlutterMethodChannel(name: "customer_io_messaging_in_app", binaryMessenger: registrar.messenger()) - + guard let methodChannel = methodChannel else { print("customer_io_messaging_in_app methodChannel is nil") return } - + registrar.addMethodCallDelegate(self, channel: methodChannel) } - - + deinit { methodChannel?.setMethodCallHandler(nil) + methodChannel = nil } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { // Handle method calls for this method channel - switch(call.method) { - case Keys.Methods.dismissMessage: + switch call.method { + case "dismissMessage": + call.nativeNoArgs(result: result) { MessagingInApp.shared.dismissMessage() - default: - result(FlutterMethodNotImplemented) + } + + default: + result(FlutterMethodNotImplemented) } } - - func detachFromEngine() { - methodChannel?.setMethodCallHandler(nil) - methodChannel = nil - } func configureModule(params: [String: AnyHashable]) { if let inAppConfig = try? MessagingInAppConfigBuilder.build(from: params) { diff --git a/ios/Classes/MessagingPush/CustomerIOMessagingPush.swift b/ios/Classes/MessagingPush/CustomerIOMessagingPush.swift index 0cb6760..59839d4 100644 --- a/ios/Classes/MessagingPush/CustomerIOMessagingPush.swift +++ b/ios/Classes/MessagingPush/CustomerIOMessagingPush.swift @@ -5,16 +5,14 @@ import Foundation public class CustomerIOMessagingPush: NSObject, FlutterPlugin { private let channelName: String = "customer_io_messaging_push" - public static func register(with registrar: FlutterPluginRegistrar) { - } + public static func register(with _: FlutterPluginRegistrar) {} private var methodChannel: FlutterMethodChannel? init(with registrar: FlutterPluginRegistrar) { super.init() - methodChannel = FlutterMethodChannel( - name: channelName, binaryMessenger: registrar.messenger()) + methodChannel = FlutterMethodChannel(name: channelName, binaryMessenger: registrar.messenger()) guard let methodChannel = methodChannel else { print("\(channelName) methodChannel is nil") return @@ -24,22 +22,18 @@ public class CustomerIOMessagingPush: NSObject, FlutterPlugin { } deinit { - detachFromEngine() + methodChannel?.setMethodCallHandler(nil) + methodChannel = nil } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { // Handle method calls for this method channel switch call.method { - case Keys.Methods.getRegisteredDeviceToken: - result(CustomerIO.shared.registeredDeviceToken) + case "getRegisteredDeviceToken": + call.nativeNoArgs(result: result) { CustomerIO.shared.registeredDeviceToken } default: result(FlutterMethodNotImplemented) } } - - func detachFromEngine() { - methodChannel?.setMethodCallHandler(nil) - methodChannel = nil - } } diff --git a/ios/Classes/SwiftCustomerIOPlugin.swift b/ios/Classes/SwiftCustomerIOPlugin.swift index 556fc55..7027a40 100644 --- a/ios/Classes/SwiftCustomerIOPlugin.swift +++ b/ios/Classes/SwiftCustomerIOPlugin.swift @@ -1,85 +1,73 @@ -import Flutter -import UIKit import CioDataPipelines import CioInternalCommon import CioMessagingInApp +import Flutter +import UIKit public class SwiftCustomerIOPlugin: NSObject, FlutterPlugin { - private var methodChannel: FlutterMethodChannel! private var inAppMessagingChannelHandler: CustomerIOInAppMessaging! private var messagingPushChannelHandler: CustomerIOMessagingPush! + private let logger: CioInternalCommon.Logger = DIGraphShared.shared.logger - + public static func register(with registrar: FlutterPluginRegistrar) { let instance = SwiftCustomerIOPlugin() + instance.methodChannel = FlutterMethodChannel(name: "customer_io", binaryMessenger: registrar.messenger()) registrar.addMethodCallDelegate(instance, channel: instance.methodChannel) - + instance.inAppMessagingChannelHandler = CustomerIOInAppMessaging(with: registrar) instance.messagingPushChannelHandler = CustomerIOMessagingPush(with: registrar) } - + deinit { self.methodChannel.setMethodCallHandler(nil) - self.inAppMessagingChannelHandler.detachFromEngine() } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch(call.method) { - case Keys.Methods.initialize: - call.toNativeMethodCall( - result: result) { - initialize(params: $0) - } - case Keys.Methods.clearIdentify: - clearIdentify() - case Keys.Methods.track: - call.toNativeMethodCall( - result: result) { - track(params: $0) - } - case Keys.Methods.screen: - call.toNativeMethodCall( - result: result) { - screen(params: $0) - } - case Keys.Methods.identify: - call.toNativeMethodCall( - result: result) { - identify(params: $0) - } - case Keys.Methods.setProfileAttributes: - call.toNativeMethodCall(result: result) { - setProfileAttributes(params: $0) - } - case Keys.Methods.setDeviceAttributes: - call.toNativeMethodCall(result: result) { - setDeviceAttributes(params: $0) - } - case Keys.Methods.registerDeviceToken: - call.toNativeMethodCall(result: result) { - registerDeviceToken(params: $0) - } - case Keys.Methods.trackMetric: - call.toNativeMethodCall(result: result) { - trackMetric(params: $0) - } - default: - result(FlutterMethodNotImplemented) + switch call.method { + case "clearIdentify": + call.nativeNoArgs(result: result, handler: clearIdentify) + + case "identify": + call.nativeMapArgs(result: result, handler: identify) + + case "initialize": + call.nativeMapArgs(result: result, handler: initialize) + + case "setDeviceAttributes": + call.nativeMapArgs(result: result, handler: setDeviceAttributes) + + case "setProfileAttributes": + call.nativeMapArgs(result: result, handler: setProfileAttributes) + + case "registerDeviceToken": + call.nativeMapArgs(result: result, handler: registerDeviceToken) + + case "screen": + call.nativeMapArgs(result: result, handler: screen) + + case "track": + call.nativeMapArgs(result: result, handler: track) + + case "trackMetric": + call.nativeMapArgs(result: result, handler: trackMetric) + + default: + result(FlutterMethodNotImplemented) } } - - private func identify(params : Dictionary){ - - let userId = params[Keys.Tracking.userId] as? String - let traits = params[Keys.Tracking.traits] as? Dictionary ?? [:] - - if userId == nil && traits.isEmpty { + + private func identify(params: [String: AnyHashable]) { + let userId = params[Args.userId] as? String + let traits = params[Args.traits] as? [String: AnyHashable] ?? [:] + + if userId == nil, traits.isEmpty { logger.error("Please provide either an ID or traits to identify.") return } - + if let userId = userId, !traits.isEmpty { CustomerIO.shared.identify(userId: userId, traits: traits) } else if let userId = userId { @@ -88,88 +76,74 @@ public class SwiftCustomerIOPlugin: NSObject, FlutterPlugin { CustomerIO.shared.profileAttributes = traits } } - + private func clearIdentify() { CustomerIO.shared.clearIdentify() } - - private func track(params : Dictionary) { - guard let name = params[Keys.Tracking.name] as? String else { - logger.error("Missing event name in: \(params) for key: \(Keys.Tracking.name)") + + private func track(params: [String: AnyHashable]) { + guard let name: String = params.require(Args.name) else { return } - - guard let properties = params[Keys.Tracking.properties] as? Dictionary else { + + guard let properties = params[Args.properties] as? [String: AnyHashable] else { CustomerIO.shared.track(name: name) return } - + CustomerIO.shared.track(name: name, properties: properties) } - - func screen(params : Dictionary) { - guard let title = params[Keys.Tracking.title] as? String else { - logger.error("Missing screen title in: \(params) for key: \(Keys.Tracking.title)") + + func screen(params: [String: AnyHashable]) { + guard let title: String = params.require(Args.title) else { return } - - guard let properties = params[Keys.Tracking.properties] as? Dictionary else { + + guard let properties = params[Args.properties] as? [String: AnyHashable] else { CustomerIO.shared.screen(title: title) return } - + CustomerIO.shared.screen(title: title, properties: properties) } - - - private func setDeviceAttributes(params : Dictionary){ - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary - else { - logger.error("Missing device attributes in: \(params) for key: \(Keys.Tracking.attributes)") + + private func setDeviceAttributes(params: [String: AnyHashable]) { + guard let attributes: [String: AnyHashable] = params.require(Args.attributes) else { return } - + CustomerIO.shared.deviceAttributes = attributes } - - private func setProfileAttributes(params : Dictionary){ - guard let attributes = params[Keys.Tracking.attributes] as? Dictionary - else { - logger.error("Missing profile attributes in: \(params) for key: \(Keys.Tracking.attributes)") + + private func setProfileAttributes(params: [String: AnyHashable]) { + guard let attributes: [String: AnyHashable] = params.require(Args.attributes) else { return } - + CustomerIO.shared.profileAttributes = attributes } - - private func registerDeviceToken(params : Dictionary){ - guard let token = params[Keys.Tracking.token] as? String - else { - logger.error("Missing token in: \(params) for key: \(Keys.Tracking.token)") + + private func registerDeviceToken(params: [String: AnyHashable]) { + guard let token: String = params.require(Args.token) else { return } - + CustomerIO.shared.registerDeviceToken(token) } - - private func trackMetric(params : Dictionary){ - - guard let deliveryId = params[Keys.Tracking.deliveryId] as? String, - let deviceToken = params[Keys.Tracking.deliveryToken] as? String, - let metricEvent = params[Keys.Tracking.metricEvent] as? String, + + private func trackMetric(params: [String: AnyHashable]) { + guard let deliveryId: String = params.require(Args.deliveryId), + let deviceToken: String = params.require(Args.deliveryToken), + let metricEvent: String = params.require(Args.metricEvent), let event = Metric.getEvent(from: metricEvent) else { - logger.error("Missing required parameters in: \(params)") return } - - CustomerIO.shared.trackMetric(deliveryID: deliveryId, - event: event, - deviceToken: deviceToken) - + + CustomerIO.shared.trackMetric(deliveryID: deliveryId, event: event, deviceToken: deviceToken) } - - private func initialize(params : Dictionary){ + + private func initialize(params: [String: AnyHashable]) { do { // Configure and override SdkClient for Flutter before initializing native SDK CustomerIOSdkClient.configure(using: params) @@ -179,29 +153,23 @@ public class SwiftCustomerIOPlugin: NSObject, FlutterPlugin { // Initialize in-app messaging with provided config inAppMessagingChannelHandler.configureModule(params: params) - - // TODO: Initialize in-app module with given config + logger.debug("Customer.io SDK initialized with config: \(params)") } catch { logger.error("Initializing Customer.io SDK failed with error: \(error)") } } -} -private extension FlutterMethodCall { - func toNativeMethodCall( result: @escaping FlutterResult, - method: (_: Dictionary) throws -> Void) { - do { - if let attributes = self.arguments as? Dictionary { - print(attributes) - try method(attributes) - result(true) - } else{ - result(FlutterError(code: self.method, message: "params not available", details: nil)) - } - } catch { - result(FlutterError(code: self.method, message: "Unexpected error: \(error).", details: nil)) - } - + enum Args { + static let attributes = "attributes" + static let deliveryId = "deliveryId" + static let deliveryToken = "deliveryToken" + static let metricEvent = "metricEvent" + static let name = "name" + static let properties = "properties" + static let title = "title" + static let token = "token" + static let traits = "traits" + static let userId = "userId" } } diff --git a/ios/Classes/Utilities/DictionaryExtensions.swift b/ios/Classes/Utilities/DictionaryExtensions.swift new file mode 100644 index 0000000..25a4696 --- /dev/null +++ b/ios/Classes/Utilities/DictionaryExtensions.swift @@ -0,0 +1,23 @@ +import CioInternalCommon + +extension Dictionary where Key == String, Value == AnyHashable { + /// Retrieves a value from dictionary for given key and casts it to given type. + /// + /// - Parameters: + /// - key: Key to look up in the dictionary. + /// - onFailure: An optional closure executed if the key is missing or the value cannot be cast. + /// Defaults to `nil`, in which case no additional action is taken. + /// - Returns: The value associated with the key, cast to given type, or `nil` if the key is missing or the cast fails. + func require(_ key: String, onFailure: (() -> Void)? = nil) -> T? { + guard let value = self[key] as? T else { + // Using if-else for increased readability + if let onFailure = onFailure { + onFailure() + } else { + DIGraphShared.shared.logger.error("Missing or invalid value for key: \(key) in: \(self)") + } + return nil + } + return value + } +} From a2d084b3ac101bcf3585a8c63378e91d20abd87d Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Fri, 29 Nov 2024 18:14:25 +0500 Subject: [PATCH 30/34] chore: rename event listener for in-app (#186) --- .../lib/src/screens/dashboard.dart | 2 +- lib/messaging_in_app/method_channel.dart | 2 +- lib/messaging_in_app/platform_interface.dart | 4 +-- test/customer_io_test.mocks.dart | 27 ------------------- 4 files changed, 4 insertions(+), 31 deletions(-) diff --git a/apps/amiapp_flutter/lib/src/screens/dashboard.dart b/apps/amiapp_flutter/lib/src/screens/dashboard.dart index 74e3532..a61fde6 100644 --- a/apps/amiapp_flutter/lib/src/screens/dashboard.dart +++ b/apps/amiapp_flutter/lib/src/screens/dashboard.dart @@ -54,7 +54,7 @@ class _DashboardScreenState extends State { .then((value) => setState(() => _buildInfo = value)); inAppMessageStreamSubscription = CustomerIO.inAppMessaging - .subscribeToInAppEventListener(handleInAppEvent); + .subscribeToEventsListener(handleInAppEvent); // Setup 3rd party SDK, flutter-fire. // We install this SDK into sample app to make sure the CIO SDK behaves as expected when there is another SDK installed that handles push notifications. diff --git a/lib/messaging_in_app/method_channel.dart b/lib/messaging_in_app/method_channel.dart index 1678b90..5da6860 100644 --- a/lib/messaging_in_app/method_channel.dart +++ b/lib/messaging_in_app/method_channel.dart @@ -27,7 +27,7 @@ class CustomerIOMessagingInAppMethodChannel /// The `onEvent` function will be called whenever an In-App event occurs. /// Returns a [StreamSubscription] object that can be used to unsubscribe from the stream. @override - StreamSubscription subscribeToInAppEventListener( + StreamSubscription subscribeToEventsListener( void Function(InAppEvent) onEvent) { StreamSubscription subscription = _inAppEventStreamController.stream.listen(onEvent); diff --git a/lib/messaging_in_app/platform_interface.dart b/lib/messaging_in_app/platform_interface.dart index d123ac0..701b7f2 100644 --- a/lib/messaging_in_app/platform_interface.dart +++ b/lib/messaging_in_app/platform_interface.dart @@ -31,9 +31,9 @@ abstract class CustomerIOMessagingInAppPlatform extends PlatformInterface { throw UnimplementedError('dismissMessage() has not been implemented.'); } - StreamSubscription subscribeToInAppEventListener( + StreamSubscription subscribeToEventsListener( void Function(InAppEvent) onEvent) { throw UnimplementedError( - 'subscribeToInAppEventListener() has not been implemented.'); + 'subscribeToEventsListener() has not been implemented.'); } } diff --git a/test/customer_io_test.mocks.dart b/test/customer_io_test.mocks.dart index 2250179..051a2d2 100644 --- a/test/customer_io_test.mocks.dart +++ b/test/customer_io_test.mocks.dart @@ -7,7 +7,6 @@ import 'dart:async' as _i2; import 'package:customer_io/customer_io_config.dart' as _i4; import 'package:customer_io/customer_io_enums.dart' as _i5; -import 'package:customer_io/customer_io_inapp.dart' as _i6; import 'package:mockito/mockito.dart' as _i1; import 'customer_io_test.dart' as _i3; @@ -23,17 +22,6 @@ import 'customer_io_test.dart' as _i3; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeStreamSubscription_0 extends _i1.SmartFake - implements _i2.StreamSubscription { - _FakeStreamSubscription_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [TestCustomerIoPlatform]. /// /// See the documentation for Mockito's code generation for more information. @@ -158,19 +146,4 @@ class MockTestCustomerIoPlatform extends _i1.Mock ), returnValueForMissingStub: null, ); - _i2.StreamSubscription subscribeToInAppEventListener( - void Function(_i6.InAppEvent)? onEvent) => - (super.noSuchMethod( - Invocation.method( - #subscribeToInAppEventListener, - [onEvent], - ), - returnValue: _FakeStreamSubscription_0( - this, - Invocation.method( - #subscribeToInAppEventListener, - [onEvent], - ), - ), - ) as _i2.StreamSubscription); } From 1fcc0103d0f0bc33800460cc060b764c2d598acd Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Tue, 3 Dec 2024 16:53:46 +0500 Subject: [PATCH 31/34] chore: native SDKs version bump --- android/build.gradle | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index c8691f1..ecc0112 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -58,7 +58,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // Customer.io SDK - def cioVersion = "4.3.0" + def cioVersion = "4.4.1" implementation "io.customer.android:datapipelines:$cioVersion" implementation "io.customer.android:messaging-push-fcm:$cioVersion" implementation "io.customer.android:messaging-in-app:$cioVersion" diff --git a/pubspec.yaml b/pubspec.yaml index 3718439..769a8f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,4 +42,4 @@ flutter: pluginClass: CustomerIOPlugin ios: pluginClass: CustomerIOPlugin - native_sdk_version: 3.5.1 + native_sdk_version: 3.6.0 From f29a417d43b9fdfffd50713d10e44bc479965392 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 11 Dec 2024 01:51:22 +0500 Subject: [PATCH 32/34] chore: cleanup files and fixes --- .github/workflows/build.yml | 46 ------------------- .../customer/customer_io/CustomerIOPlugin.kt | 4 +- .../main/res/values/customer_io_config.xml | 2 +- lib/customer_io.dart | 4 +- lib/customer_io_plugin_version.dart | 2 +- pubspec.yaml | 2 +- 6 files changed, 8 insertions(+), 52 deletions(-) delete mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 2138ae2..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Flutter Build - -on: - push: - branches: [main] -# Cancel jobs and just run the last one -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -defaults: - run: - working-directory: apps/amiapp_flutter - -jobs: - build_ios: - name: Build iOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - name: Install CLI tools used in CI script - run: | - brew install sd # used in CI script as an easier to use sed CLI. Replaces text in files. - - name: Setup workspace credentials in iOS environment files - run: | - cp "ios/Env.swift.example" "ios/Env.swift" - sd 'siteId: String = ".*"' "siteId: String = \"${{ secrets.CUSTOMERIO_AMIAPP_FLUTTER_WORKSPACE_SITE_ID }}\"" "ios/Env.swift" - sd 'siteId: String = ".*"' "siteId: String = \"${{ secrets.CUSTOMERIO_AMIAPP_FLUTTER_WORKSPACE_API_KEY }}\"" "ios/Env.swift" - - uses: ./.github/actions/setup-flutter - - run: flutter build ios --release --no-codesign - - build_android: - name: Build Android - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - uses: ./.github/actions/setup-flutter - - run: flutter build apk --release diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt index f99720b..773960c 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt @@ -17,6 +17,7 @@ import io.customer.sdk.core.util.Logger import io.customer.sdk.data.model.Region import io.customer.sdk.events.Metric import io.customer.sdk.events.TrackMetric +import io.customer.sdk.events.serializedName import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -125,7 +126,8 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { throw IllegalArgumentException("Missing required parameters") } - val event = Metric.valueOf(eventName) + val event = Metric.values().find { it.serializedName.equals(eventName, true) } + ?: throw IllegalArgumentException("Invalid metric event name") CustomerIO.instance().trackMetric( event = TrackMetric.Push( diff --git a/android/src/main/res/values/customer_io_config.xml b/android/src/main/res/values/customer_io_config.xml index 20b8417..325d399 100644 --- a/android/src/main/res/values/customer_io_config.xml +++ b/android/src/main/res/values/customer_io_config.xml @@ -9,5 +9,5 @@ This will be updated by update-version script with Flutter package version whenever new version of Flutter package is released --> - 1.5.2 + 2.0.0 diff --git a/lib/customer_io.dart b/lib/customer_io.dart index 783c399..8f803be 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -61,12 +61,12 @@ class CustomerIO { /// Access push messaging functionality static CustomerIOMessagingPushPlatform get pushMessaging { - return instance._pushMessaging; + return _instance?._pushMessaging ?? CustomerIOMessagingPushPlatform.instance; } /// Access in-app messaging functionality static CustomerIOMessagingInAppPlatform get inAppMessaging { - return instance._inAppMessaging; + return _instance?._inAppMessaging ?? CustomerIOMessagingInAppPlatform.instance; } /// To initialize the plugin diff --git a/lib/customer_io_plugin_version.dart b/lib/customer_io_plugin_version.dart index 98d7f0f..e691fbf 100755 --- a/lib/customer_io_plugin_version.dart +++ b/lib/customer_io_plugin_version.dart @@ -1,2 +1,2 @@ // Don't modify this line - it's automatically updated -const version = "1.5.2"; +const version = "2.0.0"; diff --git a/pubspec.yaml b/pubspec.yaml index 769a8f8..f4d743d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: customer_io description: An official flutter plugin for Customer.io, an automated messaging platform for tech-savvy marketers. -version: 1.5.2 +version: 2.0.0 homepage: https://customer.io repository: https://github.com/customerio/customerio-flutter From 0f4b3080ae0d347e7a36a7c37d6e42f2a7d38200 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 11 Dec 2024 02:05:48 +0500 Subject: [PATCH 33/34] chore: updated push config value --- lib/config/customer_io_config.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config/customer_io_config.dart b/lib/config/customer_io_config.dart index d2b7715..0e4ee82 100644 --- a/lib/config/customer_io_config.dart +++ b/lib/config/customer_io_config.dart @@ -48,7 +48,7 @@ class CustomerIOConfig { 'flushAt': flushAt, 'flushInterval': flushInterval, 'inApp': inAppConfig?.toMap(), - 'pushConfig': pushConfig.toMap(), + 'push': pushConfig.toMap(), 'version': version, 'source': source }; From 061be53c97bd0536d278f3af210af1260242e781 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 11 Dec 2024 02:10:38 +0500 Subject: [PATCH 34/34] chore: fix tests --- pubspec.yaml | 2 +- test/customer_io_config_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index f4d743d..769a8f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: customer_io description: An official flutter plugin for Customer.io, an automated messaging platform for tech-savvy marketers. -version: 2.0.0 +version: 1.5.2 homepage: https://customer.io repository: https://github.com/customerio/customerio-flutter diff --git a/test/customer_io_config_test.dart b/test/customer_io_config_test.dart index 3a38281..13c1165 100644 --- a/test/customer_io_config_test.dart +++ b/test/customer_io_config_test.dart @@ -103,7 +103,7 @@ void main() { 'flushAt': 25, 'flushInterval': 55, 'inApp': inAppConfig.toMap(), - 'pushConfig': pushConfig.toMap(), + 'push': pushConfig.toMap(), 'version': config.version, 'source': config.source, };