From 1310845a9ad363e521d4c4b7732d4616d0c2192d Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Thu, 16 May 2024 15:56:26 +0200 Subject: [PATCH 01/30] android: bump batch sdk to 2.0 --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 2037bce..17f1abb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ def DEFAULT_MIN_SDK_VERSION = 16 def DEFAULT_COMPILE_SDK_VERSION = 34 def DEFAULT_BUILD_TOOLS_VERSION = "34.0.0" def DEFAULT_TARGET_SDK_VERSION = 34 -def DEFAULT_BATCH_SDK_VERSION = "1.21.+" +def DEFAULT_BATCH_SDK_VERSION = "2.0.+" def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback From 2f44baa498e848a0cbafb71db0f821e388e90a6b Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Thu, 16 May 2024 15:59:51 +0200 Subject: [PATCH 02/30] android: update how to start batch --- android/src/main/java/com/batch/batch_rn/RNBatchModule.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index eec4c88..0989e40 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -21,7 +21,6 @@ import com.batch.android.BatchInboxNotificationContent; import com.batch.android.BatchMessage; import com.batch.android.BatchUserDataEditor; -import com.batch.android.Config; import com.batch.android.json.JSONObject; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; @@ -92,7 +91,7 @@ public static void initialize(Application application) { Resources resources = application.getResources(); String packageName = application.getPackageName(); String batchAPIKey = resources.getString(resources.getIdentifier("BATCH_API_KEY", "string", packageName)); - Batch.setConfig(new Config(batchAPIKey)); + Batch.start(batchAPIKey); Batch.EventDispatcher.addDispatcher(eventDispatcher); try { boolean doNotDisturbEnabled = resources.getBoolean(resources.getIdentifier("BATCH_DO_NOT_DISTURB_INITIAL_STATE", "bool", packageName)); From 0558a8da9fece484583386ee8157c43c99bf5a36 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Thu, 16 May 2024 16:00:36 +0200 Subject: [PATCH 03/30] android: update how to get push token --- android/src/main/java/com/batch/batch_rn/RNBatchModule.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index 0989e40..a1f4eb1 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -13,6 +13,7 @@ import com.batch.android.Batch; import com.batch.android.BatchActivityLifecycleHelper; import com.batch.android.BatchAttributesFetchListener; +import com.batch.android.BatchPushRegistration; import com.batch.android.BatchTagCollectionsFetchListener; import com.batch.android.BatchEmailSubscriptionState; import com.batch.android.BatchUserAttribute; @@ -172,8 +173,8 @@ public void push_dismissNotifications() { /* No effect on android */ } @ReactMethod public void push_getLastKnownPushToken(Promise promise) { - String pushToken = Batch.Push.getLastKnownPushToken(); - promise.resolve(pushToken); + BatchPushRegistration registration = Batch.Push.getRegistration(); + promise.resolve(registration != null ? registration.getToken() : null); } @ReactMethod From 398bcc970e16b120e883e27568727993889cd9ef Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Thu, 16 May 2024 16:11:20 +0200 Subject: [PATCH 04/30] bridge: remove trackTransaction API --- CHANGELOG.md | 15 +++++++++++- .../com/batch/batch_rn/RNBatchModule.java | 9 -------- ios/RNBatch.m | 11 --------- src/Batch.ts | 4 ++-- src/BatchUser.ts | 23 ------------------- 5 files changed, 16 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c449c5..383d73e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ +9.0.0 +---- + +**Plugin** + +* Updated Batch 2.0 +* Batch requires iOS 13.0 or higher. +* Batch requires a `minSdk` level of 21 or higher. + +**User** + +- Removed method `trackTransaction` with no equivalent. + 8.2.0 -____ +---- **Plugin** diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index a1f4eb1..fd4dbd6 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -673,15 +673,6 @@ public void userData_trackEvent(String name, String label, ReadableMap serialize Batch.User.trackEvent(name, label, RNUtils.convertSerializedEventDataToEventData(serializedEventData)); } - @ReactMethod - public void userData_trackTransaction(double amount, ReadableMap data) { - JSONObject transactionData = null; - if (data != null) { - transactionData = new JSONObject(data.toHashMap()); - } - Batch.User.trackTransaction(amount, transactionData); - } - @ReactMethod public void userData_trackLocation(ReadableMap serializedLocation) { Location nativeLocation = new Location("com.batch.batch_rn"); diff --git a/ios/RNBatch.m b/ios/RNBatch.m index 2dd32a6..73c7fa2 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -453,17 +453,6 @@ -(void)stopObserving { [BatchUser trackEvent:name withLabel:label associatedData:batchEventData]; } -RCT_EXPORT_METHOD(userData_trackTransaction:(double)amount data:(NSDictionary*)rawData) -{ - if (rawData && ![rawData isKindOfClass:[NSDictionary class]]) - { - NSLog(@"RNBatch: trackTransaction data should be an dictionary or nil"); - return; - } - - [BatchUser trackTransactionWithAmount:amount data:rawData]; -} - RCT_EXPORT_METHOD(userData_trackLocation:(NSDictionary*)serializedLocation) { if (![serializedLocation isKindOfClass:[NSDictionary class]] || [serializedLocation count]==0) diff --git a/src/Batch.ts b/src/Batch.ts index 331d353..bfcdfe4 100644 --- a/src/Batch.ts +++ b/src/Batch.ts @@ -36,9 +36,9 @@ export const Batch = { * - Prevent batch.start() * - Disable any network capability from the SDK * - Disable all In-App campaigns - * - Make the Inbox module return an error immediatly when used + * - Make the Inbox module return an error immediately when used * - Make the SDK reject any editor calls - * - Make the SDK reject calls to batch.user.trackEvent(), batch.user.trackTransaction(), batch.user.trackLocation() + * - Make the SDK reject calls to batch.profile.trackEvent(), batch.profile.trackLocation() * and any related methods * * Even if you opt in afterwards, data that has been generated while opted out WILL be lost. diff --git a/src/BatchUser.ts b/src/BatchUser.ts index 18a2be5..9e0a216 100644 --- a/src/BatchUser.ts +++ b/src/BatchUser.ts @@ -107,29 +107,6 @@ export const BatchUser = { ); }, - /** - * Track a transaction. Batch must be started at some point, or events won't be sent to the server. - * @param amount Transaction's amount. - * @param data The transaction data (optional). Must be an object. - */ - trackTransaction: (amount: number, data?: { [key: string]: unknown }): void => { - if (typeof amount === 'undefined') { - Log(false, 'BatchUser - Amount must be a valid number. Ignoring transaction.'); - return; - } - - if (!isNumber(amount) || isNaN(amount)) { - Log(false, 'BatchUser - Amount must be a valid number. Ignoring transaction.'); - return; - } - - if (typeof data !== 'object') { - data = null; - } - - RNBatch.userData_trackTransaction(amount, data); - }, - /** * Track a geolocation update * You can call this method from any thread. Batch must be started at some point, or location updates won't be sent to the server. From 81dc271b4ef2c46ca5558127317bc3e197a3cca2 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 17 May 2024 16:40:36 +0200 Subject: [PATCH 05/30] android: bump android min sdk version to 21 --- android/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 17f1abb..84d8051 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,4 +1,4 @@ -def DEFAULT_MIN_SDK_VERSION = 16 +def DEFAULT_MIN_SDK_VERSION = 21 def DEFAULT_COMPILE_SDK_VERSION = 34 def DEFAULT_BUILD_TOOLS_VERSION = "34.0.0" def DEFAULT_TARGET_SDK_VERSION = 34 @@ -13,7 +13,6 @@ buildscript { repositories { mavenCentral() google() - jcenter() } dependencies { From a76812ebf829a88ee29e2c4c6d57d70a8a6b9d28 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 17 May 2024 17:32:55 +0200 Subject: [PATCH 06/30] android: bump AGP to 8.2.2 --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 84d8051..da4bbb4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,7 +16,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.2.0") + classpath("com.android.tools.build:gradle:8.2.2") } } } From 7ca6ef92bebb22699bf66fac70ece235e98ac179 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 17 May 2024 17:33:23 +0200 Subject: [PATCH 07/30] android: make plugin compile on batch v2 --- .../com/batch/batch_rn/RNBatchModule.java | 45 +++++-------------- .../main/java/com/batch/batch_rn/RNUtils.java | 34 ++++++++------ 2 files changed, 33 insertions(+), 46 deletions(-) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index fd4dbd6..40e1d59 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -13,6 +13,7 @@ import com.batch.android.Batch; import com.batch.android.BatchActivityLifecycleHelper; import com.batch.android.BatchAttributesFetchListener; +import com.batch.android.BatchProfileAttributeEditor; import com.batch.android.BatchPushRegistration; import com.batch.android.BatchTagCollectionsFetchListener; import com.batch.android.BatchEmailSubscriptionState; @@ -21,8 +22,6 @@ import com.batch.android.BatchInboxFetcher; import com.batch.android.BatchInboxNotificationContent; import com.batch.android.BatchMessage; -import com.batch.android.BatchUserDataEditor; -import com.batch.android.json.JSONObject; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; @@ -571,7 +570,7 @@ public void onError() { @ReactMethod public void userData_save(ReadableArray actions) { - BatchUserDataEditor editor = Batch.User.editor(); + BatchProfileAttributeEditor editor = Batch.Profile.editor(); for (int i = 0; i < actions.size(); i++) { ReadableMap action = actions.getMap(i); String type = action.getString("type"); @@ -605,27 +604,17 @@ public void userData_save(ReadableArray actions) { } else if (type.equals("removeAttribute")) { String key = action.getString("key"); editor.removeAttribute(key); - } else if (type.equals("clearAttributes")) { - editor.clearAttributes(); - } else if (type.equals("setIdentifier")) { + } else if (type.equals("setEmail")) { ReadableType valueType = action.getType("value"); if (valueType.equals(ReadableType.Null)) { - editor.setIdentifier(null); + editor.setEmailAddress(null); } else { String value = action.getString("value"); - editor.setIdentifier(value); - } - } else if (type.equals("setEmail")) { - ReadableType valueType = action.getType("value"); - if (valueType.equals(ReadableType.Null)) { - editor.setEmail(null); - } else { - String value = action.getString("value"); - editor.setEmail(value); + editor.setEmailAddress(value); } } else if (type.equals("setEmailMarketingSubscriptionState")) { String value = action.getString("value"); - editor.setEmailMarketingSubscriptionState(BatchEmailSubscriptionState.valueOf(value)); + editor.setEmailMarketingSubscription(BatchEmailSubscriptionState.valueOf(value)); } else if (type.equals("setLanguage")) { ReadableType valueType = action.getType("value"); if (valueType.equals(ReadableType.Null)) { @@ -642,35 +631,25 @@ public void userData_save(ReadableArray actions) { String value = action.getString("value"); editor.setRegion(value); } - } else if (type.equals("setAttributionId")) { - ReadableType valueType = action.getType("value"); - if (valueType.equals(ReadableType.Null)) { - editor.setAttributionIdentifier(null); - } else { - String value = action.getString("value"); - editor.setAttributionIdentifier(value); - } } else if (type.equals("addTag")) { String collection = action.getString("collection"); String tag = action.getString("tag"); - editor.addTag(collection, tag); + editor.addToArray(collection, tag); } else if (type.equals("removeTag")) { String collection = action.getString("collection"); String tag = action.getString("tag"); - editor.removeTag(collection, tag); + editor.removeFromArray(collection, tag); } else if (type.equals("clearTagCollection")) { String collection = action.getString("collection"); - editor.clearTagCollection(collection); - } else if (type.equals("clearTags")) { - editor.clearTags(); + editor.removeAttribute(collection); } } editor.save(); } @ReactMethod - public void userData_trackEvent(String name, String label, ReadableMap serializedEventData) { - Batch.User.trackEvent(name, label, RNUtils.convertSerializedEventDataToEventData(serializedEventData)); + public void userData_trackEvent(String name, ReadableMap serializedEventData) { + Batch.Profile.trackEvent(name, RNUtils.convertSerializedEventDataToEventAttributes(serializedEventData)); } @ReactMethod @@ -687,6 +666,6 @@ public void userData_trackLocation(ReadableMap serializedLocation) { nativeLocation.setTime((long) serializedLocation.getDouble("date")); } - Batch.User.trackLocation(nativeLocation); + Batch.Profile.trackLocation(nativeLocation); } } diff --git a/android/src/main/java/com/batch/batch_rn/RNUtils.java b/android/src/main/java/com/batch/batch_rn/RNUtils.java index f55d70a..0036326 100644 --- a/android/src/main/java/com/batch/batch_rn/RNUtils.java +++ b/android/src/main/java/com/batch/batch_rn/RNUtils.java @@ -4,7 +4,7 @@ import android.util.Log; -import com.batch.android.BatchEventData; +import com.batch.android.BatchEventAttributes; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; @@ -16,7 +16,9 @@ import org.json.JSONArray; import java.net.URI; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Map; public class RNUtils { @@ -78,16 +80,22 @@ public static WritableArray convertArrayToWritableArray(Object[] input) { } @Nullable - public static BatchEventData convertSerializedEventDataToEventData(@Nullable ReadableMap serializedEventData) { + public static BatchEventAttributes convertSerializedEventDataToEventAttributes(@Nullable ReadableMap serializedEventData) { if (serializedEventData == null) { return null; } - BatchEventData batchEventData = new BatchEventData(); - ReadableArray tags = serializedEventData.getArray("tags"); - + BatchEventAttributes eventAttributes = new BatchEventAttributes(); + ReadableArray tags = serializedEventData.getArray("$tags"); + List tagsList = new ArrayList<>(); for (int i = 0; i < tags.size(); i++) { - batchEventData.addTag(tags.getString(i)); + tagsList.add(tags.getString(i)); + } + eventAttributes.putStringList("$tags", tagsList); + + String label = serializedEventData.getString("$label"); + if (label != null) { + eventAttributes.put("$label", label); } ReadableMap attributes = serializedEventData.getMap("attributes"); @@ -98,24 +106,24 @@ public static BatchEventData convertSerializedEventDataToEventData(@Nullable Rea ReadableMap valueMap = attributes.getMap(key); String type = valueMap.getString("type"); if ("string".equals(type)) { - batchEventData.put(key, valueMap.getString("value")); + eventAttributes.put(key, valueMap.getString("value")); } else if ("boolean".equals(type)) { - batchEventData.put(key, valueMap.getBoolean("value")); + eventAttributes.put(key, valueMap.getBoolean("value")); } else if ("integer".equals(type)) { - batchEventData.put(key, valueMap.getDouble("value")); + eventAttributes.put(key, valueMap.getDouble("value")); } else if ("float".equals(type)) { - batchEventData.put(key, valueMap.getDouble("value")); + eventAttributes.put(key, valueMap.getDouble("value")); } else if ("date".equals(type)) { long timestamp = (long) valueMap.getDouble("value"); Date date = new Date(timestamp); - batchEventData.put(key, date); + eventAttributes.put(key, date); } else if ("url".equals(type)) { - batchEventData.put(key, URI.create(valueMap.getString("value"))); + eventAttributes.put(key, URI.create(valueMap.getString("value"))); } else { Log.e("RNBatchPush", "Invalid parameter : Unknown event_data.attributes type (" + type + ")"); } } - return batchEventData; + return eventAttributes; } } From 2cce40d5e9c8874dd73cf3f1e2b2731749db00e2 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Wed, 22 May 2024 10:05:06 +0200 Subject: [PATCH 08/30] android: add profile module + rework editor --- .../com/batch/batch_rn/RNBatchModule.java | 39 ++- .../main/java/com/batch/batch_rn/RNUtils.java | 20 +- src/BatchProfile.ts | 94 +++++++ src/BatchProfileAttributeEditor.ts | 175 +++++++++++++ src/BatchUser.ts | 91 ------- src/BatchUserEditor.ts | 235 ------------------ 6 files changed, 311 insertions(+), 343 deletions(-) create mode 100644 src/BatchProfile.ts create mode 100644 src/BatchProfileAttributeEditor.ts delete mode 100644 src/BatchUserEditor.ts diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index 40e1d59..1900c67 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -35,6 +35,7 @@ import com.facebook.react.bridge.WritableNativeMap; import java.net.URI; +import java.util.ArrayList; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; @@ -591,6 +592,9 @@ public void userData_save(ReadableArray actions) { case String: editor.setAttribute(key, action.getString("value")); break; + case Array: + ReadableArray array = action.getArray("value"); + editor.setAttribute(key, RNUtils.convertReadableArrayToList(array)); } } else if (type.equals("setDateAttribute")) { String key = action.getString("key"); @@ -604,7 +608,7 @@ public void userData_save(ReadableArray actions) { } else if (type.equals("removeAttribute")) { String key = action.getString("key"); editor.removeAttribute(key); - } else if (type.equals("setEmail")) { + } else if (type.equals("setEmailAddress")) { ReadableType valueType = action.getType("value"); if (valueType.equals(ReadableType.Null)) { editor.setEmailAddress(null); @@ -612,7 +616,7 @@ public void userData_save(ReadableArray actions) { String value = action.getString("value"); editor.setEmailAddress(value); } - } else if (type.equals("setEmailMarketingSubscriptionState")) { + } else if (type.equals("setEmailMarketingSubscription")) { String value = action.getString("value"); editor.setEmailMarketingSubscription(BatchEmailSubscriptionState.valueOf(value)); } else if (type.equals("setLanguage")) { @@ -631,17 +635,26 @@ public void userData_save(ReadableArray actions) { String value = action.getString("value"); editor.setRegion(value); } - } else if (type.equals("addTag")) { - String collection = action.getString("collection"); - String tag = action.getString("tag"); - editor.addToArray(collection, tag); - } else if (type.equals("removeTag")) { - String collection = action.getString("collection"); - String tag = action.getString("tag"); - editor.removeFromArray(collection, tag); - } else if (type.equals("clearTagCollection")) { - String collection = action.getString("collection"); - editor.removeAttribute(collection); + } else if (type.equals("addToArray")) { + String key = action.getString("key"); + ReadableType valueType = action.getType("value"); + if (valueType.equals(ReadableType.String)) { + String value = action.getString("value"); + editor.addToArray(key, value); + } else if(valueType.equals(ReadableType.Array)) { + ReadableArray arrayValue = action.getArray("value"); + editor.addToArray(key, RNUtils.convertReadableArrayToList(arrayValue)); + } + } else if (type.equals("removeFromArray")) { + String key = action.getString("key"); + ReadableType valueType = action.getType("value"); + if (valueType.equals(ReadableType.String)) { + String value = action.getString("value"); + editor.removeFromArray(key, value); + } else if(valueType.equals(ReadableType.Array)) { + ReadableArray arrayValue = action.getArray("value"); + editor.removeFromArray(key, RNUtils.convertReadableArrayToList(arrayValue)); + } } } editor.save(); diff --git a/android/src/main/java/com/batch/batch_rn/RNUtils.java b/android/src/main/java/com/batch/batch_rn/RNUtils.java index 0036326..5a6eed0 100644 --- a/android/src/main/java/com/batch/batch_rn/RNUtils.java +++ b/android/src/main/java/com/batch/batch_rn/RNUtils.java @@ -87,11 +87,9 @@ public static BatchEventAttributes convertSerializedEventDataToEventAttributes(@ BatchEventAttributes eventAttributes = new BatchEventAttributes(); ReadableArray tags = serializedEventData.getArray("$tags"); - List tagsList = new ArrayList<>(); - for (int i = 0; i < tags.size(); i++) { - tagsList.add(tags.getString(i)); + if (tags != null && tags.size() > 0) { + eventAttributes.putStringList("$tags", convertReadableArrayToList(tags)); } - eventAttributes.putStringList("$tags", tagsList); String label = serializedEventData.getString("$label"); if (label != null) { @@ -126,4 +124,18 @@ public static BatchEventAttributes convertSerializedEventDataToEventAttributes(@ return eventAttributes; } + + /** + * Convert a ReadableArray into a List of String + * + * @param array ReadableArray to convert + * @return List of String + */ + public static List convertReadableArrayToList(ReadableArray array) { + List list = new ArrayList<>(); + for (int i = 0; i < array.size(); i++) { + list.add(array.getString(i)); + } + return list; + } } diff --git a/src/BatchProfile.ts b/src/BatchProfile.ts new file mode 100644 index 0000000..0e4b778 --- /dev/null +++ b/src/BatchProfile.ts @@ -0,0 +1,94 @@ +import { NativeModules } from 'react-native'; + +import { BatchEventData } from './BatchEventData'; +import { BatchProfileAttributeEditor } from './BatchProfileAttributeEditor'; +import Log from './helpers/Logger'; + +const RNBatch = NativeModules.RNBatch; + +/** + * Represents a locations, using lat/lng coordinates + */ +export interface Location { + /** + * Latitude + */ + latitude: number; + + /** + * Longitude + */ + longitude: number; + + /** + * Date of the tracked location + */ + date?: Date; + + /** + * Precision radius in meters + */ + precision?: number; +} + +/** + * Batch's user module + */ +export const BatchProfile = { + /** + * Creates a new instance of BatchProfileAttributeEditor + * @function + * @returns {BatchProfileAttributeEditor} A new instance of BatchProfileAttributeEditor + */ + editor: (): BatchProfileAttributeEditor => new BatchProfileAttributeEditor(), + + /** + * Track an event. Batch must be started at some point, or events won't be sent to the server. + * @param name The event name. Must be a string. + * @param data The event data (optional). Must be an object. + */ + trackEvent: (name: string, data?: BatchEventData): void => { + // Since _toInternalRepresentation is private, we have to resort to this little hack to access the method. + // That syntax keeps the argument type checking, while casting as any would not. + RNBatch.userData_trackEvent(name, data instanceof BatchEventData ? data['_toInternalRepresentation']() : null); + }, + + /** + * Track a geolocation update + * You can call this method from any thread. Batch must be started at some point, or location updates won't be sent to the server. + * @param location User location object + */ + trackLocation: (location: Location): void => { + if (typeof location !== 'object') { + Log(false, 'BatchUser - Invalid trackLocation argument. Skipping.'); + return; + } + + if (typeof location.latitude !== 'number' || isNaN(location.latitude)) { + Log(false, 'BatchUser - Invalid latitude. Skipping.'); + return; + } + + if (typeof location.longitude !== 'number' || isNaN(location.longitude)) { + Log(false, 'BatchUser - Invalid longitude. Skipping.'); + return; + } + + if (location.precision && (typeof location.precision !== 'number' || isNaN(location.precision))) { + Log(false, 'BatchUser - Invalid precision. Skipping.'); + return; + } + + if (location.date && !(location.date instanceof Date)) { + Log(false, 'BatchUser - Invalid date. Skipping.'); + return; + } + + RNBatch.userData_trackLocation({ + date: location.date ? location.date.getTime() : undefined, + latitude: location.latitude, + longitude: location.longitude, + precision: location.precision, + }); + }, +}; diff --git a/src/BatchProfileAttributeEditor.ts b/src/BatchProfileAttributeEditor.ts new file mode 100644 index 0000000..c66c7b4 --- /dev/null +++ b/src/BatchProfileAttributeEditor.ts @@ -0,0 +1,175 @@ +import { NativeModules } from 'react-native'; +const RNBatch = NativeModules.RNBatch; + +/** + * Enum defining the state of an email subscription + */ +export enum BatchEmailSubscriptionState { + SUBSCRIBED = 'SUBSCRIBED', + UNSUBSCRIBED = 'UNSUBSCRIBED', +} + +interface IUserSettingsSetAttributeAction { + type: 'setAttribute'; + key: string; + value: string | boolean | number | null | string[]; +} + +interface IUserSettingsRemoveAttributeAction { + type: 'removeAttribute'; + key: string; +} + +interface IUserSettingsSetDateAttributeAction { + type: 'setDateAttribute'; + key: string; + value: number; +} + +interface IUserSettingsSetURLAttributeAction { + type: 'setURLAttribute'; + key: string; + value: string; +} + +interface IUserSettingsSetLanguageAction { + type: 'setLanguage'; + value?: string; +} + +interface IUserSettingsSetRegionAction { + type: 'setRegion'; + value?: string; +} + +interface IUserSettingsSetEmailAddressAction { + type: 'setEmailAddress'; + value: string | null; +} + +interface IUserSettingsSetEmailMarketingSubscriptionAction { + type: 'setEmailMarketingSubscription'; + value: BatchEmailSubscriptionState; +} + +interface IUserSettingsAddToArrayAction { + type: 'addToArray'; + key: string; + value: string | string[]; +} + +interface IUserSettingsRemoveFromArrayAction { + type: 'removeFromArray'; + key: string; + value: string | string[]; +} + + +type IUserSettingsAction = + | IUserSettingsSetAttributeAction + | IUserSettingsRemoveAttributeAction + | IUserSettingsSetDateAttributeAction + | IUserSettingsSetURLAttributeAction + | IUserSettingsSetLanguageAction + | IUserSettingsSetRegionAction + | IUserSettingsAddToArrayAction + | IUserSettingsRemoveFromArrayAction + | IUserSettingsSetEmailAddressAction + | IUserSettingsSetEmailMarketingSubscriptionAction; + +type IUserSettingsActions = IUserSettingsAction[]; + +/** + * Editor class used to create and save user tags and attributes + */ +export class BatchProfileAttributeEditor { + private _settings: IUserSettingsActions; + + public constructor(settings: IUserSettingsActions = []) { + this._settings = settings; + } + + private addAction(action: IUserSettingsAction): BatchProfileAttributeEditor { + this._settings.push(action); + return this; + } + + public setAttribute(key: string, value: string | boolean | number | string[] | null): BatchProfileAttributeEditor { + return this.addAction({ + type: 'setAttribute', + key, + value, + }); + } + + public setDateAttribute(key: string, value: number): BatchProfileAttributeEditor { + return this.addAction({ + type: 'setDateAttribute', + key, + value, + }); + } + + public setURLAttribute(key: string, value: string): BatchProfileAttributeEditor { + return this.addAction({ + type: 'setURLAttribute', + key, + value, + }); + } + + public removeAttribute(key: string): BatchProfileAttributeEditor { + return this.addAction({ + type: 'removeAttribute', + key, + }); + } + + public setEmailAddress(value: string | null): BatchProfileAttributeEditor { + return this.addAction({ + type: 'setEmailAddress', + value, + }); + } + + public setEmailMarketingSubscription(value: BatchEmailSubscriptionState): BatchProfileAttributeEditor { + return this.addAction({ + type: 'setEmailMarketingSubscription', + value, + }); + } + + public setLanguage(value: string | null): BatchProfileAttributeEditor { + return this.addAction({ + type: 'setLanguage', + value, + }); + } + + public setRegion(value: string | null): BatchProfileAttributeEditor { + return this.addAction({ + type: 'setRegion', + value, + }); + } + + public addToArray(key: string, value: string | string[]): BatchProfileAttributeEditor { + return this.addAction({ + type: 'addToArray', + key, + value, + }); + } + + public removeFromArray(key: string, value: string | string[]): BatchProfileAttributeEditor { + return this.addAction({ + type: 'removeFromArray', + key, + value, + }); + } + + public save(): void { + RNBatch.userData_save(this._settings); + } +} diff --git a/src/BatchUser.ts b/src/BatchUser.ts index 9e0a216..3f6ffea 100644 --- a/src/BatchUser.ts +++ b/src/BatchUser.ts @@ -1,38 +1,9 @@ import { NativeModules } from 'react-native'; -import { BatchEventData } from './BatchEventData'; import { BatchUserAttribute } from './BatchUserAttribute'; -import { BatchUserEditor } from './BatchUserEditor'; -import Log from './helpers/Logger'; -import { isNumber, isString } from './helpers/TypeHelpers'; const RNBatch = NativeModules.RNBatch; -/** - * Represents a locations, using lat/lng coordinates - */ -export interface Location { - /** - * Latitude - */ - latitude: number; - - /** - * Longitude - */ - longitude: number; - - /** - * Date of the tracked location - */ - date?: Date; - - /** - * Precision radius in meters - */ - precision?: number; -} - /** * Batch's user module */ @@ -83,66 +54,4 @@ export const BatchUser = { * @returns The tags added with BatchUser.editor().addTag() */ getTagCollections: (): Promise<{ [key: string]: string[] }> => RNBatch.userData_getTags(), - - /** - * Creates an editor for the user profile - * The profile is not updated until the method `save()` is called - */ - editor: (): BatchUserEditor => new BatchUserEditor(), - - /** - * Track an event. Batch must be started at some point, or events won't be sent to the server. - * @param name The event name. Must be a string. - * @param label The event label (optional). Must be a string. - * @param data The event data (optional). Must be an object. - */ - trackEvent: (name: string, label?: string, data?: BatchEventData): void => { - //TODO (arnaud): Check if "isString" really is necessary. Same for data - // Since _toInternalRepresentation is private, we have to resort to this little hack to access the method. - // That syntax keeps the argument type checking, while casting as any would not. - RNBatch.userData_trackEvent( - name, - isString(label) ? label : null, - data instanceof BatchEventData ? data['_toInternalRepresentation']() : null - ); - }, - - /** - * Track a geolocation update - * You can call this method from any thread. Batch must be started at some point, or location updates won't be sent to the server. - * @param location User location object - */ - trackLocation: (location: Location): void => { - if (typeof location !== 'object') { - Log(false, 'BatchUser - Invalid trackLocation argument. Skipping.'); - return; - } - - if (typeof location.latitude !== 'number' || isNaN(location.latitude)) { - Log(false, 'BatchUser - Invalid latitude. Skipping.'); - return; - } - - if (typeof location.longitude !== 'number' || isNaN(location.longitude)) { - Log(false, 'BatchUser - Invalid longitude. Skipping.'); - return; - } - - if (location.precision && (typeof location.precision !== 'number' || isNaN(location.precision))) { - Log(false, 'BatchUser - Invalid precision. Skipping.'); - return; - } - - if (location.date && !(location.date instanceof Date)) { - Log(false, 'BatchUser - Invalid date. Skipping.'); - return; - } - - RNBatch.userData_trackLocation({ - date: location.date ? location.date.getTime() : undefined, - latitude: location.latitude, - longitude: location.longitude, - precision: location.precision, - }); - }, }; diff --git a/src/BatchUserEditor.ts b/src/BatchUserEditor.ts deleted file mode 100644 index 4ee3a11..0000000 --- a/src/BatchUserEditor.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { NativeModules } from 'react-native'; -const RNBatch = NativeModules.RNBatch; - -/** - * Enum defining the state of an email subscription - */ -export enum BatchEmailSubscriptionState { - SUBSCRIBED = 'SUBSCRIBED', - UNSUBSCRIBED = 'UNSUBSCRIBED', -} - -interface IUserSettingsSetAttributeAction { - type: 'setAttribute'; - key: string; - value: string | boolean | number | null; -} - -interface IUserSettingsRemoveAttributeAction { - type: 'removeAttribute'; - key: string; -} - -interface IUserSettingsClearAttributesAction { - type: 'clearAttributes'; -} - -interface IUserSettingsSetDateAttributeAction { - type: 'setDateAttribute'; - key: string; - value: number; -} - -interface IUserSettingsSetURLAttributeAction { - type: 'setURLAttribute'; - key: string; - value: string; -} - -interface IUserSettingsSetLanguageAction { - type: 'setLanguage'; - value?: string; -} - -interface IUserSettingsSetRegionAction { - type: 'setRegion'; - value?: string; -} - -interface IUserSettingsSetIdentifierAction { - type: 'setIdentifier'; - value: string | null; -} - -interface IUserSettingsSetEmailAction { - type: 'setEmail'; - value: string | null; -} - -interface IUserSettingsSetEmailMarketingSubscriptionStateAction { - type: 'setEmailMarketingSubscriptionState'; - value: BatchEmailSubscriptionState; -} - -interface IUserSettingsSetAttributionIdAction { - type: 'setAttributionId'; - value: string | null; -} - -interface IUserSettingsAddTagAction { - type: 'addTag'; - collection: string; - tag: string; -} - -interface IUserSettingsRemoveTagAction { - type: 'removeTag'; - collection: string; - tag: string; -} - -interface IUserSettingsClearTagCollectionAction { - type: 'clearTagCollection'; - collection: string; -} - -interface IUserSettingsClearTagsAction { - type: 'clearTags'; -} - -type IUserSettingsAction = - | IUserSettingsSetAttributeAction - | IUserSettingsRemoveAttributeAction - | IUserSettingsClearAttributesAction - | IUserSettingsSetDateAttributeAction - | IUserSettingsSetURLAttributeAction - | IUserSettingsSetIdentifierAction - | IUserSettingsSetLanguageAction - | IUserSettingsSetRegionAction - | IUserSettingsAddTagAction - | IUserSettingsRemoveTagAction - | IUserSettingsClearTagsAction - | IUserSettingsClearTagCollectionAction - | IUserSettingsSetEmailAction - | IUserSettingsSetEmailMarketingSubscriptionStateAction - | IUserSettingsSetAttributionIdAction; - -type IUserSettingsActions = IUserSettingsAction[]; - -/** - * Editor class used to create and save user tags and attributes - */ -export class BatchUserEditor { - private _settings: IUserSettingsActions; - - public constructor(settings: IUserSettingsActions = []) { - this._settings = settings; - } - - private addAction(action: IUserSettingsAction): BatchUserEditor { - this._settings.push(action); - return this; - } - - public setAttribute(key: string, value: string | boolean | number | null): BatchUserEditor { - return this.addAction({ - type: 'setAttribute', - key, - value, - }); - } - - public setDateAttribute(key: string, value: number): BatchUserEditor { - return this.addAction({ - type: 'setDateAttribute', - key, - value, - }); - } - - public setURLAttribute(key: string, value: string): BatchUserEditor { - return this.addAction({ - type: 'setURLAttribute', - key, - value, - }); - } - - public removeAttribute(key: string): BatchUserEditor { - return this.addAction({ - type: 'removeAttribute', - key, - }); - } - - public clearAttributes(): BatchUserEditor { - return this.addAction({ - type: 'clearAttributes', - }); - } - - public setIdentifier(value: string | null): BatchUserEditor { - return this.addAction({ - type: 'setIdentifier', - value, - }); - } - - public setEmail(value: string | null): BatchUserEditor { - return this.addAction({ - type: 'setEmail', - value, - }); - } - - public setEmailMarketingSubscriptionState(value: BatchEmailSubscriptionState): BatchUserEditor { - return this.addAction({ - type: 'setEmailMarketingSubscriptionState', - value, - }); - } - - public setLanguage(value: string | null): BatchUserEditor { - return this.addAction({ - type: 'setLanguage', - value, - }); - } - - public setRegion(value: string | null): BatchUserEditor { - return this.addAction({ - type: 'setRegion', - value, - }); - } - - public setAttributionIdentifier(value: string | null): BatchUserEditor { - return this.addAction({ - type: 'setAttributionId', - value, - }); - } - - public addTag(collection: string, tag: string): BatchUserEditor { - return this.addAction({ - type: 'addTag', - collection, - tag, - }); - } - - public removeTag(collection: string, tag: string): BatchUserEditor { - return this.addAction({ - type: 'removeTag', - collection, - tag, - }); - } - - public clearTagCollection(collection: string): BatchUserEditor { - return this.addAction({ - type: 'clearTagCollection', - collection, - }); - } - - public clearTags(): BatchUserEditor { - return this.addAction({ - type: 'clearTags', - }); - } - - public save(): void { - RNBatch.userData_save(this._settings); - } -} From d779e873d8e9e9d965e4d231a76ce92a7e349562 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Thu, 23 May 2024 13:51:34 +0200 Subject: [PATCH 09/30] android: add rich event compatibility --- .../com/batch/batch_rn/RNBatchModule.java | 4 +- .../main/java/com/batch/batch_rn/RNUtils.java | 38 ++--- src/Batch.ts | 2 +- ...chEventData.ts => BatchEventAttributes.ts} | 133 +++++++++++------- src/BatchEventData.test.ts | 16 +-- src/BatchProfile.ts | 6 +- src/helpers/TypeHelpers.ts | 18 +++ 7 files changed, 137 insertions(+), 80 deletions(-) rename src/{BatchEventData.ts => BatchEventAttributes.ts} (52%) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index 1900c67..c6ffe84 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -13,6 +13,7 @@ import com.batch.android.Batch; import com.batch.android.BatchActivityLifecycleHelper; import com.batch.android.BatchAttributesFetchListener; +import com.batch.android.BatchEventAttributes; import com.batch.android.BatchProfileAttributeEditor; import com.batch.android.BatchPushRegistration; import com.batch.android.BatchTagCollectionsFetchListener; @@ -662,7 +663,8 @@ public void userData_save(ReadableArray actions) { @ReactMethod public void userData_trackEvent(String name, ReadableMap serializedEventData) { - Batch.Profile.trackEvent(name, RNUtils.convertSerializedEventDataToEventAttributes(serializedEventData)); + BatchEventAttributes attributes = RNUtils.convertSerializedEventDataToEventAttributes(serializedEventData); + Batch.Profile.trackEvent(name, attributes); } @ReactMethod diff --git a/android/src/main/java/com/batch/batch_rn/RNUtils.java b/android/src/main/java/com/batch/batch_rn/RNUtils.java index 5a6eed0..f716268 100644 --- a/android/src/main/java/com/batch/batch_rn/RNUtils.java +++ b/android/src/main/java/com/batch/batch_rn/RNUtils.java @@ -80,29 +80,18 @@ public static WritableArray convertArrayToWritableArray(Object[] input) { } @Nullable - public static BatchEventAttributes convertSerializedEventDataToEventAttributes(@Nullable ReadableMap serializedEventData) { - if (serializedEventData == null) { + public static BatchEventAttributes convertSerializedEventDataToEventAttributes(@Nullable ReadableMap attributes) { + if (attributes == null) { return null; } - BatchEventAttributes eventAttributes = new BatchEventAttributes(); - ReadableArray tags = serializedEventData.getArray("$tags"); - if (tags != null && tags.size() > 0) { - eventAttributes.putStringList("$tags", convertReadableArrayToList(tags)); - } - - String label = serializedEventData.getString("$label"); - if (label != null) { - eventAttributes.put("$label", label); - } - - ReadableMap attributes = serializedEventData.getMap("attributes"); ReadableMapKeySetIterator iterator = attributes.keySetIterator(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); ReadableMap valueMap = attributes.getMap(key); String type = valueMap.getString("type"); + if ("string".equals(type)) { eventAttributes.put(key, valueMap.getString("value")); } else if ("boolean".equals(type)) { @@ -117,11 +106,28 @@ public static BatchEventAttributes convertSerializedEventDataToEventAttributes(@ eventAttributes.put(key, date); } else if ("url".equals(type)) { eventAttributes.put(key, URI.create(valueMap.getString("value"))); + } else if ("object".equals(type)) { + BatchEventAttributes object = convertSerializedEventDataToEventAttributes(valueMap.getMap("value")); + if (object != null) { + eventAttributes.put(key, object); + } + } else if ("string_array".equals(type)) { + List list = convertReadableArrayToList(valueMap.getArray("value")); + eventAttributes.putStringList(key, list); + } else if ("object_array".equals(type)) { + List list = new ArrayList<>(); + ReadableArray array = valueMap.getArray("value"); + for (int i = 0; i < array.size(); i++) { + BatchEventAttributes object = convertSerializedEventDataToEventAttributes(array.getMap(i)); + if (object != null) { + list.add(object); + } + } + eventAttributes.putObjectList(key, list); } else { - Log.e("RNBatchPush", "Invalid parameter : Unknown event_data.attributes type (" + type + ")"); + Log.e(RNBatchModule.LOGGER_TAG, "Invalid parameter : Unknown event_data.attributes type (" + type + ")"); } } - return eventAttributes; } diff --git a/src/Batch.ts b/src/Batch.ts index bfcdfe4..ddb2bc8 100644 --- a/src/Batch.ts +++ b/src/Batch.ts @@ -1,5 +1,5 @@ import { NativeModules, Platform } from 'react-native'; -export * from './BatchEventData'; +export * from './BatchEventAttributes'; export * from './BatchInbox'; export * from './BatchInboxFetcher'; export * from './BatchMessaging'; diff --git a/src/BatchEventData.ts b/src/BatchEventAttributes.ts similarity index 52% rename from src/BatchEventData.ts rename to src/BatchEventAttributes.ts index 433bef3..c91706f 100644 --- a/src/BatchEventData.ts +++ b/src/BatchEventAttributes.ts @@ -1,5 +1,5 @@ import { Log } from './helpers/Logger'; -import { isBoolean, isNumber, isString } from './helpers/TypeHelpers'; +import { isBoolean, isNumber, isObject, isObjectArray, isString, isStringArray } from './helpers/TypeHelpers'; export const Consts = { AttributeKeyRegexp: /^[a-zA-Z0-9_]{1,30}$/, @@ -16,52 +16,59 @@ export enum TypedEventAttributeType { Float = 'float', Date = 'date', URL = 'url', + StringArray = 'string_array', + ObjectArray = 'object_array', + Object = 'object', } +export type TypedEventAttributeValue = string | boolean | number | TypedEventAttributes | Array; + +export type TypedEventAttributes = { [key: string]: ITypedEventAttribute }; + export interface ITypedEventAttribute { type: TypedEventAttributeType; - value: string | boolean | number; + value: TypedEventAttributeValue; } -export class BatchEventData { - private _tags: { [key: string]: true }; // tslint:disable-line - private _attributes: { [key: string]: ITypedEventAttribute }; // tslint:disable-line +export class BatchEventAttributes { + private readonly _attributes: { [key: string]: ITypedEventAttribute }; // tslint:disable-line public constructor() { - this._tags = {}; this._attributes = {}; } - public addTag(tag: string): BatchEventData { - if (typeof tag === 'undefined') { - Log(false, 'BatchEventData - A tag is required'); - return this; - } - - if (isString(tag)) { - if (tag.length === 0 || tag.length > Consts.EventDataStringMaxLength) { - Log( - false, - "BatchEventData - Tags can't be empty or longer than " + - Consts.EventDataStringMaxLength + - " characters. Ignoring tag '" + - tag + - "'." - ); + private addTags(tags: Array): BatchEventAttributes { + tags.forEach(tag => { + if (typeof tag === 'undefined') { + Log(false, 'BatchEventData - A tag is required'); return this; } - } else { - Log(false, 'BatchEventData - Tag argument must be a string'); - return this; - } - - if (Object.keys(this._tags).length >= Consts.EventDataMaxTags) { - Log(false, 'BatchEventData - Event data cannot hold more than ' + Consts.EventDataMaxTags + " tags. Ignoring tag: '" + tag + "'"); - return this; - } - - this._tags[tag.toLowerCase()] = true; + if (isString(tag)) { + if (tag.length === 0 || tag.length > Consts.EventDataStringMaxLength) { + Log( + false, + "BatchEventData - Tags can't be empty or longer than " + + Consts.EventDataStringMaxLength + + " characters. Ignoring tag '" + + tag + + "'." + ); + return this; + } + } else { + Log(false, 'BatchEventData - Tag argument must be a string'); + return this; + } + if (tags.length >= Consts.EventDataMaxTags) { + Log(false, 'BatchEventData - Event data cannot hold more than ' + Consts.EventDataMaxTags + " tags. Ignoring tag: '" + tag + "'"); + return this; + } + }); + this._attributes['$tags'] = { + type: TypedEventAttributeType.StringArray, + value: tags, + }; return this; } @@ -72,14 +79,16 @@ export class BatchEventData { } if (!Consts.AttributeKeyRegexp.test(key || '')) { - Log( - false, - 'BatchEventData - Invalid key. Please make sure that the key is made of letters, underscores and numbers only (a-zA-Z0-9_).' + - "It also can't be longer than 30 characters. Ignoring attribute '" + - key + - "'" - ); - throw new Error(); + if (key !== '$tags' && key !== '$label') { + Log( + false, + 'BatchEventData - Invalid key. Please make sure that the key is made of letters, underscores and numbers only (a-zA-Z0-9_).' + + "It also can't be longer than 30 characters. Ignoring attribute '" + + key + + "'" + ); + throw new Error(); + } } if (typeof value === 'undefined' || value === null) { @@ -100,7 +109,7 @@ export class BatchEventData { return key.toLowerCase(); } - public putDate(key: string, value: number): BatchEventData { + public putDate(key: string, value: number): BatchEventAttributes { key = this.prepareAttributeKey(key); try { this.checkBeforePuttingAttribute(key, value); @@ -116,7 +125,7 @@ export class BatchEventData { return this; } - public putURL(key: string, url: string): BatchEventData { + public putURL(key: string, url: string): BatchEventAttributes { key = this.prepareAttributeKey(key); try { this.checkBeforePuttingAttribute(key, url); @@ -144,7 +153,10 @@ export class BatchEventData { return this; } - public put(key: string, value: string | number | boolean): BatchEventData { + public put( + key: string, + value: string | number | boolean | Array | BatchEventAttributes + ): BatchEventAttributes { key = this.prepareAttributeKey(key); try { @@ -153,8 +165,13 @@ export class BatchEventData { return this; } - let typedAttrValue: ITypedEventAttribute | undefined; + // Check if data contains legacy tags + if (key == '$tags' && isStringArray(value)) { + this.addTags(value); + return this; + } + let typedAttrValue: ITypedEventAttribute | undefined; if (isString(value)) { typedAttrValue = { type: TypedEventAttributeType.String, @@ -170,6 +187,25 @@ export class BatchEventData { type: TypedEventAttributeType.Boolean, value, }; + } else if (isObject(value)) { + typedAttrValue = { + type: TypedEventAttributeType.Object, + value: value._attributes, + }; + } else if (isStringArray(value)) { + typedAttrValue = { + type: TypedEventAttributeType.StringArray, + value, + }; + } else if (isObjectArray(value)) { + const array = []; + value.forEach(item => { + array.push(item._attributes); + }); + typedAttrValue = { + type: TypedEventAttributeType.ObjectArray, + value: array, + }; } else { Log(false, 'BatchEventData - Invalid attribute value type. Must be a string, number or boolean'); return this; @@ -178,14 +214,9 @@ export class BatchEventData { if (typedAttrValue) { this._attributes[key] = typedAttrValue; } - return this; } - protected _toInternalRepresentation(): unknown { - return { - attributes: this._attributes, - tags: Object.keys(this._tags), - }; + return this._attributes; } } diff --git a/src/BatchEventData.test.ts b/src/BatchEventData.test.ts index c571af3..9282a04 100644 --- a/src/BatchEventData.test.ts +++ b/src/BatchEventData.test.ts @@ -1,4 +1,4 @@ -import { BatchEventData, Consts } from './BatchEventData'; +import { BatchEventAttributes, Consts } from './BatchEventAttributes'; import * as Logger from './helpers/Logger'; describe('BatchEventData', () => { @@ -7,7 +7,7 @@ describe('BatchEventData', () => { }); it(`handles less than or equal ${Consts.EventDataMaxTags} tags`, () => { - const batchEventData = new BatchEventData(); + const batchEventData = new BatchEventAttributes(); const spy = jest.spyOn(Logger, 'Log'); for (let i = 1; i <= Consts.EventDataMaxTags; i++) { @@ -17,7 +17,7 @@ describe('BatchEventData', () => { expect(spy).not.toHaveBeenCalled(); }); it(`handles less than or equal ${Consts.EventDataMaxValues} attributes`, () => { - const batchEventData = new BatchEventData(); + const batchEventData = new BatchEventAttributes(); const spy = jest.spyOn(Logger, 'Log'); for (let i = 1; i <= Consts.EventDataMaxValues; i++) { @@ -27,7 +27,7 @@ describe('BatchEventData', () => { expect(spy).not.toHaveBeenCalled(); }); it(`skips other tags after the first ${Consts.EventDataMaxTags}`, () => { - const batchEventData = new BatchEventData(); + const batchEventData = new BatchEventAttributes(); const spy = jest.spyOn(Logger, 'Log'); for (let i = 1; i <= Consts.EventDataMaxTags; i++) { @@ -39,7 +39,7 @@ describe('BatchEventData', () => { expect(spy).toHaveBeenCalled(); }); it(`skips other attributes after the first ${Consts.EventDataMaxValues}`, () => { - const batchEventData = new BatchEventData(); + const batchEventData = new BatchEventAttributes(); const spy = jest.spyOn(Logger, 'Log'); for (let i = 1; i <= Consts.EventDataMaxValues; i++) { @@ -51,7 +51,7 @@ describe('BatchEventData', () => { expect(spy).toHaveBeenCalled(); }); it(`handles a date attribute`, () => { - const batchEventData = new BatchEventData(); + const batchEventData = new BatchEventAttributes(); const spy = jest.spyOn(Logger, 'Log'); batchEventData.putDate('test_date', Date.now()); @@ -59,7 +59,7 @@ describe('BatchEventData', () => { expect(spy).not.toHaveBeenCalled(); }); it(`handles an url attribute`, () => { - const batchEventData = new BatchEventData(); + const batchEventData = new BatchEventAttributes(); const spy = jest.spyOn(Logger, 'Log'); batchEventData.putURL('test_url', 'https://batch.com'); @@ -67,7 +67,7 @@ describe('BatchEventData', () => { expect(spy).not.toHaveBeenCalled(); }); it(`skips a too long url attribute`, () => { - const batchEventData = new BatchEventData(); + const batchEventData = new BatchEventAttributes(); const spy = jest.spyOn(Logger, 'Log'); batchEventData.putURL( diff --git a/src/BatchProfile.ts b/src/BatchProfile.ts index 0e4b778..08aef59 100644 --- a/src/BatchProfile.ts +++ b/src/BatchProfile.ts @@ -1,6 +1,6 @@ import { NativeModules } from 'react-native'; -import { BatchEventData } from './BatchEventData'; +import { BatchEventAttributes } from './BatchEventAttributes'; import { BatchProfileAttributeEditor } from './BatchProfileAttributeEditor'; import Log from './helpers/Logger'; @@ -47,10 +47,10 @@ export const BatchProfile = { * @param name The event name. Must be a string. * @param data The event data (optional). Must be an object. */ - trackEvent: (name: string, data?: BatchEventData): void => { + trackEvent: (name: string, data?: BatchEventAttributes): void => { // Since _toInternalRepresentation is private, we have to resort to this little hack to access the method. // That syntax keeps the argument type checking, while casting as any would not. - RNBatch.userData_trackEvent(name, data instanceof BatchEventData ? data['_toInternalRepresentation']() : null); + RNBatch.userData_trackEvent(name, data instanceof BatchEventAttributes ? data['_toInternalRepresentation']() : null); }, /** diff --git a/src/helpers/TypeHelpers.ts b/src/helpers/TypeHelpers.ts index e1fb55f..cd2920e 100644 --- a/src/helpers/TypeHelpers.ts +++ b/src/helpers/TypeHelpers.ts @@ -1,3 +1,5 @@ +import { BatchEventAttributes } from '../BatchEventAttributes'; + export function isString(value: unknown): value is string { return value instanceof String || typeof value === 'string'; } @@ -9,3 +11,19 @@ export function isNumber(value: unknown): value is number { export function isBoolean(value: unknown): value is boolean { return value instanceof Boolean || typeof value === 'boolean'; } + +export const isObject = (value: unknown): value is BatchEventAttributes => { + return value instanceof BatchEventAttributes; +}; + +export function isArray(value: unknown): value is Array { + return Array.isArray(value); +} + +export function isStringArray(value: unknown): value is Array { + return isArray(value) && value.every(it => isString(it)); +} + +export function isObjectArray(value: unknown): value is Array { + return isArray(value) && value.every(it => isObject(it)); +} From 9cf117fbe2d6f576eec511c20dc23a7d9659eb3b Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 24 May 2024 10:37:35 +0200 Subject: [PATCH 10/30] ios: bump batch to 2.0 --- RNBatchPush.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RNBatchPush.podspec b/RNBatchPush.podspec index 53cfe7b..9bfa9c0 100644 --- a/RNBatchPush.podspec +++ b/RNBatchPush.podspec @@ -13,5 +13,5 @@ Pod::Spec.new do |s| s.requires_arc = true s.dependency "React" - s.dependency 'Batch', '~> 1.21.0' + s.dependency 'Batch', '~> 2.0.0' end From 3f1a554328e266a20473d44d07c063805837bf92 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 24 May 2024 10:38:57 +0200 Subject: [PATCH 11/30] ios: bump min ios to 13 --- RNBatchPush.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RNBatchPush.podspec b/RNBatchPush.podspec index 9bfa9c0..c34f6b0 100644 --- a/RNBatchPush.podspec +++ b/RNBatchPush.podspec @@ -7,7 +7,7 @@ Pod::Spec.new do |s| s.authors = { "Batch.com" => "support@batch.com" } - s.platform = :ios, "12.0" + s.platform = :ios, "13.0" s.source = { :git => "git@github.com:BatchLabs/Batch-React-Native-Plugin.git", :tag => "master" } s.source_files = "ios/*.{h,m}" s.requires_arc = true From b9bbc492ce89edb37e1acb2164e66e0c10225833 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 24 May 2024 10:41:56 +0200 Subject: [PATCH 12/30] ios: compat batch sdk 2.0 --- ios/RNBatch.m | 286 +++++++++++++++++++++++++++----------------------- 1 file changed, 153 insertions(+), 133 deletions(-) diff --git a/ios/RNBatch.m b/ios/RNBatch.m index 73c7fa2..8cd3748 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -47,7 +47,7 @@ + (void)start } NSString *batchAPIKey = [info objectForKey:@"BatchAPIKey"]; - [Batch startWithAPIKey:batchAPIKey]; + [BatchSDK startWithAPIKey:batchAPIKey]; dispatcher = [[RNBatchEventDispatcher alloc] init]; [BatchEventDispatcher addDispatcher:dispatcher]; } @@ -78,7 +78,7 @@ -(void)stopObserving { RCT_EXPORT_METHOD(optIn:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - [Batch optIn]; + [BatchSDK optIn]; [RNBatch start]; resolve([NSNull null]); } @@ -86,21 +86,21 @@ -(void)stopObserving { RCT_EXPORT_METHOD(optOut:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - [Batch optOut]; + [BatchSDK optOut]; resolve([NSNull null]); } RCT_EXPORT_METHOD(optOutAndWipeData:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - [Batch optOutAndWipeData]; + [BatchSDK optOutAndWipeData]; resolve([NSNull null]); } RCT_EXPORT_METHOD(presentDebugViewController) { dispatch_async(dispatch_get_main_queue(), ^{ - UIViewController *debugVC = [Batch debugViewController]; + UIViewController *debugVC = [BatchSDK makeDebugViewController]; if (debugVC) { [RCTPresentedViewController() presentViewController:debugVC animated:YES completion:nil]; } @@ -108,12 +108,6 @@ -(void)stopObserving { } // Push Module - -RCT_EXPORT_METHOD(push_registerForRemoteNotifications) -{ - [BatchPush registerForRemoteNotifications]; -} - RCT_EXPORT_METHOD(push_requestNotificationAuthorization) { [BatchPush requestNotificationAuthorization]; @@ -268,47 +262,52 @@ -(void)stopObserving { RCT_EXPORT_METHOD(userData_save:(NSArray*)actions) { - BatchUserDataEditor *editor = [BatchUser editor]; + BatchProfileEditor *editor = [BatchProfile editor]; for (NSDictionary* action in actions) { NSString* type = action[@"type"]; + NSString* key = action[@"key"]; // Set double, long, NSString, bool class values if([type isEqualToString:@"setAttribute"]) { - [editor setAttribute:[self safeNilValue:action[@"value"]] forKey:action[@"key"]]; - + id value = action[@"value"]; + if ([value isKindOfClass:[NSString class]]) { + [editor setStringAttribute:value forKey:key error:nil]; + } else if([value isKindOfClass:[NSNumber class]]) { + if (value == (id)kCFBooleanTrue || value == (id)kCFBooleanFalse) { + [editor setBooleanAttribute:[value boolValue] forKey:key error:nil]; + } else { + [editor setDoubleAttribute:[value doubleValue] forKey:key error:nil]; + } + } else if([value isKindOfClass:[NSString class]]) { + [editor setStringAttribute:value forKey:key error:nil]; + } else if([value isKindOfClass:[NSArray class]]) { + [editor setStringArrayAttribute:value forKey:key error:nil]; + } else if([value isKindOfClass:[NSNull class]]) { + [editor removeAttributeForKey:key error:nil]; + } } - // Handle dates // @TODO: prevent date parsing from erroring else if([type isEqualToString:@"setDateAttribute"]) { double timestamp = [action[@"value"] doubleValue]; NSTimeInterval unixTimeStamp = timestamp / 1000.0; NSDate *date = [NSDate dateWithTimeIntervalSince1970:unixTimeStamp]; - [editor setAttribute:date forKey:action[@"key"]]; + [editor setDateAttribute:date forKey:key error:nil]; } - else if([type isEqualToString:@"setURLAttribute"]) { NSURL *url = [NSURL URLWithString:[self safeNilValue:action[@"value"]]]; - [editor setAttribute:url forKey:action[@"key"]]; - } + [editor setURLAttribute:url forKey:action[@"key"] error:nil]; - else if([type isEqualToString:@"removeAttribute"]) { - [editor removeAttributeForKey:action[@"key"]]; } - - else if([type isEqualToString:@"clearAttributes"]) { - [editor clearAttributes]; - } - - else if([type isEqualToString:@"setIdentifier"]) { - [editor setIdentifier:[self safeNilValue:action[@"value"]]]; + else if([type isEqualToString:@"removeAttribute"]) { + [editor removeAttributeForKey:action[@"key"] error:nil]; } - else if([type isEqualToString:@"setEmail"]) { - [editor setEmail:[self safeNilValue:action[@"value"]] error:nil]; + else if([type isEqualToString:@"setEmailAddress"]) { + [editor setEmailAddress:[self safeNilValue:action[@"value"]] error:nil]; } - else if([type isEqualToString:@"setEmailMarketingSubscriptionState"]) { + else if([type isEqualToString:@"setEmailMarketingSubscription"]) { NSString* value = action[@"value"]; if([value isEqualToString:@"SUBSCRIBED"]) { [editor setEmailMarketingSubscriptionState:BatchEmailSubscriptionStateSubscribed]; @@ -318,31 +317,33 @@ -(void)stopObserving { } else if([type isEqualToString:@"setLanguage"]) { - [editor setLanguage:[self safeNilValue:action[@"value"]]]; + [editor setLanguage:[self safeNilValue:action[@"value"]] error:nil]; } else if([type isEqualToString:@"setRegion"]) { - [editor setRegion:[self safeNilValue:action[@"value"]]]; - } - - else if([type isEqualToString:@"setAttributionId"]) { - [editor setAttributionIdentifier:[self safeNilValue:action[@"value"]]]; - } - - else if([type isEqualToString:@"addTag"]) { - [editor addTag:action[@"tag"] inCollection:action[@"collection"]]; - } - - else if([type isEqualToString:@"removeTag"]) { - [editor removeTag:action[@"tag"] fromCollection:action[@"collection"]]; + [editor setRegion:[self safeNilValue:action[@"value"]] error:nil]; } - else if([type isEqualToString:@"clearTagCollection"]) { - [editor clearTagCollection:action[@"collection"]]; + else if([type isEqualToString:@"addToArray"]) { + id value = action[@"value"]; + if ([value isKindOfClass:[NSString class]]) { + [editor addItemToStringArrayAttribute:value forKey:key error:nil]; + } else if ([value isKindOfClass:[NSArray class]]) { + for (NSString *item in value) { + [editor addItemToStringArrayAttribute:item forKey:key error:nil]; + } + } } - else if([type isEqualToString:@"clearTags"]) { - [editor clearTags]; + else if([type isEqualToString:@"removeFromArray"]) { + id value = action[@"value"]; + if ([value isKindOfClass:[NSString class]]) { + [editor removeItemFromStringArrayAttribute:value forKey:key error:nil]; + } else if ([value isKindOfClass:[NSArray class]]) { + for (NSString *item in value) { + [editor removeItemFromStringArrayAttribute:item forKey:key error:nil]; + } + } } } [editor save]; @@ -350,107 +351,127 @@ -(void)stopObserving { // Event tracking -RCT_EXPORT_METHOD(userData_trackEvent:(NSString*)name label:(NSString*)label data:(NSDictionary*)serializedEventData) +RCT_EXPORT_METHOD(userData_trackEvent:(NSString*)name data:(NSDictionary*)serializedEventData) { - BatchEventData *batchEventData = nil; + BatchEventAttributes *batchEventAttributes = nil; - if ([serializedEventData isKindOfClass:[NSDictionary class]]) - { - batchEventData = [BatchEventData new]; + if ([serializedEventData isKindOfClass:[NSDictionary class]]) { - if (![serializedEventData isKindOfClass:[NSDictionary class]]) - { - NSLog(@"RNBatch: Error while tracking event data: event data should be an object or null"); + batchEventAttributes = [self convertSerializedEventDataToEventAttributes:serializedEventData]; + + NSError *err; + [batchEventAttributes validateWithError:&err]; + if (batchEventAttributes != nil && err == nil) { + [BatchProfile trackEventWithName:name attributes:batchEventAttributes]; + } else { + NSLog(@"Event validation error: %@", err.description); return; } + } + [BatchProfile trackEventWithName:name attributes:batchEventAttributes]; +} - NSArray* tags = serializedEventData[@"tags"]; - NSDictionary* attributes = serializedEventData[@"attributes"]; +- (BatchEventAttributes*) convertSerializedEventDataToEventAttributes:(NSDictionary *) serializedAttributes { - if (![tags isKindOfClass:[NSArray class]]) - { - NSLog(@"RNBatch: Error while tracking event data: event data.tags should be an array"); - return; - } - if (![attributes isKindOfClass:[NSDictionary class]]) + BatchEventAttributes *eventAttributes = [BatchEventAttributes new]; + + if (![serializedAttributes isKindOfClass:[NSDictionary class]]) { + NSLog(@"RNBatch: Error while tracking event data: event data.attributes should be a dictionary"); + return nil; + } + + for (NSString *key in serializedAttributes.allKeys) { + NSDictionary *typedAttribute = serializedAttributes[key]; + if (![typedAttribute isKindOfClass:[NSDictionary class]]) { - NSLog(@"RNBatch: Error while tracking event data: event data.attributes should be a dictionnary"); - return; + NSLog(@"RNBatch: Error while tracking event data: event data.attributes childrens should all be String/Dictionary tuples"); + return nil; } - for (NSString *tag in tags) - { - if (![tag isKindOfClass:[NSString class]]) + NSString *type = typedAttribute[@"type"]; + NSObject *value = typedAttribute[@"value"]; + + if ([@"string" isEqualToString:type]) { + if (![value isKindOfClass:[NSString class]]) { - NSLog(@"RNBatch: Error while tracking event data: event data.tag childrens should all be strings"); - return; + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected string value, got something else"); + return nil; } - [batchEventData addTag:tag]; - } - - for (NSString *key in attributes.allKeys) - { - NSDictionary *typedAttribute = attributes[key]; - if (![typedAttribute isKindOfClass:[NSDictionary class]]) + [eventAttributes putString:(NSString*)value forKey:key]; + } else if ([@"boolean" isEqualToString:type]) { + if (![value isKindOfClass:[NSNumber class]]) { - NSLog(@"RNBatch: Error while tracking event data: event data.attributes childrens should all be String/Dictionary tuples"); - return; + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected number (boolean) value, got something else"); + return nil; + } + [eventAttributes putBool:[(NSNumber*)value boolValue] forKey:key]; + } else if ([@"integer" isEqualToString:type]) { + if (![value isKindOfClass:[NSNumber class]]) + { + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected number (integer) value, got something else"); + return nil; + } + [eventAttributes putInteger:[(NSNumber*)value integerValue] forKey:key]; + } else if ([@"float" isEqualToString:type]) { + if (![value isKindOfClass:[NSNumber class]]) + { + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected number (float) value, got something else"); + return nil; + } + [eventAttributes putDouble:[(NSNumber*)value doubleValue] forKey:key]; + } else if ([@"date" isEqualToString:type]) { + if (![value isKindOfClass:[NSNumber class]]) + { + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected number value, got something else"); + return nil; + } + NSDate *date = [NSDate dateWithTimeIntervalSince1970:[(NSNumber*)value doubleValue] / 1000.0]; + [eventAttributes putDate:date forKey:key]; + } else if ([@"url" isEqualToString:type]) { + if (![value isKindOfClass:[NSString class]]) + { + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected string value, got something else"); + return nil; + } + [eventAttributes putURL:[NSURL URLWithString:(NSString*) value] forKey:key]; + } + else if ([@"object" isEqualToString:type]) { + if (![value isKindOfClass:[NSDictionary class]]){ + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected dictionnary value, got something else"); + return nil; + } + BatchEventAttributes *attributes = [self convertSerializedEventDataToEventAttributes:(NSDictionary*)value]; + if (attributes != nil) { + [eventAttributes putObject:attributes forKey:key]; + } + } + else if ([@"string_array" isEqualToString:type]) { + if (![value isKindOfClass:[NSArray class]]){ + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected string array value, got something else"); + return nil; } - NSString *type = typedAttribute[@"type"]; - NSObject *value = typedAttribute[@"value"]; - - if ([@"string" isEqualToString:type]) { - if (![value isKindOfClass:[NSString class]]) - { - NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected string value, got something else"); - return; - } - [batchEventData putString:(NSString*)value forKey:key]; - } else if ([@"boolean" isEqualToString:type]) { - if (![value isKindOfClass:[NSNumber class]]) - { - NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected number (boolean) value, got something else"); - return; - } - [batchEventData putBool:[(NSNumber*)value boolValue] forKey:key]; - } else if ([@"integer" isEqualToString:type]) { - if (![value isKindOfClass:[NSNumber class]]) - { - NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected number (integer) value, got something else"); - return; - } - [batchEventData putInteger:[(NSNumber*)value integerValue] forKey:key]; - } else if ([@"float" isEqualToString:type]) { - if (![value isKindOfClass:[NSNumber class]]) - { - NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected number (float) value, got something else"); - return; - } - [batchEventData putDouble:[(NSNumber*)value doubleValue] forKey:key]; - } else if ([@"date" isEqualToString:type]) { - if (![value isKindOfClass:[NSNumber class]]) - { - NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected number value, got something else"); - return; - } - NSDate *date = [NSDate dateWithTimeIntervalSince1970:[(NSNumber*)value doubleValue] / 1000.0]; - [batchEventData putDate:date forKey:key]; - } else if ([@"url" isEqualToString:type]) { - if (![value isKindOfClass:[NSString class]]) - { - NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected string value, got something else"); - return; + [eventAttributes putStringArray:(NSArray*)value forKey:key]; + } else if ([@"object_array" isEqualToString:type]) { + if (![value isKindOfClass:[NSArray class]]) { + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected dictionnary value, got something else"); + return nil; + } + NSMutableArray *list = [NSMutableArray array]; + NSArray *array = (NSArray*)value; + for (int i = 0; i < array.count; i++) { + BatchEventAttributes *object = [self convertSerializedEventDataToEventAttributes:array[i]]; + if (object != nil) { + [list addObject:object]; } - [batchEventData putURL:[NSURL URLWithString:(NSString*) value] forKey:key]; - } else { - NSLog(@"RNBatch: Error while tracking event data: Unknown event data.attributes type"); - return; } + [eventAttributes putObjectArray:list forKey:key]; + }else { + NSLog(@"RNBatch: Error while tracking event data: Unknown event data.attributes type"); + return nil; } } - - [BatchUser trackEvent:name withLabel:label associatedData:batchEventData]; + return eventAttributes; } RCT_EXPORT_METHOD(userData_trackLocation:(NSDictionary*)serializedLocation) @@ -503,7 +524,7 @@ -(void)stopObserving { } } - [BatchUser trackLocation:[[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake([latitude doubleValue], [longitude doubleValue]) + [BatchProfile trackLocation:[[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake([latitude doubleValue], [longitude doubleValue]) altitude:0 horizontalAccuracy:parsedPrecision verticalAccuracy:-1 @@ -736,7 +757,6 @@ - (BatchInboxNotificationContent *) findNotificationInList: (NSArray Date: Mon, 27 May 2024 16:45:32 +0200 Subject: [PATCH 13/30] bridge: add profile identify api --- .../main/java/com/batch/batch_rn/RNBatchModule.java | 5 +++++ ios/RNBatch.m | 5 +++++ src/BatchProfile.ts | 11 ++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index c6ffe84..10b02aa 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -661,6 +661,11 @@ public void userData_save(ReadableArray actions) { editor.save(); } + @ReactMethod + public void profile_identify(String identifier) { + Batch.Profile.identify(identifier); + } + @ReactMethod public void userData_trackEvent(String name, ReadableMap serializedEventData) { BatchEventAttributes attributes = RNUtils.convertSerializedEventDataToEventAttributes(serializedEventData); diff --git a/ios/RNBatch.m b/ios/RNBatch.m index 8cd3748..462b981 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -349,6 +349,11 @@ -(void)stopObserving { [editor save]; } +RCT_EXPORT_METHOD(profile_identify:(NSString*)identifier) +{ + [BatchProfile identify:identifier]; +} + // Event tracking RCT_EXPORT_METHOD(userData_trackEvent:(NSString*)name data:(NSDictionary*)serializedEventData) diff --git a/src/BatchProfile.ts b/src/BatchProfile.ts index 08aef59..54671b3 100644 --- a/src/BatchProfile.ts +++ b/src/BatchProfile.ts @@ -42,10 +42,19 @@ export const BatchProfile = { */ editor: (): BatchProfileAttributeEditor => new BatchProfileAttributeEditor(), + /** + * Identifies this device with a profile using a Custom User ID. + * @param {string | null} identifier - Custom user ID of the profile you want to identify against. + * If a profile already exists, this device will be attached to it. Must not be longer than 1024 characters. + */ + identify: (identifier: string | null): void => { + RNBatch.profile_identify(identifier); + }, + /** * Track an event. Batch must be started at some point, or events won't be sent to the server. * @param name The event name. Must be a string. - * @param data The event data (optional). Must be an object. + * @param data The event attributes (optional). Must be an object. */ trackEvent: (name: string, data?: BatchEventAttributes): void => { // Since _toInternalRepresentation is private, we have to resort to this little hack to access the method. From 3e2fc507092e4f6e5c0069f0474af5410c885bb0 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Mon, 27 May 2024 17:17:16 +0200 Subject: [PATCH 14/30] bridge: add isOptedOut api --- .../src/main/java/com/batch/batch_rn/RNBatchModule.java | 5 +++++ ios/RNBatch.m | 6 ++++++ src/Batch.ts | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index 10b02aa..a224013 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -145,6 +145,11 @@ public void optOutAndWipeData(Promise promise) { promise.resolve(null); } + @ReactMethod + public void isOptedOut(Promise promise) { + boolean isOptedOut = Batch.isOptedOut(reactContext); + promise.resolve(isOptedOut); + } @ReactMethod public void addListener(String eventName) { diff --git a/ios/RNBatch.m b/ios/RNBatch.m index 462b981..1010928 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -97,6 +97,12 @@ -(void)stopObserving { resolve([NSNull null]); } +RCT_EXPORT_METHOD(isOptedOut:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + resolve([NSNumber numberWithBool:BatchSDK.isOptedOut]); +} + RCT_EXPORT_METHOD(presentDebugViewController) { dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/src/Batch.ts b/src/Batch.ts index ddb2bc8..33a5197 100644 --- a/src/Batch.ts +++ b/src/Batch.ts @@ -61,6 +61,14 @@ export const Batch = { */ optOutAndWipeData: (): Promise => RNBatch.optOutAndWipeData(), + /** + * Checks whether Batch has been opted out from or not. + * + * @returns {Promise} A promise that resolves to a boolean value indicating whether Batch has been + * opted out from or not. + */ + isOptedOut: (): Promise => RNBatch.isOptedOut(), + /** * Shows debug view * From 1285a5adec9ba5c8bf89f604ef55954e36a90a2e Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Tue, 28 May 2024 14:59:56 +0200 Subject: [PATCH 15/30] bridge: add updateDataCollection api --- .../com/batch/batch_rn/RNBatchModule.java | 24 ++++++++++++ ios/RNBatch.m | 18 +++++++++ src/Batch.ts | 39 +++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index a224013..b9f1221 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -6,6 +6,7 @@ import android.content.res.Resources; import android.graphics.Typeface; import android.location.Location; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -13,6 +14,7 @@ import com.batch.android.Batch; import com.batch.android.BatchActivityLifecycleHelper; import com.batch.android.BatchAttributesFetchListener; +import com.batch.android.BatchDataCollectionConfig; import com.batch.android.BatchEventAttributes; import com.batch.android.BatchProfileAttributeEditor; import com.batch.android.BatchPushRegistration; @@ -151,6 +153,28 @@ public void isOptedOut(Promise promise) { promise.resolve(isOptedOut); } + @ReactMethod + public void updateAutomaticDataCollection(@NonNull ReadableMap dataCollectionConfig) { + boolean hasDeviceBrand = dataCollectionConfig.hasKey("deviceBrand"); + boolean hasDeviceModel = dataCollectionConfig.hasKey("deviceModel"); + boolean hasGeoIP = dataCollectionConfig.hasKey("geoIP"); + if (hasDeviceBrand || hasDeviceModel || hasGeoIP) { + Batch.updateAutomaticDataCollection(batchDataCollectionConfig -> { + if (hasDeviceBrand) { + batchDataCollectionConfig.setDeviceBrandEnabled(dataCollectionConfig.getBoolean("deviceBrand")); + } + if (hasDeviceModel) { + batchDataCollectionConfig.setDeviceModelEnabled(dataCollectionConfig.getBoolean("deviceModel")); + } + if (hasGeoIP) { + batchDataCollectionConfig.setGeoIPEnabled(dataCollectionConfig.getBoolean("geoIP")); + } + }); + } else { + Log.e(RNBatchModule.LOGGER_TAG, "Invalid parameter : Data collection config cannot be empty"); + } + } + @ReactMethod public void addListener(String eventName) { eventDispatcher.setHasListener(true); diff --git a/ios/RNBatch.m b/ios/RNBatch.m index 1010928..14a18ca 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -103,6 +103,24 @@ -(void)stopObserving { resolve([NSNumber numberWithBool:BatchSDK.isOptedOut]); } +RCT_EXPORT_METHOD(updateAutomaticDataCollection:(NSDictionary *)dataCollectionConfig) { + BOOL hasDeviceModel = [dataCollectionConfig objectForKey:@"deviceModel"] != nil; + BOOL hasGeoIP = [dataCollectionConfig objectForKey:@"geoIP"] != nil; + + if (hasDeviceModel || hasGeoIP) { + [BatchSDK updateAutomaticDataCollection:^(BatchDataCollectionConfig * _Nonnull batchDataCollectionConfig) { + if (hasDeviceModel) { + batchDataCollectionConfig.deviceModelEnabled = [dataCollectionConfig[@"deviceModel"] boolValue]; + } + if (hasGeoIP) { + batchDataCollectionConfig.geoIPEnabled = [dataCollectionConfig[@"geoIP"] boolValue]; + } + }]; + } else { + NSLog(@"BatchBridge - Invalid parameter: Data collection config cannot be empty."); + } +} + RCT_EXPORT_METHOD(presentDebugViewController) { dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/src/Batch.ts b/src/Batch.ts index 33a5197..3fe375a 100644 --- a/src/Batch.ts +++ b/src/Batch.ts @@ -7,9 +7,31 @@ export * from './BatchPush'; export * from './BatchUser'; export * from './BatchUserAttribute'; export * from './BatchEventEmitter'; +export * from './BatchProfile'; const RNBatch = NativeModules.RNBatch; +/** + * Object holding the configuration parameters for the automatic data collect. + */ +export interface DataCollectionConfig { + /** + * Whether Batch can send the device brand information. (Android only) + * @defaultValue false + */ + deviceBrand?: boolean; + /** + * Whether Batch can send the device model information. + * @defaultValue false + */ + deviceModel?: boolean; + /** + * Whether Batch can resolve the GeoIP on server side. + * @defaultValue false + */ + geoIP?: boolean; +} + /** * Batch React-Native Module */ @@ -69,6 +91,23 @@ export const Batch = { */ isOptedOut: (): Promise => RNBatch.isOptedOut(), + /** + * Configure the SDK Automatic Data Collection. + * + * @param {DataCollectionConfig} dataCollection A configuration object to fine-tune the data you authorize to be tracked by Batch. + * @see {@link DataCollectionConfig} for more info. + * @example + * Here's an example: + * ``` + * Batch.updateAutomaticDataCollection({ + * geoIP: false, // Deny Batch from resolving the user's region from the ip address. + * deviceModel: true // Authorize Batch to use the user's device model information. + * }); + * ``` + * @remarks Batch will persist the changes, so you can call this method at any time according to user consent. + */ + updateAutomaticDataCollection: (dataCollection: DataCollectionConfig): void => RNBatch.updateAutomaticDataCollection(dataCollection), + /** * Shows debug view * From 0af4e3c2a99337c1eb0247691fa6df0707b31bf9 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Wed, 29 May 2024 15:14:19 +0200 Subject: [PATCH 16/30] bridge: add default profile migrations --- .../com/batch/batch_rn/RNBatchModule.java | 30 +++++++++++++++++++ ios/RNBatch.m | 15 ++++++++++ 2 files changed, 45 insertions(+) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index b9f1221..cfcf6a3 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -2,10 +2,13 @@ import android.app.Activity; import android.app.Application; +import android.content.Context; +import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.Typeface; import android.location.Location; +import android.os.Bundle; import android.util.Log; import androidx.annotation.NonNull; @@ -16,6 +19,7 @@ import com.batch.android.BatchAttributesFetchListener; import com.batch.android.BatchDataCollectionConfig; import com.batch.android.BatchEventAttributes; +import com.batch.android.BatchMigration; import com.batch.android.BatchProfileAttributeEditor; import com.batch.android.BatchPushRegistration; import com.batch.android.BatchTagCollectionsFetchListener; @@ -94,6 +98,7 @@ public static void initialize(Application application) { if (!isInitialized) { Resources resources = application.getResources(); String packageName = application.getPackageName(); + setDefaultProfileMigrations(application.getApplicationContext(), packageName); String batchAPIKey = resources.getString(resources.getIdentifier("BATCH_API_KEY", "string", packageName)); Batch.start(batchAPIKey); Batch.EventDispatcher.addDispatcher(eventDispatcher); @@ -110,6 +115,31 @@ public static void initialize(Application application) { } } + private static void setDefaultProfileMigrations(Context context, String packageName) { + try { + Bundle metaData = context.getPackageManager() + .getApplicationInfo(packageName, PackageManager.GET_META_DATA) + .metaData; + if (metaData != null) { + boolean profileCustomIdMigrationEnabled = metaData.getBoolean("batch.profile_custom_id_migration_enabled", true); + boolean profileCustomDataMigrationEnabled = metaData.getBoolean("batch.profile_custom_data_migration_enabled", true); + EnumSet migrations = EnumSet.noneOf(BatchMigration.class); + if (!profileCustomIdMigrationEnabled) { + Log.d(RNBatchModule.LOGGER_TAG, "Disabling profile custom id migration."); + migrations.add(BatchMigration.CUSTOM_ID); + } + if (!profileCustomDataMigrationEnabled) { + Log.d(RNBatchModule.LOGGER_TAG, "Disabling profile custom data migration."); + migrations.add(BatchMigration.CUSTOM_DATA); + } + Batch.disableMigration(migrations); + } + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + public RNBatchModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext = reactContext; diff --git a/ios/RNBatch.m b/ios/RNBatch.m index 14a18ca..bf7235c 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -39,6 +39,7 @@ + (void)start NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; + // Handling do not disturbed initial state id doNotDisturbEnabled = [info objectForKey:@"BatchDoNotDisturbInitialState"]; if (doNotDisturbEnabled != nil) { [BatchMessaging setDoNotDisturb:[doNotDisturbEnabled boolValue]]; @@ -46,6 +47,20 @@ + (void)start [BatchMessaging setDoNotDisturb:false]; } + // Handling profile migrations state + id profileCustomIDMigrationEnabled = [info objectForKey:@"BatchProfileCustomIdMigrationEnabled"]; + id profileCustomDataMigrationEnabled = [info objectForKey:@"BatchProfileCustomDataMigrationEnabled"]; + BatchMigration migrations = BatchMigrationNone; + if (profileCustomIDMigrationEnabled != nil && [profileCustomIDMigrationEnabled boolValue] == false) { + NSLog(@"[BatchBridge] Disabling profile custom id migration"); + migrations |= BatchMigrationCustomID; + } + if (profileCustomDataMigrationEnabled != nil && [profileCustomDataMigrationEnabled boolValue] == false) { + NSLog(@"[BatchBridge] Disabling profile custom data migration"); + migrations |= BatchMigrationCustomData; + } + [BatchSDK setDisabledMigrations:migrations]; + NSString *batchAPIKey = [info objectForKey:@"BatchAPIKey"]; [BatchSDK startWithAPIKey:batchAPIKey]; dispatcher = [[RNBatchEventDispatcher alloc] init]; From 160679dddcc90ed42f0a0ba0773722ebf93018f2 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Wed, 29 May 2024 15:37:31 +0200 Subject: [PATCH 17/30] bridge: add clear install data api --- .../src/main/java/com/batch/batch_rn/RNBatchModule.java | 5 +++++ ios/RNBatch.m | 7 ++++++- src/BatchUser.ts | 6 ++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index cfcf6a3..f6e4e89 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -720,6 +720,11 @@ public void userData_save(ReadableArray actions) { editor.save(); } + @ReactMethod + public void userData_clearInstallationData() { + Batch.User.clearInstallationData(); + } + @ReactMethod public void profile_identify(String identifier) { Batch.Profile.identify(identifier); diff --git a/ios/RNBatch.m b/ios/RNBatch.m index bf7235c..93c9e44 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -388,6 +388,11 @@ -(void)stopObserving { [editor save]; } +RCT_EXPORT_METHOD(userData_clearInstallationData) +{ + [BatchUser clearInstallationData]; +} + RCT_EXPORT_METHOD(profile_identify:(NSString*)identifier) { [BatchProfile identify:identifier]; @@ -481,7 +486,7 @@ - (BatchEventAttributes*) convertSerializedEventDataToEventAttributes:(NSDiction } else if ([@"object" isEqualToString:type]) { if (![value isKindOfClass:[NSDictionary class]]){ - NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected dictionnary value, got something else"); + NSLog(@"RNBatch: Error while tracking event data: event data.attributes: expected dictionary value, got something else"); return nil; } BatchEventAttributes *attributes = [self convertSerializedEventDataToEventAttributes:(NSDictionary*)value]; diff --git a/src/BatchUser.ts b/src/BatchUser.ts index 3f6ffea..e2fd78a 100644 --- a/src/BatchUser.ts +++ b/src/BatchUser.ts @@ -54,4 +54,10 @@ export const BatchUser = { * @returns The tags added with BatchUser.editor().addTag() */ getTagCollections: (): Promise<{ [key: string]: string[] }> => RNBatch.userData_getTags(), + + /** + * Clear all tags and attributes set on an installation and their local cache returned by fetchAttributes and fetchTagCollections. + * This doesn’t affect data set on profiles using BatchProfile. + */ + clearInstallationData: (): void => RNBatch.userData_clearInstallationData(), }; From 3bb5af57e7a6227ffe223159d3f326c488a05e9b Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Wed, 29 May 2024 15:50:59 +0200 Subject: [PATCH 18/30] bridge: rename internal bridge methods --- .../com/batch/batch_rn/RNBatchModule.java | 24 ++++++++++--------- ios/RNBatch.m | 24 +++++++++---------- src/BatchProfile.ts | 4 ++-- src/BatchProfileAttributeEditor.ts | 2 +- src/BatchUser.ts | 14 +++++------ 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index f6e4e89..66a1e43 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -515,34 +515,34 @@ public void inbox_fetcher_displayLandingMessage(String fetcherIdentifier, String promise.resolve(null); } - // USER DATA EDITOR MODULE + // USER MODULE @ReactMethod - public void userData_getInstallationId(Promise promise) { + public void user_getInstallationId(Promise promise) { String userId = Batch.User.getInstallationID(); promise.resolve(userId); } @ReactMethod - public void userData_getIdentifier(Promise promise) { + public void user_getIdentifier(Promise promise) { String userId = Batch.User.getIdentifier(reactContext); promise.resolve(userId); } @ReactMethod - public void userData_getRegion(Promise promise) { + public void user_getRegion(Promise promise) { String region = Batch.User.getRegion(reactContext); promise.resolve(region); } @ReactMethod - public void userData_getLanguage(Promise promise) { + public void user_getLanguage(Promise promise) { String language = Batch.User.getLanguage(reactContext); promise.resolve(language); } @ReactMethod - public void userData_getAttributes(final Promise promise) { + public void user_getAttributes(final Promise promise) { Batch.User.fetchAttributes(reactContext, new BatchAttributesFetchListener() { @Override public void onSuccess(@NonNull Map map) { @@ -607,7 +607,7 @@ public void onError() { } @ReactMethod - public void userData_getTags(final Promise promise) { + public void user_getTags(final Promise promise) { Batch.User.fetchTagCollections(reactContext, new BatchTagCollectionsFetchListener() { @Override public void onSuccess(@NonNull Map> map) { @@ -630,7 +630,7 @@ public void onError() { } @ReactMethod - public void userData_save(ReadableArray actions) { + public void user_save(ReadableArray actions) { BatchProfileAttributeEditor editor = Batch.Profile.editor(); for (int i = 0; i < actions.size(); i++) { ReadableMap action = actions.getMap(i); @@ -721,23 +721,25 @@ public void userData_save(ReadableArray actions) { } @ReactMethod - public void userData_clearInstallationData() { + public void user_clearInstallationData() { Batch.User.clearInstallationData(); } + // PROFILE MODULE + @ReactMethod public void profile_identify(String identifier) { Batch.Profile.identify(identifier); } @ReactMethod - public void userData_trackEvent(String name, ReadableMap serializedEventData) { + public void profile_trackEvent(String name, ReadableMap serializedEventData) { BatchEventAttributes attributes = RNUtils.convertSerializedEventDataToEventAttributes(serializedEventData); Batch.Profile.trackEvent(name, attributes); } @ReactMethod - public void userData_trackLocation(ReadableMap serializedLocation) { + public void profile_trackLocation(ReadableMap serializedLocation) { Location nativeLocation = new Location("com.batch.batch_rn"); nativeLocation.setLatitude(serializedLocation.getDouble("latitude")); nativeLocation.setLongitude(serializedLocation.getDouble("longitude")); diff --git a/ios/RNBatch.m b/ios/RNBatch.m index 93c9e44..a78c469 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -193,31 +193,31 @@ -(void)stopObserving { // User module -RCT_EXPORT_METHOD(userData_getInstallationId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(user_getInstallationId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSString* installationId = [BatchUser installationID]; resolve(installationId); } -RCT_EXPORT_METHOD(userData_getIdentifier:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(user_getIdentifier:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSString* userId = [BatchUser identifier]; resolve(userId); } -RCT_EXPORT_METHOD(userData_getRegion:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(user_getRegion:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSString* region = [BatchUser region]; resolve(region); } -RCT_EXPORT_METHOD(userData_getLanguage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(user_getLanguage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSString* language = [BatchUser language]; resolve(language); } -RCT_EXPORT_METHOD(userData_getAttributes:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(user_getAttributes:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [BatchUser fetchAttributes:^(NSDictionary * _Nullable attributes) { @@ -282,7 +282,7 @@ -(void)stopObserving { }]; } -RCT_EXPORT_METHOD(userData_getTags:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(user_getTags:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [BatchUser fetchTagCollections:^(NSDictionary *> * _Nullable collections) { if (collections == nil) { @@ -299,7 +299,7 @@ -(void)stopObserving { } -RCT_EXPORT_METHOD(userData_save:(NSArray*)actions) +RCT_EXPORT_METHOD(user_save:(NSArray*)actions) { BatchProfileEditor *editor = [BatchProfile editor]; for (NSDictionary* action in actions) { @@ -388,19 +388,19 @@ -(void)stopObserving { [editor save]; } -RCT_EXPORT_METHOD(userData_clearInstallationData) +RCT_EXPORT_METHOD(user_clearInstallationData) { [BatchUser clearInstallationData]; } +// Profile + RCT_EXPORT_METHOD(profile_identify:(NSString*)identifier) { [BatchProfile identify:identifier]; } -// Event tracking - -RCT_EXPORT_METHOD(userData_trackEvent:(NSString*)name data:(NSDictionary*)serializedEventData) +RCT_EXPORT_METHOD(profile_trackEvent:(NSString*)name data:(NSDictionary*)serializedEventData) { BatchEventAttributes *batchEventAttributes = nil; @@ -523,7 +523,7 @@ - (BatchEventAttributes*) convertSerializedEventDataToEventAttributes:(NSDiction return eventAttributes; } -RCT_EXPORT_METHOD(userData_trackLocation:(NSDictionary*)serializedLocation) +RCT_EXPORT_METHOD(profile_trackLocation:(NSDictionary*)serializedLocation) { if (![serializedLocation isKindOfClass:[NSDictionary class]] || [serializedLocation count]==0) { diff --git a/src/BatchProfile.ts b/src/BatchProfile.ts index 54671b3..0d6e605 100644 --- a/src/BatchProfile.ts +++ b/src/BatchProfile.ts @@ -59,7 +59,7 @@ export const BatchProfile = { trackEvent: (name: string, data?: BatchEventAttributes): void => { // Since _toInternalRepresentation is private, we have to resort to this little hack to access the method. // That syntax keeps the argument type checking, while casting as any would not. - RNBatch.userData_trackEvent(name, data instanceof BatchEventAttributes ? data['_toInternalRepresentation']() : null); + RNBatch.profile_trackEvent(name, data instanceof BatchEventAttributes ? data['_toInternalRepresentation']() : null); }, /** @@ -93,7 +93,7 @@ export const BatchProfile = { return; } - RNBatch.userData_trackLocation({ + RNBatch.profile_trackLocation({ date: location.date ? location.date.getTime() : undefined, latitude: location.latitude, longitude: location.longitude, diff --git a/src/BatchProfileAttributeEditor.ts b/src/BatchProfileAttributeEditor.ts index c66c7b4..1e8624e 100644 --- a/src/BatchProfileAttributeEditor.ts +++ b/src/BatchProfileAttributeEditor.ts @@ -170,6 +170,6 @@ export class BatchProfileAttributeEditor { } public save(): void { - RNBatch.userData_save(this._settings); + RNBatch.user_save(this._settings); } } diff --git a/src/BatchUser.ts b/src/BatchUser.ts index e2fd78a..4af8e7a 100644 --- a/src/BatchUser.ts +++ b/src/BatchUser.ts @@ -12,28 +12,28 @@ export const BatchUser = { * Get the unique installation ID, generated by Batch. Batch must be started to read it. * You will get the result in a promise. */ - getInstallationID: (): Promise => RNBatch.userData_getInstallationId(), + getInstallationID: (): Promise => RNBatch.user_getInstallationId(), /** * Get the custom user identifier. * @returns The custom user identifier set with BatchUser.editor().setIdentifier(); */ - getIdentifier: (): Promise => RNBatch.userData_getIdentifier(), + getIdentifier: (): Promise => RNBatch.user_getIdentifier(), /** * Get the region. * @returns The region set with BatchUser.editor().setRegion(); */ - getRegion: (): Promise => RNBatch.userData_getRegion(), + getRegion: (): Promise => RNBatch.user_getRegion(), /** * Get the language. * @returns The language set with BatchUser.editor().setLanguage(); */ - getLanguage: (): Promise => RNBatch.userData_getLanguage(), + getLanguage: (): Promise => RNBatch.user_getLanguage(), /** * Read the saved attributes. @@ -41,7 +41,7 @@ export const BatchUser = { * @returns The attributes set with Batch.editor().setAttribute() */ getAttributes: (): Promise<{ [key: string]: BatchUserAttribute }> => { - return RNBatch.userData_getAttributes().then(attributes => { + return RNBatch.user_getAttributes().then(attributes => { Object.keys(attributes).map(key => { attributes[key] = new BatchUserAttribute(attributes[key].type, attributes[key].value); }); @@ -53,11 +53,11 @@ export const BatchUser = { * Get the tag collections. * @returns The tags added with BatchUser.editor().addTag() */ - getTagCollections: (): Promise<{ [key: string]: string[] }> => RNBatch.userData_getTags(), + getTagCollections: (): Promise<{ [key: string]: string[] }> => RNBatch.user_getTags(), /** * Clear all tags and attributes set on an installation and their local cache returned by fetchAttributes and fetchTagCollections. * This doesn’t affect data set on profiles using BatchProfile. */ - clearInstallationData: (): void => RNBatch.userData_clearInstallationData(), + clearInstallationData: (): void => RNBatch.user_clearInstallationData(), }; From 4873e0e684ef3607267dcc6a56f66b3d2dfca174 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Wed, 29 May 2024 17:41:59 +0200 Subject: [PATCH 19/30] event: remove js side event attributes validations since its already done on native side --- src/BatchEventAttributes.ts | 105 +----------------------------------- 1 file changed, 1 insertion(+), 104 deletions(-) diff --git a/src/BatchEventAttributes.ts b/src/BatchEventAttributes.ts index c91706f..e0427f0 100644 --- a/src/BatchEventAttributes.ts +++ b/src/BatchEventAttributes.ts @@ -36,86 +36,12 @@ export class BatchEventAttributes { public constructor() { this._attributes = {}; } - - private addTags(tags: Array): BatchEventAttributes { - tags.forEach(tag => { - if (typeof tag === 'undefined') { - Log(false, 'BatchEventData - A tag is required'); - return this; - } - - if (isString(tag)) { - if (tag.length === 0 || tag.length > Consts.EventDataStringMaxLength) { - Log( - false, - "BatchEventData - Tags can't be empty or longer than " + - Consts.EventDataStringMaxLength + - " characters. Ignoring tag '" + - tag + - "'." - ); - return this; - } - } else { - Log(false, 'BatchEventData - Tag argument must be a string'); - return this; - } - if (tags.length >= Consts.EventDataMaxTags) { - Log(false, 'BatchEventData - Event data cannot hold more than ' + Consts.EventDataMaxTags + " tags. Ignoring tag: '" + tag + "'"); - return this; - } - }); - this._attributes['$tags'] = { - type: TypedEventAttributeType.StringArray, - value: tags, - }; - return this; - } - - private checkBeforePuttingAttribute(key: string, value: unknown): void { - if (!isString(key)) { - Log(false, 'BatchEventData - Key must be a string'); - throw new Error(); - } - - if (!Consts.AttributeKeyRegexp.test(key || '')) { - if (key !== '$tags' && key !== '$label') { - Log( - false, - 'BatchEventData - Invalid key. Please make sure that the key is made of letters, underscores and numbers only (a-zA-Z0-9_).' + - "It also can't be longer than 30 characters. Ignoring attribute '" + - key + - "'" - ); - throw new Error(); - } - } - - if (typeof value === 'undefined' || value === null) { - Log(false, 'BatchEventData - Value cannot be undefined or null'); - throw new Error(); - } - - if (Object.keys(this._attributes).length >= Consts.EventDataMaxValues && !Object.prototype.hasOwnProperty.call(this._attributes, key)) { - Log( - false, - 'BatchEventData - Event data cannot hold more than ' + Consts.EventDataMaxValues + " attributes. Ignoring attribute: '" + key + "'" - ); - throw new Error(); - } - } - private prepareAttributeKey(key: string): string { return key.toLowerCase(); } public putDate(key: string, value: number): BatchEventAttributes { key = this.prepareAttributeKey(key); - try { - this.checkBeforePuttingAttribute(key, value); - } catch { - return this; - } this._attributes[key] = { type: TypedEventAttributeType.Date, @@ -127,23 +53,6 @@ export class BatchEventAttributes { public putURL(key: string, url: string): BatchEventAttributes { key = this.prepareAttributeKey(key); - try { - this.checkBeforePuttingAttribute(key, url); - } catch { - return this; - } - - if (url.length > Consts.EventDataURLMaxLength) { - Log( - false, - "BatchEventData - Event data can't be longer than " + - Consts.EventDataURLMaxLength + - " characters. Ignoring event data value '" + - url + - "'." - ); - return this; - } this._attributes[key] = { type: TypedEventAttributeType.URL, @@ -159,18 +68,6 @@ export class BatchEventAttributes { ): BatchEventAttributes { key = this.prepareAttributeKey(key); - try { - this.checkBeforePuttingAttribute(key, value); - } catch { - return this; - } - - // Check if data contains legacy tags - if (key == '$tags' && isStringArray(value)) { - this.addTags(value); - return this; - } - let typedAttrValue: ITypedEventAttribute | undefined; if (isString(value)) { typedAttrValue = { @@ -207,7 +104,7 @@ export class BatchEventAttributes { value: array, }; } else { - Log(false, 'BatchEventData - Invalid attribute value type. Must be a string, number or boolean'); + Log(false, 'BatchEventAttributes - Invalid attribute value type. Must be a string, number or boolean'); return this; } From 6228fc24c0992bc1180c87170c5bc5c3234090e9 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Thu, 30 May 2024 14:33:44 +0200 Subject: [PATCH 20/30] expo: add expo configuration support for batch 2.0 --- .../withReactNativeBatchAppBuildGradle.ts | 6 +-- .../android/withReactNativeBatchManifest.ts | 48 +++++++++++++++++++ plugin/src/constants.ts | 5 +- plugin/src/fixtures/buildGradle.ts | 4 +- .../src/ios/withReactNativeBatchInfoPlist.ts | 18 ++++++- plugin/src/withReactNativeBatch.ts | 11 ++++- 6 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 plugin/src/android/withReactNativeBatchManifest.ts diff --git a/plugin/src/android/withReactNativeBatchAppBuildGradle.ts b/plugin/src/android/withReactNativeBatchAppBuildGradle.ts index 36c42d9..7ed4504 100644 --- a/plugin/src/android/withReactNativeBatchAppBuildGradle.ts +++ b/plugin/src/android/withReactNativeBatchAppBuildGradle.ts @@ -1,11 +1,11 @@ import { ConfigPlugin, withAppBuildGradle } from '@expo/config-plugins'; -import { BATCH_SDK_VERISON, BATCH_DO_NOT_DISTURB_INITIAL_STATE } from '../constants'; +import { BATCH_SDK_VERSION, BATCH_DO_NOT_DISTURB_INITIAL_STATE } from '../constants'; import { Props } from '../withReactNativeBatch'; export const pushDependencies = (contents: string, props: Props): string => { let newContents = contents; - const doNotDisturb = props.enableDoNotDisturb || props.enableDoNotDistrub || BATCH_DO_NOT_DISTURB_INITIAL_STATE; + const doNotDisturb = props.enableDoNotDisturb !== undefined ? props.enableDoNotDisturb : BATCH_DO_NOT_DISTURB_INITIAL_STATE; const versionNameLine = newContents.match(/versionName "([^"]*)"/); if (versionNameLine) { @@ -33,7 +33,7 @@ export const pushDependencies = (contents: string, props: Props): string => { start + `\n implementation platform('com.google.firebase:firebase-bom:25.12.0') implementation "com.google.firebase:firebase-messaging" - api "com.batch.android:batch-sdk:${BATCH_SDK_VERISON}"` + + api "com.batch.android:batch-sdk:${BATCH_SDK_VERSION}"` + end; } return newContents; diff --git a/plugin/src/android/withReactNativeBatchManifest.ts b/plugin/src/android/withReactNativeBatchManifest.ts new file mode 100644 index 0000000..4ccafb9 --- /dev/null +++ b/plugin/src/android/withReactNativeBatchManifest.ts @@ -0,0 +1,48 @@ +import { ConfigPlugin, AndroidManifest, withAndroidManifest } from '@expo/config-plugins'; + +import { + BATCH_DEFAULT_PROFILE_CUSTOM_DATA_MIGRATION, + BATCH_DEFAULT_PROFILE_CUSTOM_ID_MIGRATION, + BATCH_DEFAULT_OPT_OUT_INITIAL_STATE, +} from '../constants'; +import { Props } from '../withReactNativeBatch'; + +export const modifyAndroidManifest = (modResults: AndroidManifest, props: Props): AndroidManifest => { + const profileCustomIdMigrationEnabled = + props.enableProfileCustomIDMigration !== undefined ? props.enableProfileCustomIDMigration : BATCH_DEFAULT_PROFILE_CUSTOM_ID_MIGRATION; + const profileCustomDataMigrationEnabled = + props.enableProfileCustomDataMigration !== undefined + ? props.enableProfileCustomDataMigration + : BATCH_DEFAULT_PROFILE_CUSTOM_DATA_MIGRATION; + const defaultOptedOut = props.enableDefaultOptOut !== undefined ? props.enableDefaultOptOut : BATCH_DEFAULT_OPT_OUT_INITIAL_STATE; + modResults.manifest?.application?.map(element => { + if (element['meta-data']) { + element['meta-data'].push({ + $: { + 'android:name': 'batch.profile_custom_id_migration_enabled', + 'android:value': String(profileCustomIdMigrationEnabled), + }, + }); + element['meta-data'].push({ + $: { + 'android:name': 'batch.profile_custom_data_migration_enabled', + 'android:value': String(profileCustomDataMigrationEnabled), + }, + }); + element['meta-data'].push({ + $: { + 'android:name': 'batch_opted_out_by_default', + 'android:value': String(defaultOptedOut), + }, + }); + } + }); + return modResults; +}; + +export const withReactNativeBatchManifest: ConfigPlugin = (config, props) => { + return withAndroidManifest(config, config => { + config.modResults = modifyAndroidManifest(config.modResults, props); + return config; + }); +}; diff --git a/plugin/src/constants.ts b/plugin/src/constants.ts index 0d47efe..9aaefb0 100644 --- a/plugin/src/constants.ts +++ b/plugin/src/constants.ts @@ -1,3 +1,6 @@ //Define the version used in the pre-built gradle file for the batch native sdk dependency. -export const BATCH_SDK_VERISON = '1.21.+'; +export const BATCH_SDK_VERSION = '2.0.+'; export const BATCH_DO_NOT_DISTURB_INITIAL_STATE = false; +export const BATCH_DEFAULT_OPT_OUT_INITIAL_STATE = false; +export const BATCH_DEFAULT_PROFILE_CUSTOM_ID_MIGRATION = true; +export const BATCH_DEFAULT_PROFILE_CUSTOM_DATA_MIGRATION = true; diff --git a/plugin/src/fixtures/buildGradle.ts b/plugin/src/fixtures/buildGradle.ts index a3201f2..5c20361 100644 --- a/plugin/src/fixtures/buildGradle.ts +++ b/plugin/src/fixtures/buildGradle.ts @@ -1,4 +1,4 @@ -import { BATCH_SDK_VERISON } from "../constants"; +import { BATCH_SDK_VERSION } from "../constants"; const FLIPPER_VERSION = '1.0.0'; export const buildGradleFixture = ` @@ -213,7 +213,7 @@ android { dependencies { implementation platform('com.google.firebase:firebase-bom:25.12.0') implementation "com.google.firebase:firebase-messaging" - api "com.batch.android:batch-sdk:${BATCH_SDK_VERISON}" + api "com.batch.android:batch-sdk:${BATCH_SDK_VERSION}" implementation fileTree(dir: "libs", include: ["*.jar"]) //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" // From node_modules diff --git a/plugin/src/ios/withReactNativeBatchInfoPlist.ts b/plugin/src/ios/withReactNativeBatchInfoPlist.ts index acc7712..74354ab 100644 --- a/plugin/src/ios/withReactNativeBatchInfoPlist.ts +++ b/plugin/src/ios/withReactNativeBatchInfoPlist.ts @@ -1,11 +1,25 @@ import { ConfigPlugin, withInfoPlist, InfoPlist } from '@expo/config-plugins'; -import { BATCH_DO_NOT_DISTURB_INITIAL_STATE } from '../constants'; +import { + BATCH_DO_NOT_DISTURB_INITIAL_STATE, + BATCH_DEFAULT_OPT_OUT_INITIAL_STATE, + BATCH_DEFAULT_PROFILE_CUSTOM_ID_MIGRATION, + BATCH_DEFAULT_PROFILE_CUSTOM_DATA_MIGRATION, +} from '../constants'; import { Props } from '../withReactNativeBatch'; export const modifyInfoPlist = (infoPlist: InfoPlist, props: Props): InfoPlist => { infoPlist.BatchAPIKey = props.iosApiKey; - infoPlist.BatchDoNotDisturbInitialState = props.enableDoNotDisturb || props.enableDoNotDistrub || BATCH_DO_NOT_DISTURB_INITIAL_STATE; + infoPlist.BatchDoNotDisturbInitialState = + props.enableDoNotDisturb !== undefined ? props.enableDoNotDisturb : BATCH_DO_NOT_DISTURB_INITIAL_STATE; + infoPlist.BatchProfileCustomIdMigrationEnabled = + props.enableProfileCustomIDMigration !== undefined ? props.enableProfileCustomIDMigration : BATCH_DEFAULT_PROFILE_CUSTOM_ID_MIGRATION; + infoPlist.BatchProfileCustomDataMigrationEnabled = + props.enableProfileCustomDataMigration !== undefined + ? props.enableProfileCustomDataMigration + : BATCH_DEFAULT_PROFILE_CUSTOM_DATA_MIGRATION; + infoPlist.BATCH_OPTED_OUT_BY_DEFAULT = + props.enableDefaultOptOut !== undefined ? props.enableDefaultOptOut : BATCH_DEFAULT_OPT_OUT_INITIAL_STATE; return infoPlist; }; diff --git a/plugin/src/withReactNativeBatch.ts b/plugin/src/withReactNativeBatch.ts index aa47c05..e388b34 100644 --- a/plugin/src/withReactNativeBatch.ts +++ b/plugin/src/withReactNativeBatch.ts @@ -3,10 +3,18 @@ import { withClassPath, withApplyPlugin, withGoogleServicesFile } from '@expo/co import { withReactNativeBatchAppBuildGradle } from './android/withReactNativeBatchAppBuildGradle'; import { withReactNativeBatchMainActivity } from './android/withReactNativeBatchMainActivity'; +import { withReactNativeBatchManifest } from './android/withReactNativeBatchManifest'; import { withReactNativeBatchAppDelegate } from './ios/withReactNativeBatchAppDelegate'; import { withReactNativeBatchInfoPlist } from './ios/withReactNativeBatchInfoPlist'; -export type Props = { androidApiKey: string; iosApiKey: string; enableDoNotDisturb?: boolean; enableDoNotDistrub?: boolean }; +export type Props = { + androidApiKey: string; + iosApiKey: string; + enableDoNotDisturb?: boolean; + enableDefaultOptOut?: boolean; + enableProfileCustomIDMigration?: boolean; + enableProfileCustomDataMigration?: boolean; +}; /** * Apply react-native-batch configuration for Expo SDK 42 projects. */ @@ -16,6 +24,7 @@ const withReactNativeBatch: ConfigPlugin = (config, props) => { let newConfig = withGoogleServicesFile(config); newConfig = withClassPath(newConfig); newConfig = withApplyPlugin(newConfig); + newConfig = withReactNativeBatchManifest(newConfig, _props); newConfig = withReactNativeBatchAppBuildGradle(newConfig, _props); newConfig = withReactNativeBatchMainActivity(newConfig); newConfig = withReactNativeBatchInfoPlist(newConfig, _props); From e4ca1190c114e9482e3ce3a5e9529afc8cf6c184 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Thu, 30 May 2024 17:52:38 +0200 Subject: [PATCH 21/30] ios: add default chained notification delegate support --- ios/BatchBridgeNotificationCenterDelegate.h | 43 ++++++ ios/BatchBridgeNotificationCenterDelegate.m | 151 ++++++++++++++++++++ ios/RNBatch.m | 8 +- 3 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 ios/BatchBridgeNotificationCenterDelegate.h create mode 100644 ios/BatchBridgeNotificationCenterDelegate.m diff --git a/ios/BatchBridgeNotificationCenterDelegate.h b/ios/BatchBridgeNotificationCenterDelegate.h new file mode 100644 index 0000000..183074d --- /dev/null +++ b/ios/BatchBridgeNotificationCenterDelegate.h @@ -0,0 +1,43 @@ +// +// Copyright © Batch.com. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +// Batch's bridge UNUserNotificationCenterDelegate +// Handles: +// - Forwarding calls to another delegate (chaining, rather than swizzling) +// - Giving notification callbacks to Batch +// - Enabling/Disabling foreground notifications +@interface BatchBridgeNotificationCenterDelegate : NSObject + +/// Shared singleton BatchUNUserNotificationCenterDelegate. +/// Using this allows you to set the instance as UNUserNotificationCenter's delegate without having to retain it yourself. +/// The shared instance is lazily loaded. +@property (class, retain, readonly, nonnull) BatchBridgeNotificationCenterDelegate* sharedInstance; + +/// Registers this class' sharedInstance as UNUserNotificationCenter's delegate, and stores the previous one as a property ++ (void)registerAsDelegate; + +/// Should iOS display notifications even if the app is in foreground? +/// Default: true +@property (assign) BOOL showForegroundNotifications; + +/// Should Batch use the chained delegate's completionHandler responses or force its own, while still calling the chained delegate. +/// This is useful if you want Batch to enforce its "showForegroundNotifications" setting while still informing the chained delegate. +/// Default: true, but the plugin will automatically set that to false when calling "setShowForegroundNotification" from JavaScript. +@property (assign) BOOL shouldUseChainedCompletionHandlerResponse; + +/// Previous delegate +@property (weak, nullable) id previousDelegate; + +/// Should this class automatically register itself as UNUserNotificationCenterDelegate when the app is launched? Default: true +/// This value needs to be changed before `[RNBatch start]` be called. +@property (class, assign) BOOL automaticallyRegister; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/BatchBridgeNotificationCenterDelegate.m b/ios/BatchBridgeNotificationCenterDelegate.m new file mode 100644 index 0000000..daf441c --- /dev/null +++ b/ios/BatchBridgeNotificationCenterDelegate.m @@ -0,0 +1,151 @@ +// +// Copyright © Batch.com. All rights reserved. +// + +#import "BatchBridgeNotificationCenterDelegate.h" + +#import + +@implementation BatchBridgeNotificationCenterDelegate +{ + __weak __nullable id _previousDelegate; +} + +static BOOL _batBridgeNotifDelegateShouldAutomaticallyRegister = true; + + ++ (BatchBridgeNotificationCenterDelegate *)sharedInstance +{ + static BatchBridgeNotificationCenterDelegate *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[BatchBridgeNotificationCenterDelegate alloc] init]; + }); + + return sharedInstance; +} + ++ (void)registerAsDelegate +{ + UNUserNotificationCenter *notifCenter = [UNUserNotificationCenter currentNotificationCenter]; + BatchBridgeNotificationCenterDelegate *instance = [self sharedInstance]; + instance.previousDelegate = notifCenter.delegate; + notifCenter.delegate = instance; +} + ++ (BOOL)automaticallyRegister +{ + return _batBridgeNotifDelegateShouldAutomaticallyRegister; +} + ++ (void)setAutomaticallyRegister:(BOOL)automaticallyRegister +{ + _batBridgeNotifDelegateShouldAutomaticallyRegister = automaticallyRegister; +} + +- (nullable id)previousDelegate +{ + return _previousDelegate; +} + +- (void)setPreviousDelegate:(nullable id)delegate +{ + // Do not register ourserlves as previous delegate to avoid + // an infinite loop + if (delegate == self || [delegate isKindOfClass:[self class]]) { + _previousDelegate = nil; + } else { + _previousDelegate = delegate; + } +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _showForegroundNotifications = true; + _shouldUseChainedCompletionHandlerResponse = true; + } + return self; +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler +{ + [BatchPush handleUserNotificationCenter:center willPresentNotification:notification willShowSystemForegroundAlert:self.showForegroundNotifications]; + + id chainDelegate = self.previousDelegate; + // It's the chain delegate's responsibility to call the completionHandler + if ([chainDelegate respondsToSelector:@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:)]) { + //returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...}; + void (^chainCompletionHandler)(UNNotificationPresentationOptions); + + if (self.shouldUseChainedCompletionHandlerResponse) { + // Set iOS' completion handler as the one we give to the method, as we don't want to override the result + chainCompletionHandler = completionHandler; + } else { + // Set ourselves as the chained completion handler so we can wait for the implementation but rewrite the response + chainCompletionHandler = ^(UNNotificationPresentationOptions ignored) { + [self performPresentCompletionHandler:completionHandler]; + }; + } + + [chainDelegate userNotificationCenter:center + willPresentNotification:notification + withCompletionHandler:chainCompletionHandler]; + } else { + [self performPresentCompletionHandler:completionHandler]; + } +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler +{ + [BatchPush handleUserNotificationCenter:center didReceiveNotificationResponse:response]; + + id chainDelegate = self.previousDelegate; + // It's the chain delegate's responsibility to call the completionHandler + if ([chainDelegate respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)]) { + [chainDelegate userNotificationCenter:center + didReceiveNotificationResponse:response + withCompletionHandler:completionHandler]; + } else { + if (completionHandler) { + completionHandler(); + } + } + +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(UNNotification *)notification +{ + if (@available(iOS 12.0, *)) { + id chainDelegate = self.previousDelegate; + if ([chainDelegate respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)]) { + [self.previousDelegate userNotificationCenter:center + openSettingsForNotification:notification]; + } + } +} + +/// Call iOS back on the "present" completion handler with Batch controlled presentation options +- (void)performPresentCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { + UNNotificationPresentationOptions options = UNNotificationPresentationOptionNone; + if (self.showForegroundNotifications) { + options = UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; + +#ifdef __IPHONE_14_0 + if (@available(iOS 14.0, *)) { + options = options | UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner; + } else { + options = options | UNNotificationPresentationOptionAlert; + } +#else + options = options | UNNotificationPresentationOptionAlert; +#endif + } + + if (completionHandler) { + completionHandler(options); + }; +} + +@end diff --git a/ios/RNBatch.m b/ios/RNBatch.m index a78c469..2b89663 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -2,6 +2,7 @@ # import "RNBatch.h" # import "RNBatchOpenedNotificationObserver.h" # import "RNBatchEventDispatcher.h" +# import "BatchBridgeNotificationCenterDelegate.h" static RNBatchEventDispatcher* dispatcher = nil; @@ -63,6 +64,9 @@ + (void)start NSString *batchAPIKey = [info objectForKey:@"BatchAPIKey"]; [BatchSDK startWithAPIKey:batchAPIKey]; + if (BatchBridgeNotificationCenterDelegate.automaticallyRegister) { + [BatchBridgeNotificationCenterDelegate registerAsDelegate]; + } dispatcher = [[RNBatchEventDispatcher alloc] init]; [BatchEventDispatcher addDispatcher:dispatcher]; } @@ -166,7 +170,9 @@ -(void)stopObserving { RCT_EXPORT_METHOD(push_setShowForegroundNotification:(BOOL) enabled) { - [BatchUNUserNotificationCenterDelegate sharedInstance].showForegroundNotifications = enabled; + BatchBridgeNotificationCenterDelegate *delegate = [BatchBridgeNotificationCenterDelegate sharedInstance]; + delegate.showForegroundNotifications = enabled; + delegate.shouldUseChainedCompletionHandlerResponse = false; } RCT_EXPORT_METHOD(push_clearBadge) From 71e0bcb2584010e9a909831b1cefdc8709c9fbd8 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 31 May 2024 11:00:46 +0200 Subject: [PATCH 22/30] expo: remove manual notification delegate in pre-build --- .../withReactNativeBatchAppDelegate.test.ts | 5 +---- plugin/src/fixtures/appDelegate.ts | 3 --- .../src/ios/withReactNativeBatchAppDelegate.ts | 16 ++++------------ 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/plugin/src/__tests__/withReactNativeBatchAppDelegate.test.ts b/plugin/src/__tests__/withReactNativeBatchAppDelegate.test.ts index cc9922a..90b7a5d 100644 --- a/plugin/src/__tests__/withReactNativeBatchAppDelegate.test.ts +++ b/plugin/src/__tests__/withReactNativeBatchAppDelegate.test.ts @@ -1,8 +1,5 @@ +import { appDelegateExpectedFixture, appDelegateFixture } from '../fixtures/appDelegate'; import { modifyAppDelegate } from '../ios/withReactNativeBatchAppDelegate'; -import { - appDelegateExpectedFixture, - appDelegateFixture, -} from '../fixtures/appDelegate'; describe(modifyAppDelegate, () => { it('should modify the AppDelegate', () => { diff --git a/plugin/src/fixtures/appDelegate.ts b/plugin/src/fixtures/appDelegate.ts index 8127cfb..69daf7a 100644 --- a/plugin/src/fixtures/appDelegate.ts +++ b/plugin/src/fixtures/appDelegate.ts @@ -157,8 +157,6 @@ static void InitializeFlipper(UIApplication *application) { export const appDelegateExpectedFixture = `#import "AppDelegate.h" -#import - #if defined(EX_DEV_MENU_ENABLED) @import EXDevMenu; #endif @@ -210,7 +208,6 @@ static void InitializeFlipper(UIApplication *application) { - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [RNBatch start]; - [BatchUNUserNotificationCenterDelegate registerAsDelegate]; #if defined(FB_SONARKIT_ENABLED) && __has_include() InitializeFlipper(application); diff --git a/plugin/src/ios/withReactNativeBatchAppDelegate.ts b/plugin/src/ios/withReactNativeBatchAppDelegate.ts index bf830be..f49cab2 100644 --- a/plugin/src/ios/withReactNativeBatchAppDelegate.ts +++ b/plugin/src/ios/withReactNativeBatchAppDelegate.ts @@ -1,28 +1,20 @@ import { ConfigPlugin, withAppDelegate } from '@expo/config-plugins'; -import { Props } from '../withReactNativeBatch'; const DID_FINISH_LAUNCHING_WITH_OPTIONS_DECLARATION = '- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions\n{'; -const IMPORT_BATCH = '\n\n#import \n'; -const REGISTER_BATCH = '\n [RNBatch start];\n [BatchUNUserNotificationCenterDelegate registerAsDelegate];\n'; +const REGISTER_BATCH = '\n [RNBatch start];\n'; export const modifyAppDelegate = (contents: string) => { - contents = contents.replace('\n', IMPORT_BATCH); + const [beforeDeclaration, afterDeclaration] = contents.split(DID_FINISH_LAUNCHING_WITH_OPTIONS_DECLARATION); - const [beforeDeclaration, afterDeclaration] = contents.split( - DID_FINISH_LAUNCHING_WITH_OPTIONS_DECLARATION - ); - - const newAfterDeclaration = DID_FINISH_LAUNCHING_WITH_OPTIONS_DECLARATION.concat( - REGISTER_BATCH - ).concat(afterDeclaration); + const newAfterDeclaration = DID_FINISH_LAUNCHING_WITH_OPTIONS_DECLARATION.concat(REGISTER_BATCH).concat(afterDeclaration); contents = beforeDeclaration.concat(newAfterDeclaration); return contents; }; -export const withReactNativeBatchAppDelegate: ConfigPlugin<{} | void> = config => { +export const withReactNativeBatchAppDelegate: ConfigPlugin = config => { return withAppDelegate(config, config => { config.modResults.contents = modifyAppDelegate(config.modResults.contents); return config; From 9baa0a1b7fba8d4aebe0a42b63d016ee4a70f310 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 31 May 2024 16:11:24 +0200 Subject: [PATCH 23/30] profile: return promise from trackEvent to get errors in js side --- .../main/java/com/batch/batch_rn/RNBatchModule.java | 10 +++++++++- ios/RNBatch.m | 6 ++++-- src/BatchProfile.ts | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index 66a1e43..d0ffaec 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -733,9 +733,17 @@ public void profile_identify(String identifier) { } @ReactMethod - public void profile_trackEvent(String name, ReadableMap serializedEventData) { + public void profile_trackEvent(@NonNull String name, @Nullable ReadableMap serializedEventData, @NonNull Promise promise) { BatchEventAttributes attributes = RNUtils.convertSerializedEventDataToEventAttributes(serializedEventData); + if (attributes != null) { + List errors = attributes.validateEventAttributes(); + if (!errors.isEmpty()) { + promise.reject(BATCH_BRIDGE_ERROR_CODE, errors.toString()); + return; + } + } Batch.Profile.trackEvent(name, attributes); + promise.resolve(null); } @ReactMethod diff --git a/ios/RNBatch.m b/ios/RNBatch.m index 2b89663..fb1f64b 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -406,7 +406,7 @@ -(void)stopObserving { [BatchProfile identify:identifier]; } -RCT_EXPORT_METHOD(profile_trackEvent:(NSString*)name data:(NSDictionary*)serializedEventData) +RCT_EXPORT_METHOD(profile_trackEvent:(NSString*)name data:(NSDictionary*)serializedEventData resolver: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { BatchEventAttributes *batchEventAttributes = nil; @@ -418,12 +418,14 @@ -(void)stopObserving { [batchEventAttributes validateWithError:&err]; if (batchEventAttributes != nil && err == nil) { [BatchProfile trackEventWithName:name attributes:batchEventAttributes]; + resolve([NSNull null]); } else { - NSLog(@"Event validation error: %@", err.description); + reject(@"BatchBridgeError", @"Event attributes validation failed:", err); return; } } [BatchProfile trackEventWithName:name attributes:batchEventAttributes]; + resolve([NSNull null]); } - (BatchEventAttributes*) convertSerializedEventDataToEventAttributes:(NSDictionary *) serializedAttributes { diff --git a/src/BatchProfile.ts b/src/BatchProfile.ts index 0d6e605..ad8ca12 100644 --- a/src/BatchProfile.ts +++ b/src/BatchProfile.ts @@ -56,10 +56,10 @@ export const BatchProfile = { * @param name The event name. Must be a string. * @param data The event attributes (optional). Must be an object. */ - trackEvent: (name: string, data?: BatchEventAttributes): void => { + trackEvent: (name: string, data?: BatchEventAttributes): Promise => { // Since _toInternalRepresentation is private, we have to resort to this little hack to access the method. // That syntax keeps the argument type checking, while casting as any would not. - RNBatch.profile_trackEvent(name, data instanceof BatchEventAttributes ? data['_toInternalRepresentation']() : null); + return RNBatch.profile_trackEvent(name, data instanceof BatchEventAttributes ? data['_toInternalRepresentation']() : null); }, /** From 988093bede5f2c8b73c92f9e918c029ca2801b5b Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 31 May 2024 16:14:35 +0200 Subject: [PATCH 24/30] test: remove test since event attributes validation is done on native side --- src/BatchEventData.test.ts | 82 -------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 src/BatchEventData.test.ts diff --git a/src/BatchEventData.test.ts b/src/BatchEventData.test.ts deleted file mode 100644 index 9282a04..0000000 --- a/src/BatchEventData.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { BatchEventAttributes, Consts } from './BatchEventAttributes'; -import * as Logger from './helpers/Logger'; - -describe('BatchEventData', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it(`handles less than or equal ${Consts.EventDataMaxTags} tags`, () => { - const batchEventData = new BatchEventAttributes(); - const spy = jest.spyOn(Logger, 'Log'); - - for (let i = 1; i <= Consts.EventDataMaxTags; i++) { - batchEventData.addTag(`tag ${i}`); - } - - expect(spy).not.toHaveBeenCalled(); - }); - it(`handles less than or equal ${Consts.EventDataMaxValues} attributes`, () => { - const batchEventData = new BatchEventAttributes(); - const spy = jest.spyOn(Logger, 'Log'); - - for (let i = 1; i <= Consts.EventDataMaxValues; i++) { - batchEventData.put(`key_${i}`, 'value'); - } - - expect(spy).not.toHaveBeenCalled(); - }); - it(`skips other tags after the first ${Consts.EventDataMaxTags}`, () => { - const batchEventData = new BatchEventAttributes(); - const spy = jest.spyOn(Logger, 'Log'); - - for (let i = 1; i <= Consts.EventDataMaxTags; i++) { - batchEventData.addTag(`tag ${i}`); - } - - batchEventData.addTag('too much'); - - expect(spy).toHaveBeenCalled(); - }); - it(`skips other attributes after the first ${Consts.EventDataMaxValues}`, () => { - const batchEventData = new BatchEventAttributes(); - const spy = jest.spyOn(Logger, 'Log'); - - for (let i = 1; i <= Consts.EventDataMaxValues; i++) { - batchEventData.put(`key_${i}`, 'value'); - } - - batchEventData.put('too_much', 'value'); - - expect(spy).toHaveBeenCalled(); - }); - it(`handles a date attribute`, () => { - const batchEventData = new BatchEventAttributes(); - const spy = jest.spyOn(Logger, 'Log'); - - batchEventData.putDate('test_date', Date.now()); - - expect(spy).not.toHaveBeenCalled(); - }); - it(`handles an url attribute`, () => { - const batchEventData = new BatchEventAttributes(); - const spy = jest.spyOn(Logger, 'Log'); - - batchEventData.putURL('test_url', 'https://batch.com'); - - expect(spy).not.toHaveBeenCalled(); - }); - it(`skips a too long url attribute`, () => { - const batchEventData = new BatchEventAttributes(); - const spy = jest.spyOn(Logger, 'Log'); - - batchEventData.putURL( - 'test_url', - `https://batch.com?${Array(2048) - .fill(1) - .join()}` - ); - - expect(spy).toHaveBeenCalled(); - }); -}); From d042a297295165707c571b9d8d79c44fe3dab0aa Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 31 May 2024 16:17:34 +0200 Subject: [PATCH 25/30] all: update changelog --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 383d73e..73cd341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,50 @@ 9.0.0 ---- -**Plugin** +This is a major release, please see our [migration guide](https://doc.batch.com/react-native/advanced/8x-migration/) for more info on how to update your current Batch implementation. -* Updated Batch 2.0 +**Plugin** +* Updated Batch to 2.0. For more information see the [ios](https://doc.batch.com/ios/sdk-changelog/#2_0_0) and [android](https://doc.batch.com/android/sdk-changelog/#2_0_0) changelog . * Batch requires iOS 13.0 or higher. * Batch requires a `minSdk` level of 21 or higher. -**User** +**iOS** +- The Batch React-Native plugin now automatically registers its own `UNUserNotificationCenterDelegate` and forwards it to the previous one if it exists. +This means you no longer need to add `[BatchUNUserNotificationCenterDelegate registerAsDelegate]` in your `AppDelegate`, please delete it. +It can be disabled by calling `BatchBridgeNotificationCenterDelegate.automaticallyRegister = false` before `[RNBatch start]`. + +**Core** +- Added method `isOptedOut` to checks whether Batch has been opted out from or not. +- Added method `updateAutomaticDataCollection` to fine-tune the data you authorize to be tracked by Batch. + +**User** - Removed method `trackTransaction` with no equivalent. +- Removed method `BatchUser.editor` and the related class `BatchUserEditor`, you should now use `BatchProfile.editor` which return an instance of `BatchProfileAttributeEditor`. +- Added method `clearInstallationData` which allows you to remove the installation data without modifying the current profile. + +**Event** + +This version introduced two new types of attribute that can be attached to an event : Array and Object. + +- Removed `trackEvent` APIs from the user module. You should now use `BatchProfile.trackEvent`. +- `BatchEventData` has been renamed into `BatchEventAttributes`. +- Removed `addTag` API from `BatchEventData` You should now use the `$tags` key with `put` method. +- Removed parameter `label` from `trackEvent` API. You should now use the `$label` key in `BatchEventAttributes` with the `put(string, string)` method. +- Added support for values of type: Array and Object to the `put` method. + +**Profile** + +Introduced `BatchProfile`, a new module that enables interacting with profiles. Its functionality replaces most of BatchUser used to do. + +- Added `identify` API as replacement of `BatchUser.editor().setIdentifier`. +- Added `editor` method to get a new instance of a `BatchProfileAttributeEditor` as replacement of `BatchUserEditor`. +- Added `trackEvent` API as replacement of the `BatchUser.trackEvent` methods. +- Added `trackLocation` API as replacement of the `BatchUser.trackLocation` method. + +**Expo** +- Added configuration field `enableDefaultOptOut` to control whether Batch is opted out from by default. (default: false) +- Added configuration fields `enableProfileCustomIDMigration` and `enableProfileCustomDataMigration` to control whether Batch should trigger the profile migrations (default: true). 8.2.0 ---- From 615e53b25a76b474d897aa7983f02a472cf89375 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 31 May 2024 16:24:17 +0200 Subject: [PATCH 26/30] all: bump version to 9.0.0 --- android/src/main/java/com/batch/batch_rn/RNBatchModule.java | 2 +- ios/RNBatch.h | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java index d0ffaec..93b98a7 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModule.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModule.java @@ -55,7 +55,7 @@ public class RNBatchModule extends ReactContextBaseJavaModule { private static final String NAME = "RNBatch"; private static final String PLUGIN_VERSION_ENVIRONMENT_VARIABLE = "batch.plugin.version"; - private static final String PLUGIN_VERSION = "ReactNative/8.2.0"; + private static final String PLUGIN_VERSION = "ReactNative/9.0.0"; public static final String LOGGER_TAG = "RNBatchBridge"; diff --git a/ios/RNBatch.h b/ios/RNBatch.h index 0b57ee4..060a5eb 100644 --- a/ios/RNBatch.h +++ b/ios/RNBatch.h @@ -12,7 +12,7 @@ #import -#define PluginVersion "ReactNative/8.2.0" +#define PluginVersion "ReactNative/9.0.0" @interface RNBatch : RCTEventEmitter diff --git a/package.json b/package.json index f5281d4..75724e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@batch.com/react-native-plugin", - "version": "8.2.0", + "version": "9.0.0", "description": "Batch.com React-Native Plugin", "homepage": "https://github.com/BatchLabs/Batch-React-Native-Plugin", "main": "dist/Batch.js", From 42373e77c5ea953d36bc23751f717d50fc9f378c Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 31 May 2024 16:31:43 +0200 Subject: [PATCH 27/30] all: fix eslint --- src/BatchEventAttributes.ts | 13 ++----------- src/BatchProfileAttributeEditor.ts | 1 - src/helpers/TypeHelpers.ts | 6 +++--- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/BatchEventAttributes.ts b/src/BatchEventAttributes.ts index e0427f0..5ab5068 100644 --- a/src/BatchEventAttributes.ts +++ b/src/BatchEventAttributes.ts @@ -1,14 +1,5 @@ import { Log } from './helpers/Logger'; import { isBoolean, isNumber, isObject, isObjectArray, isString, isStringArray } from './helpers/TypeHelpers'; - -export const Consts = { - AttributeKeyRegexp: /^[a-zA-Z0-9_]{1,30}$/, - EventDataMaxTags: 10, - EventDataMaxValues: 15, - EventDataStringMaxLength: 64, - EventDataURLMaxLength: 2048, -}; - export enum TypedEventAttributeType { String = 'string', Boolean = 'boolean', @@ -21,7 +12,7 @@ export enum TypedEventAttributeType { Object = 'object', } -export type TypedEventAttributeValue = string | boolean | number | TypedEventAttributes | Array; +export type TypedEventAttributeValue = string | boolean | number | string[] | TypedEventAttributes | TypedEventAttributes[]; export type TypedEventAttributes = { [key: string]: ITypedEventAttribute }; @@ -64,7 +55,7 @@ export class BatchEventAttributes { public put( key: string, - value: string | number | boolean | Array | BatchEventAttributes + value: string | number | boolean | string[] | BatchEventAttributes | BatchEventAttributes[] ): BatchEventAttributes { key = this.prepareAttributeKey(key); diff --git a/src/BatchProfileAttributeEditor.ts b/src/BatchProfileAttributeEditor.ts index 1e8624e..bed9f07 100644 --- a/src/BatchProfileAttributeEditor.ts +++ b/src/BatchProfileAttributeEditor.ts @@ -64,7 +64,6 @@ interface IUserSettingsRemoveFromArrayAction { value: string | string[]; } - type IUserSettingsAction = | IUserSettingsSetAttributeAction | IUserSettingsRemoveAttributeAction diff --git a/src/helpers/TypeHelpers.ts b/src/helpers/TypeHelpers.ts index cd2920e..83ca163 100644 --- a/src/helpers/TypeHelpers.ts +++ b/src/helpers/TypeHelpers.ts @@ -16,14 +16,14 @@ export const isObject = (value: unknown): value is BatchEventAttributes => { return value instanceof BatchEventAttributes; }; -export function isArray(value: unknown): value is Array { +export function isArray(value: unknown): value is unknown[] { return Array.isArray(value); } -export function isStringArray(value: unknown): value is Array { +export function isStringArray(value: unknown): value is string[] { return isArray(value) && value.every(it => isString(it)); } -export function isObjectArray(value: unknown): value is Array { +export function isObjectArray(value: unknown): value is BatchEventAttributes[] { return isArray(value) && value.every(it => isObject(it)); } From 01d9b6a7232ea1147165e251882a9fc9169772ac Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Tue, 4 Jun 2024 10:05:56 +0200 Subject: [PATCH 28/30] ios: avoid registering batch default delegate as previous one. --- ios/BatchBridgeNotificationCenterDelegate.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ios/BatchBridgeNotificationCenterDelegate.m b/ios/BatchBridgeNotificationCenterDelegate.m index daf441c..3566678 100644 --- a/ios/BatchBridgeNotificationCenterDelegate.m +++ b/ios/BatchBridgeNotificationCenterDelegate.m @@ -50,6 +50,12 @@ + (void)setAutomaticallyRegister:(BOOL)automaticallyRegister - (void)setPreviousDelegate:(nullable id)delegate { + // Do not register default Batch delegate as previous one + if (delegate == self || [delegate isKindOfClass:BatchUNUserNotificationCenterDelegate.class]) { + NSLog(@"RNBatch: It looks like you are still using [BatchUNUserNotificationCenterDelegate registerAsDelegate]. Please remove it or set `BatchBridgeNotificationCenterDelegate.automaticallyRegister = false` before [RNBatch start] but calling `setShowForegroundNotification` will not work anymore."); + _previousDelegate = nil; + return; + } // Do not register ourserlves as previous delegate to avoid // an infinite loop if (delegate == self || [delegate isKindOfClass:[self class]]) { From cca01dfff371fbd359e2eba4e1ee0b9ad66d87d0 Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Tue, 4 Jun 2024 11:09:30 +0200 Subject: [PATCH 29/30] ios: fix native callback invoked twice --- ios/RNBatch.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/RNBatch.m b/ios/RNBatch.m index fb1f64b..e07cf9c 100644 --- a/ios/RNBatch.m +++ b/ios/RNBatch.m @@ -421,8 +421,8 @@ -(void)stopObserving { resolve([NSNull null]); } else { reject(@"BatchBridgeError", @"Event attributes validation failed:", err); - return; } + return; } [BatchProfile trackEventWithName:name attributes:batchEventAttributes]; resolve([NSNull null]); From f2fb5516eac2dcb7d8a900ad9d4ab2a8b46f593e Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Thu, 13 Jun 2024 12:07:46 +0200 Subject: [PATCH 30/30] ios: remove wrong delegate check --- ios/BatchBridgeNotificationCenterDelegate.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/BatchBridgeNotificationCenterDelegate.m b/ios/BatchBridgeNotificationCenterDelegate.m index 3566678..32f7194 100644 --- a/ios/BatchBridgeNotificationCenterDelegate.m +++ b/ios/BatchBridgeNotificationCenterDelegate.m @@ -51,12 +51,12 @@ + (void)setAutomaticallyRegister:(BOOL)automaticallyRegister - (void)setPreviousDelegate:(nullable id)delegate { // Do not register default Batch delegate as previous one - if (delegate == self || [delegate isKindOfClass:BatchUNUserNotificationCenterDelegate.class]) { + if ([delegate isKindOfClass:BatchUNUserNotificationCenterDelegate.class]) { NSLog(@"RNBatch: It looks like you are still using [BatchUNUserNotificationCenterDelegate registerAsDelegate]. Please remove it or set `BatchBridgeNotificationCenterDelegate.automaticallyRegister = false` before [RNBatch start] but calling `setShowForegroundNotification` will not work anymore."); _previousDelegate = nil; return; } - // Do not register ourserlves as previous delegate to avoid + // Do not register ourselves as previous delegate to avoid // an infinite loop if (delegate == self || [delegate isKindOfClass:[self class]]) { _previousDelegate = nil;