From d3096b5ee748bacb4609133e268eba0ec95ec832 Mon Sep 17 00:00:00 2001 From: Ahmed-Ali Date: Mon, 17 Jun 2024 00:32:15 +0400 Subject: [PATCH 1/2] chore: rewrite CustomerIO wrapper --- ios/wrappers/CioRctWrapper.mm | 19 ++ ios/wrappers/CioRctWrapper.swift | 119 +++++++++++ ios/wrappers/utils/CioConfigUtils.swift | 50 +++++ ios/wrappers/{ => utils}/Constants.swift | 0 src/CustomerioConfig.tsx | 44 ---- src/CustomerioEnum.tsx | 22 -- src/CustomerioTracking.tsx | 192 ------------------ src/cio-config.ts | 20 ++ src/customer-io.ts | 99 +++++++++ ...OInAppMessaging.ts => customerio-inapp.ts} | 0 ...rIOPushMessaging.ts => customerio-push.ts} | 0 src/index.ts | 2 + src/index.tsx | 26 --- 13 files changed, 309 insertions(+), 284 deletions(-) create mode 100644 ios/wrappers/CioRctWrapper.mm create mode 100644 ios/wrappers/CioRctWrapper.swift create mode 100644 ios/wrappers/utils/CioConfigUtils.swift rename ios/wrappers/{ => utils}/Constants.swift (100%) delete mode 100644 src/CustomerioConfig.tsx delete mode 100644 src/CustomerioEnum.tsx delete mode 100644 src/CustomerioTracking.tsx create mode 100644 src/cio-config.ts create mode 100644 src/customer-io.ts rename src/{CustomerIOInAppMessaging.ts => customerio-inapp.ts} (100%) rename src/{CustomerIOPushMessaging.ts => customerio-push.ts} (100%) create mode 100644 src/index.ts delete mode 100644 src/index.tsx diff --git a/ios/wrappers/CioRctWrapper.mm b/ios/wrappers/CioRctWrapper.mm new file mode 100644 index 00000000..81a6c67e --- /dev/null +++ b/ios/wrappers/CioRctWrapper.mm @@ -0,0 +1,19 @@ +#import + + +@interface RCT_EXTERN_REMAP_MODULE(NativeCustomerIO, CioRctWrapper, NSObject) + +RCT_EXTERN_METHOD(initialize:(id)config logLevel:(NSString *)logLevel) +RCT_EXTERN_METHOD(identify:(NSString *)identify traits:(NSDictionary *)traits) +RCT_EXTERN_METHOD(clearIdentify) +RCT_EXTERN_METHOD(track:(NSString *)name properties:(NSDictionary *)properties) +RCT_EXTERN_METHOD(screen:(NSString *)title category: (NSString *)category properties:(NSDictionary *)) +RCT_EXTERN_METHOD(setProfileAttributes: (NSDictionary *)attributes) +RCT_EXTERN_METHOD(setDeviceAttributes: (NSDictionary *)attributes) + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +@end diff --git a/ios/wrappers/CioRctWrapper.swift b/ios/wrappers/CioRctWrapper.swift new file mode 100644 index 00000000..edff39eb --- /dev/null +++ b/ios/wrappers/CioRctWrapper.swift @@ -0,0 +1,119 @@ +import CioDataPipelines +import CioMessagingInApp +import CioMessagingPush +import UserNotifications +import React + +func flush() { +#if DEBUG + CustomerIO.shared.flush() +#endif +} + +@objc(CioRctWrapper) +class CioRctWrapper: NSObject { + + @objc var moduleRegistry: RCTModuleRegistry! + + private var logger: CioLogger! + + @objc + func initialize(_ configJson: AnyObject, logLevel: String) { + do { + logger = CioLoggerWrapper.getInstance(moduleRegistry: moduleRegistry, logLevel: CioLogLevel(rawValue: logLevel) ?? .none) + + logger.debug("Initializing CIO with config: \(configJson)") + let rtcConfig = try RCTCioConfig.from(configJson) + let cioInitConfig = cioInitializeConfig(from: rtcConfig, logLevel: logLevel) + CustomerIO.initialize(withConfig: cioInitConfig.cdp) + if let inAppConfig = cioInitConfig.inApp { + // In app initializes UIView(s) which would crash if run from non-UI queues + DispatchQueue.main.async { + MessagingInApp.initialize(withConfig: inAppConfig) + MessagingInApp.shared.setEventListener(self) + } + } + flush() + } catch { + logger.error("Couldn't initialize CustomerIO: \(error)") + } + } + + @objc + func identify(_ userId: String? = nil, traits: [String: Any]? = nil) { + if let userId, let traits { + CustomerIO.shared.identify(userId: userId, traits: traits) + } else if let userId { + CustomerIO.shared.identify(userId: userId, traits: traits) + } else { + logger.error("CustomerIO.identify called without an ID or traits") + } + flush() + } + + @objc + func clearIdentify() { + CustomerIO.shared.clearIdentify() + flush() + } + + @objc + func track(_ name: String, properties: [String: Any]?) { + CustomerIO.shared.track(name: name, properties: properties) + flush() + } + + @objc + func screen(_ title: String, category: String?, properties: [String: Any]?) { + CustomerIO.shared.screen(title: title, category: category, properties: properties) + flush() + } + + @objc + func setProfileAttributes(_ attrs: [String: Any]) { + CustomerIO.shared.profileAttributes = attrs + flush() + } + + @objc + func setDeviceAttributes(_ attrs: [String: Any]) { + CustomerIO.shared.deviceAttributes = attrs + flush() + } +} + +extension CioRctWrapper: InAppEventListener { + private func sendEvent(eventType: String, message: InAppMessage, actionValue: String? = nil, actionName: String? = nil) { + var body = [ + CustomerioConstants.eventType: eventType, + CustomerioConstants.messageId: message.messageId, + CustomerioConstants.deliveryId: message.deliveryId + ] + if let actionValue = actionValue { + body[CustomerioConstants.actionValue] = actionValue + } + if let actionName = actionName { + body[CustomerioConstants.actionName] = actionName + } + CustomerioInAppMessaging.shared?.sendEvent( + withName: CustomerioConstants.inAppEventListener, + body: body + ) + } + + func messageShown(message: InAppMessage) { + sendEvent(eventType: CustomerioConstants.messageShown, message: message) + } + + func messageDismissed(message: InAppMessage) { + sendEvent(eventType: CustomerioConstants.messageDismissed, message: message) + } + + func errorWithMessage(message: InAppMessage) { + sendEvent(eventType: CustomerioConstants.errorWithMessage, message: message) + } + + func messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) { + sendEvent(eventType: CustomerioConstants.messageActionTaken, message: message, actionValue: actionValue, actionName: actionName) + } +} diff --git a/ios/wrappers/utils/CioConfigUtils.swift b/ios/wrappers/utils/CioConfigUtils.swift new file mode 100644 index 00000000..8a7737c0 --- /dev/null +++ b/ios/wrappers/utils/CioConfigUtils.swift @@ -0,0 +1,50 @@ +import CioInternalCommon +import CioDataPipelines +import CioMessagingInApp + +extension Region: Decodable {} + +let logLevels = Set(CioLogLevel.allCases.map(\.rawValue)) + +struct RCTCioConfig: Decodable { + let cdpApiKey: String + let siteId: String? + let region: Region? + let trackApplicationLifecycleEvents: Bool? + let enableInApp: Bool? + + static func from(_ json: AnyObject) throws -> Self { + let data = try JSONSerialization.data(withJSONObject: json, options: []) + let instance = try JSONDecoder().decode(Self.self, from: data) + return instance + } +} + +struct CioConfig { + let cdp: SDKConfigBuilderResult + let inApp: MessagingInAppConfigOptions? +} + +func ifNotNil(_ value: V?, thenPassItTo: (V) -> K) { + if let value { + _ = thenPassItTo(value) + } +} + +func cioInitializeConfig(from config: RCTCioConfig, logLevel: String?) -> CioConfig { + + let cdpConfigBuilder = SDKConfigBuilder(cdpApiKey: config.cdpApiKey) + let cioLogLevel = CioLogLevel(rawValue: logLevel ?? "no log level") + ifNotNil(config.siteId, thenPassItTo: cdpConfigBuilder.migrationSiteId) + ifNotNil(config.region, thenPassItTo: cdpConfigBuilder.region) + ifNotNil(config.trackApplicationLifecycleEvents, thenPassItTo: cdpConfigBuilder.trackApplicationLifecycleEvents) + ifNotNil(cioLogLevel, thenPassItTo: cdpConfigBuilder.logLevel) + + var inAppConfig: MessagingInAppConfigOptions? = nil + + if let siteId = config.siteId, let region = config.region, let enableInApp = config.enableInApp, enableInApp { + inAppConfig = MessagingInAppConfigBuilder(siteId: siteId, region: region).build() + } + + return CioConfig(cdp: cdpConfigBuilder.build(), inApp: inAppConfig) +} diff --git a/ios/wrappers/Constants.swift b/ios/wrappers/utils/Constants.swift similarity index 100% rename from ios/wrappers/Constants.swift rename to ios/wrappers/utils/Constants.swift diff --git a/src/CustomerioConfig.tsx b/src/CustomerioConfig.tsx deleted file mode 100644 index 8d444a33..00000000 --- a/src/CustomerioConfig.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { CioLogLevel, Region } from './CustomerioEnum'; -import { PushClickBehaviorAndroid } from './types'; - -/** - * Configure package using CustomerioConfig - * - * Usecase: - * - * const configData = new CustomerioConfig() - * configData.logLevel = CioLogLevel.debug - * configData.autoTrackDeviceAttributes = true - * CustomerIO.config(data) - */ -class CustomerioConfig { - logLevel: CioLogLevel = CioLogLevel.error; - autoTrackDeviceAttributes: boolean = true; - enableInApp: boolean = false; - trackingApiUrl: string = ''; - autoTrackPushEvents: boolean = true; - backgroundQueueMinNumberOfTasks: number = 10; - backgroundQueueSecondsDelay: number = 30; - pushClickBehaviorAndroid: PushClickBehaviorAndroid = - PushClickBehaviorAndroid.ActivityPreventRestart; -} - -class CustomerIOEnv { - siteId: string = ''; - apiKey: string = ''; - region: Region = Region.US; - /** - * @deprecated since version 2.0.2 - * - * organizationId is no longer needed and will be removed in future releases. - * Please remove organizationId from code and enable in-app messaging using {CustomerioConfig.enableInApp}. - */ - organizationId: string = ''; -} - -class PackageConfig { - version: string = ''; - source: string = ''; -} - -export { CustomerIOEnv, CustomerioConfig, PackageConfig }; diff --git a/src/CustomerioEnum.tsx b/src/CustomerioEnum.tsx deleted file mode 100644 index c9f8e648..00000000 --- a/src/CustomerioEnum.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Enum to define the log levels. - * Logs can be viewed in Xcode or Android studio. - */ -enum CioLogLevel { - none = 1, - error, - info, - debug, -} - -/** - * Use this enum to specify the region your customer.io workspace is present in. - * US - for data center in United States - * EU - for data center in European Union - */ -enum Region { - US = 'US', - EU = 'EU', -} - -export { CioLogLevel, Region }; diff --git a/src/CustomerioTracking.tsx b/src/CustomerioTracking.tsx deleted file mode 100644 index 2722a330..00000000 --- a/src/CustomerioTracking.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { NativeModules, Platform } from 'react-native'; -import { - CustomerioConfig, - CustomerIOEnv, - PackageConfig, -} from './CustomerioConfig'; -import { Region } from './CustomerioEnum'; -import { CustomerIOInAppMessaging } from './CustomerIOInAppMessaging'; -import { CustomerIOPushMessaging } from './CustomerIOPushMessaging'; -import type { PushPermissionStatus, PushPermissionOptions } from './types'; -var pjson = require('customerio-reactnative/package.json'); - -const LINKING_ERROR = - `The package 'customerio-reactnative' doesn't seem to be linked. Make sure: \n\n` + - Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + - '- You rebuilt the app after installing the package\n' + - '- You are not using Expo managed workflow\n'; - -/** - * Get CustomerioReactnative native module - */ -const CustomerioReactnative = NativeModules.CustomerioReactnative - ? NativeModules.CustomerioReactnative - : new Proxy( - {}, - { - get() { - throw new Error(LINKING_ERROR); - }, - } - ); - -class CustomerIO { - /** - * To initialize the package using workspace credentials - * such as siteId, APIKey and region as optional. - * - * @param env use CustomerIOEnv class to set environment variables such as siteId, apiKey, region, org id - * @param config set config for the package eg trackApiUrl etc - * @returns - */ - static initialize( - env: CustomerIOEnv, - config: CustomerioConfig = new CustomerioConfig() - ) { - let pversion = pjson.version ?? ''; - let expoVersion = pjson.expoVersion ?? ''; - - const packageConfig = new PackageConfig(); - packageConfig.source = 'ReactNative'; - packageConfig.version = pversion; - if (expoVersion !== '') { - packageConfig.source = 'Expo'; - packageConfig.version = expoVersion; - } - - if (env.organizationId && env.organizationId !== '') { - console.warn( - '{organizationId} is deprecated and will be removed in future releases, please remove {organizationId} and enable in-app messaging using {CustomerioConfig.enableInApp}' - ); - if (config.enableInApp === false) { - config.enableInApp = true; - console.warn( - '{config.enableInApp} set to {true} because {organizationId} was added' - ); - } - } - - CustomerioReactnative.initialize(env, config, packageConfig); - } - - /** - * Identify a person using a unique identifier, eg. email id. - * Note that you can identify only 1 profile at a time. In case, multiple - * identifiers are attempted to be identified, then the last identified profile - * will be removed automatically. - * - * @param identifier unique identifier for a profile - * @param body (Optional) data to identify a profile - */ - static identify(identifier: string, body?: Object) { - CustomerioReactnative.identify(identifier, body); - } - - /** - * 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 clearIdentify() { - CustomerioReactnative.clearIdentify(); - } - - /** - * To track user events like loggedIn, addedItemToCart etc. - * You may also track events with additional yet optional data. - * - * @param name event name to be tracked - * @param data (Optional) data to be sent with event - */ - static track(name: string, data?: Object) { - CustomerioReactnative.track(name, data); - } - - /** - * Use this function to send custom device attributes - * such as app preferences, timezone etc - * - * @param data device attributes data - */ - static setDeviceAttributes(data: Object) { - CustomerioReactnative.setDeviceAttributes(data); - } - - /** - * Set custom user profile information such as user preference, specific - * user actions etc - * - * @param data additional attributes for a user profile - */ - static setProfileAttributes(data: Object) { - CustomerioReactnative.setProfileAttributes(data); - } - - /** - * Track screen events to record the screens a user visits - * - * @param name name of the screen user visited - * @param data (Optional) any additional data to be sent - */ - static screen(name: string, data?: Object) { - CustomerioReactnative.screen(name, data); - } - - static inAppMessaging(): CustomerIOInAppMessaging { - return new CustomerIOInAppMessaging(); - } - - static pushMessaging(): CustomerIOPushMessaging { - return new CustomerIOPushMessaging(); - } - - /** - * Register a device with respect to a profile. - * If no profile is identified, no device will be registered. - * - * @param token device token (iOS/Android) - */ - static registerDeviceToken(token: string) { - if (token == null) { - return; - } - CustomerioReactnative.registerDeviceToken(token); - } - - static deleteDeviceToken() { - CustomerioReactnative.deleteDeviceToken(); - } - - /** - * Request to show prompt for push notification permissions. - * Prompt will only be shown if the current status is - not determined. - * In other cases, this function will return current status of permission. - * @param options - * @returns Success & Failure promises - */ - static async showPromptForPushNotifications( - options?: PushPermissionOptions - ): Promise { - let defaultOptions: PushPermissionOptions = { - ios: { - badge: true, - sound: true, - }, - }; - - return CustomerioReactnative.showPromptForPushNotifications( - options || defaultOptions - ); - } - - /** - * Get status of push permission for the app - * @returns Promise with status of push permission as a string - */ - static getPushPermissionStatus(): Promise { - return CustomerioReactnative.getPushPermissionStatus(); - } -} - -export { CustomerIO, Region }; diff --git a/src/cio-config.ts b/src/cio-config.ts new file mode 100644 index 00000000..29a3308e --- /dev/null +++ b/src/cio-config.ts @@ -0,0 +1,20 @@ +export enum CioRegion { + US = 'US', + EU = 'EU', +} + +export enum CioLogLevel { + None = 'none', + Error = 'error', + Info = 'info', + Debug = 'debug', +} + +export type CioConfig = { + cdpApiKey: string; + siteId?: string; + region?: CioRegion; + logLevel?: CioLogLevel; + trackApplicationLifecycleEvents?: boolean; + enableInApp?: boolean; +}; diff --git a/src/customer-io.ts b/src/customer-io.ts new file mode 100644 index 00000000..400aeeef --- /dev/null +++ b/src/customer-io.ts @@ -0,0 +1,99 @@ +import { NativeModules, Platform } from 'react-native'; +import { CioLogLevel, type CioConfig } from './cio-config'; +import { CustomerIOInAppMessaging } from './customerio-inapp'; +import { CustomerIOPushMessaging } from './customerio-push'; +import { NativeLoggerListener } from './native-logger-listener'; + +const LINKING_ERROR = + `The package 'customerio-reactnative' doesn't seem to be linked. Make sure: ` + + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + + '- You rebuilt the app after installing the package\n' + + '- You are not using Expo Go\n'; + +const NativeCustomerIO = NativeModules.NativeCustomerIO + ? NativeModules.NativeCustomerIO + : new Proxy( + {}, + { + get() { + throw new Error(LINKING_ERROR); + }, + } + ); + +export class CustomerIO { + private static initialized = false; + + static readonly initialize = async (config: CioConfig) => { + if (config.logLevel && config.logLevel !== CioLogLevel.None) { + NativeLoggerListener.initialize(); + } + NativeCustomerIO.initialize(config, config.logLevel); + CustomerIO.initialized = true; + }; + + static readonly identify = async ( + id?: string, + traits?: Record + ) => { + CustomerIO.assrtInitialized(); + if (!id && !traits) { + throw new Error('You must provide an id or traits to identify'); + } + return NativeCustomerIO.identify(id, traits); + }; + + static readonly clearIdentify = async () => { + CustomerIO.assrtInitialized(); + return NativeCustomerIO.clearIdentify(); + }; + + static readonly track = async ( + name: string, + properties?: Record + ) => { + CustomerIO.assrtInitialized(); + if (!name) { + throw new Error('You must provide a name to track'); + } + return NativeCustomerIO.track(name, properties); + }; + + static readonly screen = async ( + title: string, + category?: string, + properties?: Record + ) => { + CustomerIO.assrtInitialized(); + if (!title) { + throw new Error('You must provide a name to screen'); + } + return NativeCustomerIO.screen(title, category, properties); + }; + + static readonly setProfileAttributes = async ( + attributes: Record + ) => { + CustomerIO.assrtInitialized(); + return NativeCustomerIO.setProfileAttributes(attributes); + }; + + static readonly setDeviceAttributes = async ( + attributes: Record + ) => { + CustomerIO.assrtInitialized(); + return NativeCustomerIO.setDeviceAttributes(attributes); + }; + + static readonly isInitialized = () => CustomerIO.initialized; + + static readonly inAppMessaging = new CustomerIOInAppMessaging(); + + static readonly pushMessaging = new CustomerIOPushMessaging(); + + private static readonly assrtInitialized = () => { + if (!CustomerIO.initialized) { + throw new Error('CustomerIO must be initialized before use'); + } + }; +} diff --git a/src/CustomerIOInAppMessaging.ts b/src/customerio-inapp.ts similarity index 100% rename from src/CustomerIOInAppMessaging.ts rename to src/customerio-inapp.ts diff --git a/src/CustomerIOPushMessaging.ts b/src/customerio-push.ts similarity index 100% rename from src/CustomerIOPushMessaging.ts rename to src/customerio-push.ts diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..8465d680 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from './cio-config'; +export * from './customer-io'; diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 20d3a3d0..00000000 --- a/src/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * This is react native package for Customer.io - * Package name : customerio-reactnative - */ - -import { CustomerioConfig, CustomerIOEnv } from './CustomerioConfig'; -import { CioLogLevel, Region } from './CustomerioEnum'; -import { - CustomerIOInAppMessaging, - InAppMessageEventType, - InAppMessageEvent, -} from './CustomerIOInAppMessaging'; -import { CustomerIO } from './CustomerioTracking'; - -export { - CustomerIO, - CustomerIOInAppMessaging, - InAppMessageEventType, - InAppMessageEvent, - Region, - CustomerioConfig, - CustomerIOEnv, - CioLogLevel, -}; - -export * from './types'; From b78b3985b56061fd8fcd8f9d347a1906e5eb62dc Mon Sep 17 00:00:00 2001 From: Ahmed-Ali Date: Fri, 21 Jun 2024 21:00:53 +0400 Subject: [PATCH 2/2] chore: Add QA settings --- ios/wrappers/utils/CioConfigUtils.swift | 8 ++++++++ src/customer-io.ts | 4 ---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ios/wrappers/utils/CioConfigUtils.swift b/ios/wrappers/utils/CioConfigUtils.swift index 8a7737c0..06b89c55 100644 --- a/ios/wrappers/utils/CioConfigUtils.swift +++ b/ios/wrappers/utils/CioConfigUtils.swift @@ -6,12 +6,18 @@ extension Region: Decodable {} let logLevels = Set(CioLogLevel.allCases.map(\.rawValue)) +struct QASettings: Decodable { + let cdnHost: String + let apiHost: String +} + struct RCTCioConfig: Decodable { let cdpApiKey: String let siteId: String? let region: Region? let trackApplicationLifecycleEvents: Bool? let enableInApp: Bool? + let qa: QASettings? static func from(_ json: AnyObject) throws -> Self { let data = try JSONSerialization.data(withJSONObject: json, options: []) @@ -39,6 +45,8 @@ func cioInitializeConfig(from config: RCTCioConfig, logLevel: String?) -> CioCon ifNotNil(config.region, thenPassItTo: cdpConfigBuilder.region) ifNotNil(config.trackApplicationLifecycleEvents, thenPassItTo: cdpConfigBuilder.trackApplicationLifecycleEvents) ifNotNil(cioLogLevel, thenPassItTo: cdpConfigBuilder.logLevel) + ifNotNil(config.qa?.cdnHost, thenPassItTo: cdpConfigBuilder.cdnHost) + ifNotNil(config.qa?.apiHost, thenPassItTo: cdpConfigBuilder.apiHost) var inAppConfig: MessagingInAppConfigOptions? = nil diff --git a/src/customer-io.ts b/src/customer-io.ts index 400aeeef..f7d56e96 100644 --- a/src/customer-io.ts +++ b/src/customer-io.ts @@ -36,7 +36,6 @@ export class CustomerIO { id?: string, traits?: Record ) => { - CustomerIO.assrtInitialized(); if (!id && !traits) { throw new Error('You must provide an id or traits to identify'); } @@ -44,7 +43,6 @@ export class CustomerIO { }; static readonly clearIdentify = async () => { - CustomerIO.assrtInitialized(); return NativeCustomerIO.clearIdentify(); }; @@ -74,14 +72,12 @@ export class CustomerIO { static readonly setProfileAttributes = async ( attributes: Record ) => { - CustomerIO.assrtInitialized(); return NativeCustomerIO.setProfileAttributes(attributes); }; static readonly setDeviceAttributes = async ( attributes: Record ) => { - CustomerIO.assrtInitialized(); return NativeCustomerIO.setDeviceAttributes(attributes); };