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, + ); + }); }); }); }