diff --git a/.gitignore b/.gitignore index 9c83653..2b52f74 100644 --- a/.gitignore +++ b/.gitignore @@ -596,4 +596,7 @@ healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ -# End of https://www.gitignore.io/api/git,dart,flutter,intellij,webstorm,visualstudio \ No newline at end of file +# Remove env files +*.env + +# End of https://www.gitignore.io/api/git,dart,flutter,intellij,webstorm,visualstudio diff --git a/android/build.gradle b/android/build.gradle index 4e01a53..95f0a02 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -44,12 +44,21 @@ android { defaultConfig { minSdkVersion 21 } + + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs += [ + '-Xopt-in=kotlin.RequiresOptIn', + '-Xopt-in=io.customer.base.internal.InternalCustomerIOApi', + ] + } + } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // Customer.io SDK - def cioVersion = "3.2.0-alpha.1" + def cioVersion = "3.3.0-beta.1" implementation "io.customer.android:tracking:$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 43e0df0..e3c9a04 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt @@ -1,18 +1,27 @@ 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.extension.* +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.sdk.CustomerIO +import io.customer.sdk.CustomerIOConfig import io.customer.sdk.CustomerIOShared -import io.customer.sdk.data.store.Client +import io.customer.sdk.data.model.Region +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 import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -22,17 +31,34 @@ 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 { +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 /// when the Flutter Engine is detached from the Activity private lateinit var flutterCommunicationChannel: MethodChannel private lateinit var context: Context + private var activity: Activity? = null private val logger: Logger get() = CustomerIOShared.instance().diStaticGraph.logger + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + this.activity = 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 = @@ -41,15 +67,14 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler { } private fun MethodCall.toNativeMethodCall( - result: Result, - performAction: (params: Map) -> Unit + 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); + result.error(this.method, e.localizedMessage, null) } } @@ -105,7 +130,7 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler { CustomerIO.instance().identify(identifier, attributes) } - fun track(params: Map) { + private fun track(params: Map) { val name = params.getString(Keys.Tracking.EVENT_NAME) val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: emptyMap() @@ -117,21 +142,20 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler { } } - fun setDeviceAttributes(params: Map) { + private fun setDeviceAttributes(params: Map) { val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: emptyMap() CustomerIO.instance().deviceAttributes = attributes } - fun setProfileAttributes(params: Map) { - val attributes = - params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: return + private fun setProfileAttributes(params: Map) { + val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: return CustomerIO.instance().profileAttributes = attributes } - fun screen(params: Map) { + private fun screen(params: Map) { val name = params.getString(Keys.Tracking.EVENT_NAME) val attributes = params.getProperty>(Keys.Tracking.ATTRIBUTES) ?: emptyMap() @@ -149,24 +173,28 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler { val apiKey = configData.getString(Keys.Environment.API_KEY) val region = configData.getProperty( Keys.Environment.REGION - )?.takeIfNotBlank().toRegion() - val organizationId = configData.getProperty( - Keys.Environment.ORGANIZATION_ID )?.takeIfNotBlank() + val enableInApp = configData.getProperty( + Keys.Environment.ENABLE_IN_APP + ) CustomerIO.Builder( siteId = siteId, apiKey = apiKey, - region = region, + region = Region.getRegion(region), appContext = application, + config = configData ).apply { - setClient(client = getUserAgentClient(packageConfig = configData)) - setupConfig(configData) addCustomerIOModule(module = configureModuleMessagingPushFCM(configData)) - if (!organizationId.isNullOrBlank()) { + if (enableInApp == true) { addCustomerIOModule( module = ModuleMessagingInApp( - organizationId = organizationId, + config = MessagingInAppModuleConfig.Builder() + .setEventListener(CustomerIOInAppEventListener { method, args -> + this@CustomerIoPlugin.activity?.runOnUiThread { + flutterCommunicationChannel.invokeMethod(method, args) + } + }).build(), ) ) } @@ -177,41 +205,55 @@ class CustomerIoPlugin : FlutterPlugin, MethodCallHandler { private fun configureModuleMessagingPushFCM(config: Map?): ModuleMessagingPushFCM { return ModuleMessagingPushFCM( config = MessagingPushModuleConfig.Builder().apply { - config?.getProperty(Keys.Config.AUTO_TRACK_PUSH_EVENTS)?.let { value -> - setAutoTrackPushEvents(autoTrackPushEvents = value) - } + config?.getProperty(CustomerIOConfig.Companion.Keys.AUTO_TRACK_PUSH_EVENTS) + ?.let { value -> + setAutoTrackPushEvents(autoTrackPushEvents = value) + } }.build(), ) } - private fun getUserAgentClient(packageConfig: Map?): Client { - val sourceSDKVersion = packageConfig?.getProperty( - Keys.PackageConfig.SOURCE_SDK_VERSION - )?.takeIfNotBlank() ?: "n/a" - return Client.Flutter(sdkVersion = sourceSDKVersion) + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + flutterCommunicationChannel.setMethodCallHandler(null) + } +} + +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 + ) + ) } - private fun CustomerIO.Builder.setupConfig(config: Map?): CustomerIO.Builder { - if (config == null) return this + 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 + ) + ) + } - val logLevel = config.getProperty(Keys.Config.LOG_LEVEL).toCIOLogLevel() - setLogLevel(level = logLevel) - config.getProperty(Keys.Config.TRACKING_API_URL)?.takeIfNotBlank()?.let { value -> - setTrackingApiURL(value) - } - config.getProperty(Keys.Config.AUTO_TRACK_DEVICE_ATTRIBUTES)?.let { value -> - autoTrackDeviceAttributes(shouldTrackDeviceAttributes = value) - } - config.getProperty(Keys.Config.BACKGROUND_QUEUE_MIN_NUMBER_OF_TASKS)?.let { value -> - setBackgroundQueueMinNumberOfTasks(backgroundQueueMinNumberOfTasks = value) - } - config.getProperty(Keys.Config.BACKGROUND_QUEUE_SECONDS_DELAY)?.let { value -> - setBackgroundQueueSecondsDelay(backgroundQueueSecondsDelay = value) - } - return this + override fun messageDismissed(message: InAppMessage) { + invokeMethod( + "messageDismissed", mapOf( + "messageId" to message.messageId, "deliveryId" to message.deliveryId + ) + ) } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - flutterCommunicationChannel.setMethodCallHandler(null) + override fun messageShown(message: InAppMessage) { + invokeMethod( + "messageShown", mapOf( + "messageId" to message.messageId, "deliveryId" to message.deliveryId + ) + ) } } 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 c005b03..deb19aa 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 @@ -22,19 +22,7 @@ internal object Keys { const val SITE_ID = "siteId" const val API_KEY = "apiKey" const val REGION = "region" - const val ORGANIZATION_ID = "organizationId" + const val ENABLE_IN_APP = "enableInApp" } - object Config { - const val TRACKING_API_URL = "trackingApiUrl" - const val AUTO_TRACK_PUSH_EVENTS = "autoTrackPushEvents" - const val AUTO_TRACK_DEVICE_ATTRIBUTES = "autoTrackDeviceAttributes" - const val LOG_LEVEL = "logLevel" - const val BACKGROUND_QUEUE_MIN_NUMBER_OF_TASKS = "backgroundQueueMinNumberOfTasks" - const val BACKGROUND_QUEUE_SECONDS_DELAY = "backgroundQueueSecondsDelay" - } - - object PackageConfig { - const val SOURCE_SDK_VERSION = "version" - } } diff --git a/android/src/main/kotlin/io/customer/customer_io/extension/MapExt.kt b/android/src/main/kotlin/io/customer/customer_io/extension/MapExt.kt deleted file mode 100644 index d547d7e..0000000 --- a/android/src/main/kotlin/io/customer/customer_io/extension/MapExt.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.customer.customer_io.extension - -import io.customer.sdk.CustomerIOShared - -@Throws(IllegalArgumentException::class) -internal inline fun Map.getPropertyUnsafe(key: String): T { - val property = get(key) - - if (property !is T) { - throw IllegalArgumentException( - "Invalid value provided for key: $key, value $property must be of type ${T::class.java.simpleName}" - ) - } - return property -} - -internal inline fun Map.getProperty(key: String): T? = try { - getPropertyUnsafe(key) -} catch (ex: IllegalArgumentException) { - CustomerIOShared.instance().diStaticGraph.logger.error( - ex.message ?: "getProperty($key) -> IllegalArgumentException" - ) - null -} - -@Throws(IllegalArgumentException::class) -internal fun Map.getString(key: String): String = try { - getPropertyUnsafe(key).takeIfNotBlank() ?: throw IllegalArgumentException( - "Invalid value provided for $key, must not be blank" - ) -} catch (ex: IllegalArgumentException) { - CustomerIOShared.instance().diStaticGraph.logger.error( - ex.message ?: "getString($key) -> IllegalArgumentException" - ) - throw ex -} diff --git a/android/src/main/kotlin/io/customer/customer_io/extension/StringExt.kt b/android/src/main/kotlin/io/customer/customer_io/extension/StringExt.kt deleted file mode 100644 index b820b74..0000000 --- a/android/src/main/kotlin/io/customer/customer_io/extension/StringExt.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.customer.customer_io.extension - -internal fun String.takeIfNotBlank(): String? = takeIf { it.isNotBlank() } diff --git a/android/src/main/kotlin/io/customer/customer_io/extension/TypeConversion.kt b/android/src/main/kotlin/io/customer/customer_io/extension/TypeConversion.kt deleted file mode 100644 index ce59b89..0000000 --- a/android/src/main/kotlin/io/customer/customer_io/extension/TypeConversion.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.customer.customer_io.extension - -import io.customer.sdk.data.model.Region -import io.customer.sdk.util.CioLogLevel - -internal fun String?.toRegion(fallback: Region = Region.US): Region { - return if (this.isNullOrBlank()) fallback - else listOf( - Region.US, - Region.EU, - ).find { value -> value.code.equals(this, ignoreCase = true) } ?: fallback -} - -internal fun String?.toCIOLogLevel(fallback: CioLogLevel = CioLogLevel.NONE): CioLogLevel { - return CioLogLevel.values().find { value -> value.name.equals(this, ignoreCase = true) } - ?: fallback -} diff --git a/example/android/build.gradle b/example/android/build.gradle index 83ae220..7eb8a22 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,13 +6,14 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { + mavenLocal() google() mavenCentral() } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index cc5527d..d995037 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jun 23 08:50:38 CEST 2017 +#Fri Jan 20 23:52:06 EST 2023 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/example/credentials.env b/example/credentials.env new file mode 100644 index 0000000..5e00610 --- /dev/null +++ b/example/credentials.env @@ -0,0 +1,2 @@ +siteId=SITE_ID +apiKey=API_KEY diff --git a/example/ios/Podfile b/example/ios/Podfile index 10f3c9b..1342c81 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,3 +1,27 @@ +### +# How to configure what version of the iOS SDK is installed on your machine. +# +# This is the order of priority for installing: +# +# 1. Install local version of CIO SDK, if the source code is found on your local computer. +# To enable this feature, pass environment variable with the path set to where on your computer the iOS SDK source code is. +install_ios_sdk_local_path = ENV['INSTALL_IOS_SDK_LOCAL'] || nil +# 2. Install from a CIO SDK git branch. +# To install from a branch, pass environment varible with branch name +install_ios_sdk_branch_name = ENV['INSTALL_IOS_SDK_BRANCH'] || nil +# 3. Install version of CIO SDK specified in `/ios/customer_io.podspec` file. +# +### + +# All of the CIO pods that are listed as dependencies in `/ios/customer_io.podspec` need to be listed here in this array: +all_ios_pods_flutter_plugin_needs = [ + 'CustomerIOCommon', + 'CustomerIOTracking', + 'CustomerIOMessagingInApp', + 'CustomerIOMessagingPush', + 'CustomerIOMessagingPushFCM' +] + # Uncomment this line to define a global platform for your project platform :ios, '13.0' @@ -31,6 +55,24 @@ target 'Runner' do use_frameworks! use_modular_headers! + if install_ios_sdk_local_path != nil then + puts "" + puts "⚠️ Installing local version of the iOS SDK. Path of iOS SDK: #{install_ios_sdk_local_path}" + puts "" + + all_ios_pods_flutter_plugin_needs.each { |podname| + pod podname, :path => install_ios_sdk_local_path + } + elsif install_ios_sdk_branch_name != nil then + puts "" + puts "⚠️ Installing CIO iOS SDK from git branch #{install_ios_sdk_branch_name}" + puts "" + + all_ios_pods_flutter_plugin_needs.each { |podname| + pod podname, :git => "https://github.com/customerio/customerio-ios.git", :branch => install_ios_sdk_branch_name + } + end + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end @@ -38,4 +80,4 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end -end +end \ No newline at end of file diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index c521215..5d005fb 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -199,6 +199,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -235,6 +236,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -356,7 +358,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 2YC97BQN3N; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -374,7 +376,7 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -489,7 +491,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 2YC97BQN3N; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -513,7 +515,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 2YC97BQN3N; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 72989c4..e8a4973 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -45,5 +45,12 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + + UIApplicationSceneManifest + + UISceneConfigurations + + diff --git a/example/lib/main.dart b/example/lib/main.dart index e295fbf..91e60cc 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,16 +1,21 @@ +import 'dart:async'; + import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; +import 'package:customer_io/customer_io_inapp.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await dotenv.load(fileName: "credentials.env"); await CustomerIO.initialize( - config: CustomerIOConfig( - siteId: "YOUR_SITE_ID", - apiKey: "YOUR_API_KEY", - ), - ); + config: CustomerIOConfig( + siteId: dotenv.get('siteId', fallback: 'YOUR_SITE_ID'), + apiKey: dotenv.get('apiKey', fallback: 'YOUR_API_KEY'), + enableInApp: true)); runApp(const MyApp()); } @@ -23,12 +28,21 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { + late StreamSubscription inAppMessageStreamSubscription; + + @override + void dispose() { + /// Stop listening to streams + inAppMessageStreamSubscription.cancel(); + super.dispose(); + } + @override void initState() { super.initState(); CustomerIO.identify( - identifier: "flutter-example", - attributes: {"name": "Flutter CIO", "email": "example@flutter.io"}); + identifier: "fel-ios", + attributes: {"email": "fel-ios@flutter.io"}); } @override @@ -85,6 +99,20 @@ class _MyAppState extends State { ), ), const Spacer(), + Center( + child: ElevatedButton( + child: const Text('SUBSCRIBE IN-APP MESSAGE EVENTS'), + onPressed: () { + inAppMessageStreamSubscription = + CustomerIO.subscribeToInAppEventListener((InAppEvent event) { + if (kDebugMode) { + print("Received event: ${event.eventType.name}"); + } + }); + }, + ), + ), + const Spacer(), Center( child: ElevatedButton( child: const Text('CLEAR IDENTITY'), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8c66117..14853f9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -17,6 +17,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_dotenv: ^5.0.2 + customer_io: # When depending on this package from a real application you should use: @@ -53,8 +55,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg + assets: + - credentials.env # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see diff --git a/ios/Classes/CustomerIOExtensions.swift b/ios/Classes/CustomerIOExtensions.swift deleted file mode 100644 index bc2657b..0000000 --- a/ios/Classes/CustomerIOExtensions.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// CustomerIOExtensions.swift -// customer_io -// -// Created by ShahrozAli on 11/11/22. -// - -import Foundation -import CioTracking - -extension Region{ - static func from(regionStr : String) -> Region { - switch regionStr { - case "us" : - return Region.US - case "eu" : - return Region.EU - default: - return Region.US - } - } -} - -extension CioLogLevel { - static func from(for level : String) -> CioLogLevel { - switch level { - case "none": - return .none - case "error": - return .error - case "info": - return .info - case "debug": - return .debug - default: - return .error - } - } -} diff --git a/ios/Classes/Keys.swift b/ios/Classes/Keys.swift index 7bcc3f4..82a2db2 100644 --- a/ios/Classes/Keys.swift +++ b/ios/Classes/Keys.swift @@ -1,10 +1,3 @@ -// -// Keys.swift -// customer_io -// -// Created by ShahrozAli on 11/11/22. -// - import Foundation struct Keys { @@ -30,20 +23,7 @@ struct Keys { static let siteId = "siteId" static let apiKey = "apiKey" static let region = "region" - static let organizationId = "organizationId" - } - - struct Config{ - static let trackingApiUrl = "trackingApiUrl" - static let autoTrackDeviceAttributes = "autoTrackDeviceAttributes" - static let logLevel = "logLevel" - static let autoTrackPushEvents = "autoTrackPushEvents" - static let backgroundQueueMinNumberOfTasks = "backgroundQueueMinNumberOfTasks" - static let backgroundQueueSecondsDelay = "backgroundQueueSecondsDelay" + static let enableInApp = "enableInApp" } - struct PackageConfig{ - static let version = "version" - static let sdkVersion = "sdkVersion" - } } diff --git a/ios/Classes/SwiftCustomerIoPlugin.swift b/ios/Classes/SwiftCustomerIoPlugin.swift index 25f49a0..99b0b4d 100644 --- a/ios/Classes/SwiftCustomerIoPlugin.swift +++ b/ios/Classes/SwiftCustomerIoPlugin.swift @@ -6,10 +6,16 @@ import CioMessagingInApp public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { + private var methodChannel: FlutterMethodChannel! + public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "customer_io", binaryMessenger: registrar.messenger()) let instance = SwiftCustomerIoPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) + instance.methodChannel = FlutterMethodChannel(name: "customer_io", binaryMessenger: registrar.messenger()) + registrar.addMethodCallDelegate(instance, channel: instance.methodChannel) + } + + deinit { + self.methodChannel.setMethodCallHandler(nil) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -48,7 +54,7 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { result(FlutterMethodNotImplemented) } } - + private func identify(params : Dictionary){ guard let identifier = params[Keys.Tracking.identifier] as? String else { @@ -117,44 +123,46 @@ public class SwiftCustomerIoPlugin: NSObject, FlutterPlugin { private func initialize(params : Dictionary){ guard let siteId = params[Keys.Environment.siteId] as? String, let apiKey = params[Keys.Environment.apiKey] as? String, - let region = params[Keys.Environment.region] as? String, - let organizationId = params[Keys.Environment.organizationId] as? String + let regionStr = params[Keys.Environment.region] as? String else { return } - CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: Region.from(regionStr: region)){ + let region = Region.getRegion(from: regionStr) + + CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: region){ config in - config._sdkWrapperConfig = self.getUserAgent(params: params) - config.autoTrackDeviceAttributes = params[Keys.Config.autoTrackDeviceAttributes] as! Bool - config.logLevel = CioLogLevel.from(for: params[Keys.Config.logLevel] as! String) - config.autoTrackPushEvents = params[Keys.Config.autoTrackPushEvents] as! Bool - config.backgroundQueueMinNumberOfTasks = params[Keys.Config.backgroundQueueMinNumberOfTasks] as! Int - config.backgroundQueueSecondsDelay = params[Keys.Config.backgroundQueueSecondsDelay] as! Seconds - if let trackingApiUrl = params[Keys.Config.trackingApiUrl] as? String, !trackingApiUrl.isEmpty { - config.trackingApiUrl = trackingApiUrl - } + config.modify(params: params) } - if organizationId != "" { - initializeInApp(organizationId: organizationId) + + if let enableInApp = params[Keys.Environment.enableInApp] as? Bool { + if enableInApp{ + initializeInApp() + } } - } - - private func getUserAgent(params : Dictionary) -> SdkWrapperConfig{ - let version = params[Keys.PackageConfig.version] as? String ?? "n/a" - let sdkSource = SdkWrapperConfig.Source.flutter - return SdkWrapperConfig(source: sdkSource, version: version ) + } /** Initialize in-app using customerio plugin */ - private func initializeInApp(organizationId: String){ + private func initializeInApp(){ DispatchQueue.main.async { - MessagingInApp.shared.initialize(organizationId: organizationId) + MessagingInApp.shared.initialize(eventListener: CustomerIOInAppEventListener( + invokeMethod: {method,args in + self.invokeMethodInBackground(method, args) + }) + ) + } + } + + func invokeMethodInBackground(_ method: String, _ args: Any?) { + DispatchQueue.global(qos: .background).async { + self.methodChannel.invokeMethod(method, arguments: args) } } + } private extension FlutterMethodCall { @@ -173,5 +181,35 @@ 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/ios/customer_io.podspec b/ios/customer_io.podspec index e80b05a..6ae0092 100755 --- a/ios/customer_io.podspec +++ b/ios/customer_io.podspec @@ -13,10 +13,10 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.platform = :ios, '13.0' - s.dependency "CustomerIOTracking", '~> 2.0.1' - s.dependency "CustomerIOMessagingInApp", '~> 2.0.1' + s.dependency "CustomerIOTracking", '~> 2.1.0-beta.1' + s.dependency "CustomerIOMessagingInApp", '~> 2.1.0-beta.1' # 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 \ No newline at end of file +end diff --git a/lib/customer_io.dart b/lib/customer_io.dart index 74412cb..4274323 100644 --- a/lib/customer_io.dart +++ b/lib/customer_io.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'customer_io_config.dart'; +import 'customer_io_inapp.dart'; import 'customer_io_platform_interface.dart'; class CustomerIO { @@ -70,4 +73,15 @@ class CustomerIO { static void setProfileAttributes({required Map attributes}) { return _customerIO.setProfileAttributes(attributes: attributes); } + + /// 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. + static StreamSubscription subscribeToInAppEventListener( + void Function(InAppEvent) onEvent) { + return _customerIO.subscribeToInAppEventListener(onEvent); + } } diff --git a/lib/customer_io_config.dart b/lib/customer_io_config.dart index e753ef8..b4fcbf9 100644 --- a/lib/customer_io_config.dart +++ b/lib/customer_io_config.dart @@ -6,7 +6,6 @@ class CustomerIOConfig { final String apiKey; Region region; String organizationId; - CioLogLevel logLevel; bool autoTrackDeviceAttributes; String trackingApiUrl; @@ -14,19 +13,23 @@ class CustomerIOConfig { int backgroundQueueMinNumberOfTasks; double backgroundQueueSecondsDelay; + bool enableInApp; + String version; CustomerIOConfig( {required this.siteId, required this.apiKey, this.region = Region.us, - this.organizationId = "", + @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.enableInApp = false, this.version = ""}); Map toMap() { @@ -41,7 +44,9 @@ class CustomerIOConfig { 'autoTrackPushEvents': autoTrackPushEvents, 'backgroundQueueMinNumberOfTasks': backgroundQueueMinNumberOfTasks, 'backgroundQueueSecondsDelay': backgroundQueueSecondsDelay, + 'enableInApp': enableInApp, 'version': version, + 'source': "Flutter" }; } } diff --git a/lib/customer_io_inapp.dart b/lib/customer_io_inapp.dart new file mode 100644 index 0000000..2c9d36b --- /dev/null +++ b/lib/customer_io_inapp.dart @@ -0,0 +1,85 @@ +class InAppMessage { + /// Unique identifier for the message + final String messageId; + + /// Optional identifier for the delivery of this message + final String? deliveryId; + + InAppMessage({ + required this.messageId, + this.deliveryId, + }); +} + +/// Abstract class that defines callbacks for In-App events. +abstract class InAppEventListener { + /// Callback for when an In-App message is shown. + /// + /// [message] - The InAppMessage object. + void messageShown(InAppMessage message); + + /// Callback for when an In-App message is dismissed. + /// + /// [message] - The InAppMessage object. + void messageDismissed(InAppMessage message); + + /// Callback for when an error occurs with an In-App message. + /// + /// [message] - The InAppMessage object. + void errorWithMessage(InAppMessage message); + + /// Callback for when an action is taken on an In-App message. + /// + /// [message] - The InAppMessage object. + /// [actionValue] - The value of the action taken on the InAppMessage. + /// [actionName] - The name of the action taken on the InAppMessage. + void messageActionTaken( + InAppMessage message, String actionValue, String actionName); +} + +/// Class that holds information about the InAppEvent. +class InAppEvent { + /// The InAppMessage object. + final InAppMessage message; + + /// The value of the action taken on the InAppMessage. + final String? actionValue; + + /// The name of the action taken on the InAppMessage. + final String? actionName; + + /// The type of event. + final EventType eventType; + + /// Constructor for the InAppEvent. + /// + /// [eventType] - The type of event. Required. + /// [message] - The InAppMessage object. Required. + /// [actionValue] - The value of the action taken on the InAppMessage. + /// [actionName] - The name of the action taken on the InAppMessage. + InAppEvent({ + required this.eventType, + required this.message, + this.actionValue, + this.actionName, + }); + + /// Constructor for creating an InAppEvent from a map. + /// + /// [type] - The type of event. + /// [map] - The map containing the values for creating the InAppEvent. + InAppEvent.fromMap(EventType type, Map map) + : eventType = type, + actionValue = map['actionValue'], + actionName = map['actionName'], + message = InAppMessage( + messageId: map['messageId'], deliveryId: map['deliveryId']); +} + +/// Enum to represent the type of event. +enum EventType { + messageShown, + messageDismissed, + errorWithMessage, + messageActionTaken +} diff --git a/lib/customer_io_method_channel.dart b/lib/customer_io_method_channel.dart index 743d7be..9799cd2 100644 --- a/lib/customer_io_method_channel.dart +++ b/lib/customer_io_method_channel.dart @@ -1,7 +1,11 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; 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'; import 'customer_io_plugin_version.dart'; @@ -11,6 +15,50 @@ 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({ @@ -18,6 +66,9 @@ class CustomerIOMethodChannel extends CustomerIOPlatform { }) 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) { if (kDebugMode) { diff --git a/lib/customer_io_platform_interface.dart b/lib/customer_io_platform_interface.dart index 9490b28..29f0d70 100644 --- a/lib/customer_io_platform_interface.dart +++ b/lib/customer_io_platform_interface.dart @@ -1,6 +1,9 @@ +import 'dart:async'; + import 'package:customer_io/customer_io_config.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 @@ -58,4 +61,10 @@ 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/test/customer_io_test.dart b/test/customer_io_test.dart index 4a0d87e..bef96f8 100644 --- a/test/customer_io_test.dart +++ b/test/customer_io_test.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:customer_io/customer_io_config.dart'; +import 'package:customer_io/customer_io_inapp.dart'; import 'package:customer_io/customer_io_method_channel.dart'; import 'package:customer_io/customer_io_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -46,9 +49,18 @@ class MockCustomerIoPlatform {required String name, Map attributes = const {}}) { // TODO: implement track } + + @override + StreamSubscription subscribeToInAppEventListener( + void Function(InAppEvent p1) onEvent) { + // TODO: implement subscribeToInAppEventListener + throw UnimplementedError(); + } } void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final CustomerIOPlatform initialPlatform = CustomerIOPlatform.instance; test('$CustomerIOMethodChannel is the default instance', () {