From b4a4feddb943972f635826d55d31a4cb14904210 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Tue, 7 Jan 2025 01:13:06 +0500 Subject: [PATCH] feat: add option to disable screen view usage (#190) --- android/build.gradle | 2 +- .../customer/customer_io/CustomerIOPlugin.kt | 3 + apps/amiapp_flutter/.gitignore | 2 + apps/amiapp_flutter/ios/Podfile | 2 +- apps/amiapp_flutter/lib/src/customer_io.dart | 1 + apps/amiapp_flutter/lib/src/data/config.dart | 40 ++++---- .../lib/src/screens/settings.dart | 17 ++++ .../lib/src/utils/extensions.dart | 4 + .../lib/src/widgets/settings_form_field.dart | 96 +++++++++++++++++++ apps/amiapp_flutter/pubspec.lock | 32 +++---- .../Bridge/CustomerIOSDKConfigMapper.swift | 2 + lib/config/customer_io_config.dart | 3 + lib/customer_io_enums.dart | 5 + pubspec.yaml | 2 +- test/customer_io_config_test.dart | 5 + 15 files changed, 176 insertions(+), 40 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index ecc0112..5c953d7 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.4.1" + def cioVersion = "4.5.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 773960c..83d2254 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt @@ -9,6 +9,7 @@ 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 +import io.customer.datapipelines.config.ScreenView import io.customer.sdk.CustomerIO import io.customer.sdk.CustomerIOBuilder import io.customer.sdk.core.di.SDKComponent @@ -182,6 +183,7 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val logLevelRawValue = args.getAs("logLevel") val regionRawValue = args.getAs("region") val givenRegion = regionRawValue.let { Region.getRegion(it) } + val screenViewRawValue = args.getAs("screenViewUse") CustomerIOBuilder( applicationContext = application, @@ -189,6 +191,7 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { ).apply { logLevelRawValue?.let { logLevel(CioLogLevel.getLogLevel(it)) } regionRawValue?.let { region(givenRegion) } + screenViewRawValue?.let { screenViewUse(ScreenView.getScreenView(it)) } args.getAs("migrationSiteId")?.let(::migrationSiteId) args.getAs("autoTrackDeviceAttributes")?.let(::autoTrackDeviceAttributes) diff --git a/apps/amiapp_flutter/.gitignore b/apps/amiapp_flutter/.gitignore index 13bb5c5..162cf31 100644 --- a/apps/amiapp_flutter/.gitignore +++ b/apps/amiapp_flutter/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ fastlane/report.xml diff --git a/apps/amiapp_flutter/ios/Podfile b/apps/amiapp_flutter/ios/Podfile index b9d338d..b1304ba 100644 --- a/apps/amiapp_flutter/ios/Podfile +++ b/apps/amiapp_flutter/ios/Podfile @@ -1,7 +1,7 @@ # ------------- # This code only used internally for Customer.io testing require 'open-uri' -IO.copy_stream(URI.open('https://raw.githubusercontent.com/customerio/customerio-ios/v2/scripts/cocoapods_override_sdk.rb'), "/tmp/override_cio_sdk.rb") +IO.copy_stream(URI.open('https://raw.githubusercontent.com/customerio/customerio-ios/main/scripts/cocoapods_override_sdk.rb'), "/tmp/override_cio_sdk.rb") load "/tmp/override_cio_sdk.rb" # end of internal Customer.io testing code # ------------- diff --git a/apps/amiapp_flutter/lib/src/customer_io.dart b/apps/amiapp_flutter/lib/src/customer_io.dart index 9c8d398..451b4c3 100644 --- a/apps/amiapp_flutter/lib/src/customer_io.dart +++ b/apps/amiapp_flutter/lib/src/customer_io.dart @@ -72,6 +72,7 @@ class CustomerIOSDK extends ChangeNotifier { cdnHost: _sdkConfig?.cdnHost, flushAt: _sdkConfig?.flushAt, flushInterval: _sdkConfig?.flushInterval?.toInt(), + screenViewUse: _sdkConfig?.screenViewUse, inAppConfig: inAppConfig, ), ); diff --git a/apps/amiapp_flutter/lib/src/data/config.dart b/apps/amiapp_flutter/lib/src/data/config.dart index 21450b0..23ff968 100644 --- a/apps/amiapp_flutter/lib/src/data/config.dart +++ b/apps/amiapp_flutter/lib/src/data/config.dart @@ -15,6 +15,7 @@ class CustomerIOSDKConfig { final String? cdnHost; final int? flushAt; final int? flushInterval; + final ScreenView? screenViewUse; final InAppConfig? inAppConfig; final PushConfig pushConfig; @@ -29,6 +30,7 @@ class CustomerIOSDKConfig { this.cdnHost, this.flushAt, this.flushInterval, + this.screenViewUse, this.inAppConfig, PushConfig? pushConfig, }) : pushConfig = pushConfig ?? PushConfig(); @@ -45,10 +47,11 @@ 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; + final region = + prefs.getEnumValueFromPrefs(_PreferencesKey.region, Region.values); + final screenViewUse = prefs.getEnumValueFromPrefs( + _PreferencesKey.screenViewUse, ScreenView.values); + return CustomerIOSDKConfig( cdpApiKey: cdpApiKey, migrationSiteId: prefs.getString(_PreferencesKey.migrationSiteId), @@ -63,27 +66,11 @@ class CustomerIOSDKConfig { cdnHost: prefs.getString(_PreferencesKey.cdnHost), flushAt: prefs.getInt(_PreferencesKey.flushAt), flushInterval: prefs.getInt(_PreferencesKey.flushInterval), + screenViewUse: screenViewUse, inAppConfig: InAppConfig( siteId: prefs.getString(_PreferencesKey.migrationSiteId) ?? ""), ); } - - 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 { @@ -128,8 +115,18 @@ extension ConfigurationPreferencesExtensions on SharedPreferences { result = result && await setOrRemoveInt( _PreferencesKey.flushInterval, config.flushInterval); + result = result && + await setOrRemoveString( + _PreferencesKey.screenViewUse, config.screenViewUse?.name); return result; } + + T? getEnumValueFromPrefs(String key, List values) { + final storedValue = getString(key); + if (storedValue == null) return null; + + return values.firstWhere((e) => e.name == storedValue); + } } class _PreferencesKey { @@ -143,4 +140,5 @@ class _PreferencesKey { static const cdnHost = 'CDN_HOST'; static const flushAt = 'FLUSH_AT'; static const flushInterval = 'FLUSH_INTERVAL'; + static const screenViewUse = 'SCREEN_VIEW_USE'; } diff --git a/apps/amiapp_flutter/lib/src/screens/settings.dart b/apps/amiapp_flutter/lib/src/screens/settings.dart index a9d4629..e7853c5 100644 --- a/apps/amiapp_flutter/lib/src/screens/settings.dart +++ b/apps/amiapp_flutter/lib/src/screens/settings.dart @@ -1,5 +1,6 @@ import 'package:customer_io/config/in_app_config.dart'; import 'package:customer_io/customer_io.dart'; +import 'package:customer_io/customer_io_enums.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; @@ -44,6 +45,7 @@ class _SettingsScreenState extends State { late final TextEditingController _flushAtValueController; late final TextEditingController _flushIntervalValueController; + late ScreenView _screenViewUse; late bool _featureTrackScreens; late bool _featureTrackDeviceAttributes; late bool _featureDebugMode; @@ -65,6 +67,7 @@ class _SettingsScreenState extends State { TextEditingController(text: cioConfig?.flushAt?.toString()); _flushIntervalValueController = TextEditingController(text: cioConfig?.flushInterval?.toString()); + _screenViewUse = cioConfig?.screenViewUse ?? ScreenView.all; _featureTrackScreens = cioConfig?.screenTrackingEnabled ?? true; _featureTrackDeviceAttributes = cioConfig?.autoTrackDeviceAttributes ?? true; @@ -85,6 +88,7 @@ class _SettingsScreenState extends State { cdnHost: _cdnHostValueController.text.trim().nullIfEmpty(), flushAt: _flushAtValueController.text.trim().toIntOrNull(), flushInterval: _flushIntervalValueController.text.trim().toIntOrNull(), + screenViewUse: _screenViewUse, screenTrackingEnabled: _featureTrackScreens, autoTrackDeviceAttributes: _featureTrackDeviceAttributes, debugModeEnabled: _featureDebugMode, @@ -119,6 +123,7 @@ class _SettingsScreenState extends State { _flushAtValueController.text = defaultConfig.flushAt?.toString() ?? ''; _flushIntervalValueController.text = defaultConfig.flushInterval?.toString() ?? ''; + _screenViewUse = defaultConfig.screenViewUse ?? ScreenView.all; _featureTrackScreens = defaultConfig.screenTrackingEnabled; _featureTrackDeviceAttributes = defaultConfig.autoTrackDeviceAttributes ?? true; @@ -287,6 +292,18 @@ class _SettingsScreenState extends State { updateState: ((value) => setState(() => _featureDebugMode = value)), ), + const SizedBox(height: 8), + ChoiceSettingsFormField( + labelText: 'ScreenView Use', + semanticsLabel: 'ScreenView options', + value: _screenViewUse, + options: ScreenView.values, + updateState: (ScreenView selected) { + setState(() { + _screenViewUse = selected; + }); + }, + ), ], ), ), diff --git a/apps/amiapp_flutter/lib/src/utils/extensions.dart b/apps/amiapp_flutter/lib/src/utils/extensions.dart index 7d573cf..12a7df2 100644 --- a/apps/amiapp_flutter/lib/src/utils/extensions.dart +++ b/apps/amiapp_flutter/lib/src/utils/extensions.dart @@ -106,6 +106,10 @@ extension AmiAppStringExtensions on String { (min == null || value >= min) && (max == null || value <= max); } + + String capitalize() { + return '${this[0].toUpperCase()}${substring(1)}'; + } } extension LocationExtensions on GoRouter { 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 79d068d..47cd29d 100644 --- a/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart +++ b/apps/amiapp_flutter/lib/src/widgets/settings_form_field.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../components/text_field_label.dart'; +import '../utils/extensions.dart'; class TextSettingsFormField extends StatelessWidget { const TextSettingsFormField({ @@ -121,3 +122,98 @@ class SwitchSettingsFormField extends StatelessWidget { ); } } + +class ChoiceSettingsFormField extends StatelessWidget { + const ChoiceSettingsFormField({ + super.key, + required this.labelText, + required this.semanticsLabel, + required this.value, + required this.updateState, + required this.options, + this.enabled = true, + }); + + final String labelText; + final String semanticsLabel; + final T value; + final List options; + final void Function(T) updateState; + final bool enabled; + + @override + Widget build(BuildContext context) { + const double defaultBorderRadius = 8.0; + + return UnmanagedRestorationScope( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + labelText, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: enabled + ? Theme.of(context).colorScheme.surface + : Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(defaultBorderRadius), + ), + child: Row( + children: options.map((option) { + final bool isSelected = option == value; + return Expanded( + child: GestureDetector( + onTap: enabled + ? () { + updateState(option); + } + : null, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12.0), + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context) + .colorScheme + .surfaceContainerHighest, + borderRadius: BorderRadius.only( + topLeft: option == options.first + ? const Radius.circular(defaultBorderRadius) + : Radius.zero, + bottomLeft: option == options.first + ? const Radius.circular(defaultBorderRadius) + : Radius.zero, + topRight: option == options.last + ? const Radius.circular(defaultBorderRadius) + : Radius.zero, + bottomRight: option == options.last + ? const Radius.circular(defaultBorderRadius) + : Radius.zero, + ), + ), + child: Text( + option.toString().split('.').last.capitalize(), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: isSelected + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ), + ); + }).toList(), + ), + ), + ], + ), + ); + } +} diff --git a/apps/amiapp_flutter/pubspec.lock b/apps/amiapp_flutter/pubspec.lock index 519fb57..927397a 100644 --- a/apps/amiapp_flutter/pubspec.lock +++ b/apps/amiapp_flutter/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" crypto: dependency: transitive description: @@ -103,7 +103,7 @@ packages: path: "../.." relative: true source: path - version: "1.5.2" + version: "2.0.0" dbus: dependency: transitive description: @@ -299,18 +299,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -539,7 +539,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -552,10 +552,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -568,10 +568,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -584,10 +584,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timezone: dependency: transitive description: @@ -616,10 +616,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" web: dependency: transitive description: diff --git a/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift b/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift index b0e6487..55d88c3 100644 --- a/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift +++ b/ios/Classes/Bridge/CustomerIOSDKConfigMapper.swift @@ -16,6 +16,7 @@ extension SDKConfigBuilder { case flushInterval case apiHost case cdnHost + case screenViewUse } @available(iOSApplicationExtension, unavailable) @@ -34,6 +35,7 @@ extension SDKConfigBuilder { 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) + Config.screenViewUse.ifNotNil(in: config, thenPassItTo: builder.screenViewUse) { ScreenView.getScreenView($0) } return builder } diff --git a/lib/config/customer_io_config.dart b/lib/config/customer_io_config.dart index 0e4ee82..c5d2150 100644 --- a/lib/config/customer_io_config.dart +++ b/lib/config/customer_io_config.dart @@ -17,6 +17,7 @@ class CustomerIOConfig { final String? cdnHost; final int? flushAt; final int? flushInterval; + final ScreenView? screenViewUse; final InAppConfig? inAppConfig; final PushConfig pushConfig; @@ -31,6 +32,7 @@ class CustomerIOConfig { this.cdnHost, this.flushAt, this.flushInterval, + this.screenViewUse, this.inAppConfig, PushConfig? pushConfig, }) : pushConfig = pushConfig ?? PushConfig(); @@ -47,6 +49,7 @@ class CustomerIOConfig { 'cdnHost': cdnHost, 'flushAt': flushAt, 'flushInterval': flushInterval, + 'screenViewUse': screenViewUse?.name, 'inApp': inAppConfig?.toMap(), 'push': pushConfig.toMap(), 'version': version, diff --git a/lib/customer_io_enums.dart b/lib/customer_io_enums.dart index fe875d7..609cbf2 100644 --- a/lib/customer_io_enums.dart +++ b/lib/customer_io_enums.dart @@ -35,3 +35,8 @@ enum PushClickBehaviorAndroid { final String rawValue; } + +/// Enum class to define how CustomerIO SDK should handle screen view events. +/// all - to send screen events to destinations for analytics purposes and to display in-app messages. +/// inApp - to only display in-app messages and not send screen events to destinations. +enum ScreenView { all, inApp } diff --git a/pubspec.yaml b/pubspec.yaml index f4d743d..24c64f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,4 +42,4 @@ flutter: pluginClass: CustomerIOPlugin ios: pluginClass: CustomerIOPlugin - native_sdk_version: 3.6.0 + native_sdk_version: 3.7.1 diff --git a/test/customer_io_config_test.dart b/test/customer_io_config_test.dart index 13c1165..b0df26d 100644 --- a/test/customer_io_config_test.dart +++ b/test/customer_io_config_test.dart @@ -19,6 +19,7 @@ void main() { expect(config.cdnHost, isNull); expect(config.flushAt, isNull); expect(config.flushInterval, isNull); + expect(config.screenViewUse, isNull); expect(config.inAppConfig, isNull); @@ -50,6 +51,7 @@ void main() { cdnHost: 'https://cdn.example.com', flushAt: 15, flushInterval: 45, + screenViewUse: ScreenView.all, inAppConfig: inAppConfig, pushConfig: pushConfig, ); @@ -63,6 +65,7 @@ void main() { expect(config.cdnHost, 'https://cdn.example.com'); expect(config.flushAt, 15); expect(config.flushInterval, 45); + expect(config.screenViewUse, ScreenView.all); expect(config.inAppConfig, inAppConfig); expect(config.pushConfig, pushConfig); expect(config.source, 'Flutter'); @@ -87,6 +90,7 @@ void main() { cdnHost: 'https://cdn.example.com', flushAt: 25, flushInterval: 55, + screenViewUse: ScreenView.inApp, inAppConfig: inAppConfig, pushConfig: pushConfig, ); @@ -102,6 +106,7 @@ void main() { 'cdnHost': 'https://cdn.example.com', 'flushAt': 25, 'flushInterval': 55, + 'screenViewUse': 'inApp', 'inApp': inAppConfig.toMap(), 'push': pushConfig.toMap(), 'version': config.version,