From 4ef01ede9c36a6f557d25cf7c704271c54d99a40 Mon Sep 17 00:00:00 2001 From: Daniel Hok <dhok35@gmail.com> Date: Tue, 12 Sep 2023 13:10:44 -0400 Subject: [PATCH] Release version 7.0.0 --- BrazeProject/BrazeProject.tsx | 96 +++++++++++++++++- BrazeProject/Gemfile.lock | 6 +- BrazeProject/android/app/build.gradle | 1 + .../com/brazeproject/ReactNativeFlipper.java | 75 -------------- .../com/brazeproject/ReactNativeFlipper.kt | 68 +++++++++++++ .../java/com/brazeproject/MainActivity.java | 60 ------------ .../java/com/brazeproject/MainActivity.kt | 50 ++++++++++ .../com/brazeproject/MainApplication.java | 70 ------------- .../java/com/brazeproject/MainApplication.kt | 52 ++++++++++ .../com/brazeproject/ReactNativeFlipper.java | 20 ---- .../com/brazeproject/ReactNativeFlipper.kt | 22 +++++ BrazeProject/android/build.gradle | 10 +- BrazeProject/ios/BrazeProject/AppDelegate.mm | 24 ++++- BrazeProject/ios/Podfile.lock | 30 +++--- CHANGELOG.md | 23 +++++ __tests__/index.test.js | 98 ++++++++++++++++++- __tests__/jest.setup.js | 5 + android/build.gradle | 2 +- .../braze/reactbridge/BrazeReactBridgeImpl.kt | 64 ++++++++++-- .../com/braze/reactbridge/BrazeReactBridge.kt | 34 ++++++- .../com/braze/reactbridge/BrazeReactBridge.kt | 39 +++++++- braze-react-native-sdk.podspec | 6 +- .../BrazeReactBridge/BrazeReactBridge.mm | 70 ++++++++++++- iOS/replace-at-import-statements.sh | 3 +- package.json | 2 +- src/NativeBrazeReactModule.ts | 29 +++++- src/braze.js | 72 ++++++++++++-- src/index.d.ts | 67 +++++++++++-- 28 files changed, 804 insertions(+), 294 deletions(-) delete mode 100644 BrazeProject/android/app/src/debug/java/com/brazeproject/ReactNativeFlipper.java create mode 100644 BrazeProject/android/app/src/debug/java/com/brazeproject/ReactNativeFlipper.kt delete mode 100644 BrazeProject/android/app/src/main/java/com/brazeproject/MainActivity.java create mode 100644 BrazeProject/android/app/src/main/java/com/brazeproject/MainActivity.kt delete mode 100644 BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.java create mode 100644 BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.kt delete mode 100644 BrazeProject/android/app/src/release/java/com/brazeproject/ReactNativeFlipper.java create mode 100644 BrazeProject/android/app/src/release/java/com/brazeproject/ReactNativeFlipper.kt diff --git a/BrazeProject/BrazeProject.tsx b/BrazeProject/BrazeProject.tsx index 16062217..e10d98d0 100644 --- a/BrazeProject/BrazeProject.tsx +++ b/BrazeProject/BrazeProject.tsx @@ -10,6 +10,7 @@ import { Alert, TextInput, Platform, + Settings, } from 'react-native'; import RadioGroup from 'react-native-radio-buttons-group'; import Braze, { BrazeInAppMessage } from '@braze/react-native-sdk'; @@ -18,6 +19,9 @@ import Braze, { BrazeInAppMessage } from '@braze/react-native-sdk'; // and impressions for in-app messages and content cards. const automaticallyInteract = false; +// User Defaults keys +const iOSPushAutoEnabledKey = "iOSPushAutoEnabled"; + const Space = () => { return <View style={styles.spacing} />; }; @@ -37,6 +41,14 @@ export const BrazeProject = (): ReactElement => { useState<string>('bool'); const [featureFlagPropertyKey, setFeatureFlagPropertyKey] = useState(''); + // If key is persisted, use the value. If no key is present, default to true. + const [iOSPushAutoEnabled, setiOSPushAutoEnabled] = useState<boolean>( + () => { + const value = Settings.get(iOSPushAutoEnabledKey); + return (value != null) ? Boolean(value) : true; + } + ); + const subscriptionStateButtons = useMemo( () => [ { @@ -122,13 +134,13 @@ export const BrazeProject = (): ReactElement => { ]); }; - const showToast = (msg: string) => { + const showToast = (msg: string, duration: number = 2000) => { setMessage(msg); setToastVisible(true); setTimeout(() => { setToastVisible(false); setMessage('Success'); - }, 2000); + }, duration); }; useEffect(() => { @@ -358,10 +370,20 @@ export const BrazeProject = (): ReactElement => { Braze.setCustomUserAttribute('intattr', 88); Braze.setCustomUserAttribute('booleanattr', true); Braze.setCustomUserAttribute('dateattr', new Date()); - Braze.setCustomUserAttribute('arrayattr', ['a', 'b']); + Braze.setCustomUserAttribute('stringArrayAttr', ['a', 'b']); + Braze.setCustomUserAttribute('arrayOfObjectsAttr', [{ 'one': 1, 'two': 'too' }, { 'three': 3, 'four': 'fore' }]); + Braze.setCustomUserAttribute('objectAttr', { 'one': 1, 'two': 'too' }, false); + Braze.setCustomUserAttribute('badArray', ['123', { 'one': 1, 'two': 2 }]); + Braze.setCustomUserAttribute('badArray2', [true, 1, 'string', { 'one': 1 }]); showToast('Custom attributes set'); }; + const logCustomAttributeWithMergePress = () => { + Braze.setCustomUserAttribute('objectAttr', { 'three': 3, 'four': 'fore' }, true); + Braze.setCustomUserAttribute('objectAttr', { 'two': 'updated_too' }, true); + showToast('NCA with merge called'); + }; + const logUserPropertiesPress = () => { Braze.setFirstName('Brian'); Braze.setLastName('Wheeler'); @@ -396,6 +418,15 @@ export const BrazeProject = (): ReactElement => { showToast('Feature Flags refresh requested'); }; + const logFeatureFlagImpressionPress = () => { + if (!featureFlagId) { + console.log('No Feature Flag ID entered. Not logging Feature Flag Impression.'); + return; + } + Braze.logFeatureFlagImpression(featureFlagId); + showToast(`Feature Flag Impression logged for ID: ${featureFlagId}`); + }; + const unsetCustomUserAttributePress = () => { Braze.unsetCustomUserAttribute('sk'); showToast('Custom attribute unset'); @@ -499,6 +530,14 @@ export const BrazeProject = (): ReactElement => { showToast('Geofences Requested'); }; + const setLastKnownLocation = () => { + Braze.setLastKnownLocation(40.7128, 74.0060, 23.0, 25.0, 19.0); + Braze.setLastKnownLocation(40.7128, 74.0060, null, null, null); + Braze.setLastKnownLocation(40.7128, 74.0060, null, 25.0, null); + Braze.setLastKnownLocation(40.7128, 74.0060, 23.0, 25.0, null); + showToast('Last known location set'); + } + const setLocationCustomAttribute = () => { Braze.setLocationCustomAttribute('work', 40.7128, 74.006); showToast('Location Set'); @@ -544,15 +583,50 @@ export const BrazeProject = (): ReactElement => { for (const card of cards) { const cardId = card.id; console.log(`Got content card: ${JSON.stringify(card)}`); + + // Programmatically log impression, card click, and dismissal if (automaticallyInteract) { console.log('Automatically logging CC click and impression.'); Braze.logContentCardClicked(cardId); Braze.logContentCardImpression(cardId); + // To automate card dismissal, uncomment out the code below // Braze.logContentCardDismissed(cardId); } } }; + const getCachedContentCards = async () => { + const cards = await Braze.getCachedContentCards(); + console.log(`${cards.length} cached Content Cards found.`); + if (cards.length === 0) { + return; + } + + for (const card of cards) { + const cardId = card.id; + console.log(`Got content card from cache: ${JSON.stringify(card)}`); + + // Programmatically log impression, card click, and dismissal + if (automaticallyInteract) { + console.log(`Automatically logging CC click and impression for card ID: ${cardId}.`); + Braze.logContentCardClicked(cardId); + Braze.logContentCardImpression(cardId); + // To automate card dismissal, uncomment out the code below + // Braze.logContentCardDismissed(cardId); + } + } + }; + + // Update value, then store in NSUserDefaults to fetch in iOS layer + const toggleiOSPushAutoEnabled = () => { + const updatedValue = !iOSPushAutoEnabled; + setiOSPushAutoEnabled(updatedValue); + Settings.set({ iOSPushAutoEnabledKey: updatedValue }); + + console.log(`iOS Push Auto enabled: ${updatedValue}`); + showToast(`iOS Push Automation: ${updatedValue}.\n Restart your app to take effect.`, 4000); + }; + const requestPushPermission = () => { const options = { alert: true, @@ -636,6 +710,7 @@ export const BrazeProject = (): ReactElement => { </View> {/* Events */} + <View style={styles.row}> <TextInput style={styles.textInput} @@ -719,6 +794,9 @@ export const BrazeProject = (): ReactElement => { <TouchableHighlight onPress={logCustomAttributePress}> <Text>Set Custom User Attributes</Text> </TouchableHighlight> + <TouchableHighlight onPress={logCustomAttributeWithMergePress}> + <Text>Set Custom User Attributes with NCA Merge</Text> + </TouchableHighlight> <TouchableHighlight onPress={unsetCustomUserAttributePress}> <Text>Unset Custom User Attributes</Text> </TouchableHighlight> @@ -747,6 +825,9 @@ export const BrazeProject = (): ReactElement => { <TouchableHighlight onPress={getContentCards}> <Text>Get Content Cards {'&'} Log interactions</Text> </TouchableHighlight> + <TouchableHighlight onPress={getCachedContentCards}> + <Text>Get Cached Content Cards {'&'} Log interactions</Text> + </TouchableHighlight> {/* News Feed */} @@ -764,6 +845,9 @@ export const BrazeProject = (): ReactElement => { <TouchableHighlight onPress={refreshFeatureFlagsPress}> <Text>Refresh Feature Flags</Text> </TouchableHighlight> + <TouchableHighlight onPress={logFeatureFlagImpressionPress}> + <Text>Log Feature Flag Impression</Text> + </TouchableHighlight> <TouchableHighlight onPress={getFeatureFlagsPress}> <Text>Get All Feature Flags</Text> </TouchableHighlight> @@ -811,6 +895,9 @@ export const BrazeProject = (): ReactElement => { <TouchableHighlight onPress={requestGeofences}> <Text>Request Geofences</Text> </TouchableHighlight> + <TouchableHighlight onPress={setLastKnownLocation}> + <Text>Set Last Known Location</Text> + </TouchableHighlight> <TouchableHighlight onPress={setLocationCustomAttribute}> <Text>Set Custom Location Attribute</Text> </TouchableHighlight> @@ -818,6 +905,9 @@ export const BrazeProject = (): ReactElement => { {/* Other */} <Space /> + <TouchableHighlight onPress={toggleiOSPushAutoEnabled}> + <Text>Toggle iOS Push Automation enabled</Text> + </TouchableHighlight> <TouchableHighlight onPress={requestPushPermission}> <Text>Request Push Permission</Text> </TouchableHighlight> diff --git a/BrazeProject/Gemfile.lock b/BrazeProject/Gemfile.lock index 083aa1d4..4e285cae 100644 --- a/BrazeProject/Gemfile.lock +++ b/BrazeProject/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (7.0.4.3) + activesupport (7.0.7.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -62,10 +62,10 @@ GEM fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) json (2.6.3) - minitest (5.18.0) + minitest (5.19.0) molinillo (0.8.0) nanaimo (0.3.0) nap (1.1.0) diff --git a/BrazeProject/android/app/build.gradle b/BrazeProject/android/app/build.gradle index 3629c43b..0f464722 100644 --- a/BrazeProject/android/app/build.gradle +++ b/BrazeProject/android/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: "com.android.application" apply plugin: "com.facebook.react" +apply plugin: "kotlin-android" import com.android.build.OutputFile diff --git a/BrazeProject/android/app/src/debug/java/com/brazeproject/ReactNativeFlipper.java b/BrazeProject/android/app/src/debug/java/com/brazeproject/ReactNativeFlipper.java deleted file mode 100644 index 58da2e33..00000000 --- a/BrazeProject/android/app/src/debug/java/com/brazeproject/ReactNativeFlipper.java +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * <p>This source code is licensed under the MIT license found in the LICENSE file in the root - * directory of this source tree. - */ -package com.brazeproject; - -import android.content.Context; -import com.facebook.flipper.android.AndroidFlipperClient; -import com.facebook.flipper.android.utils.FlipperUtils; -import com.facebook.flipper.core.FlipperClient; -import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; -import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; -import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; -import com.facebook.flipper.plugins.inspector.DescriptorMapping; -import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; -import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; -import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; -import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; -import com.facebook.react.ReactInstanceEventListener; -import com.facebook.react.ReactInstanceManager; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.modules.network.NetworkingModule; -import okhttp3.OkHttpClient; - -/** - * Class responsible of loading Flipper inside your React Native application. This is the debug - * flavor of it. Here you can add your own plugins and customize the Flipper setup. - */ -public class ReactNativeFlipper { - public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { - if (FlipperUtils.shouldEnableFlipper(context)) { - final FlipperClient client = AndroidFlipperClient.getInstance(context); - - client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); - client.addPlugin(new DatabasesFlipperPlugin(context)); - client.addPlugin(new SharedPreferencesFlipperPlugin(context)); - client.addPlugin(CrashReporterPlugin.getInstance()); - - NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); - NetworkingModule.setCustomClientBuilder( - new NetworkingModule.CustomClientBuilder() { - @Override - public void apply(OkHttpClient.Builder builder) { - builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); - } - }); - client.addPlugin(networkFlipperPlugin); - client.start(); - - // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized - // Hence we run if after all native modules have been initialized - ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); - if (reactContext == null) { - reactInstanceManager.addReactInstanceEventListener( - new ReactInstanceEventListener() { - @Override - public void onReactContextInitialized(ReactContext reactContext) { - reactInstanceManager.removeReactInstanceEventListener(this); - reactContext.runOnNativeModulesQueueThread( - new Runnable() { - @Override - public void run() { - client.addPlugin(new FrescoFlipperPlugin()); - } - }); - } - }); - } else { - client.addPlugin(new FrescoFlipperPlugin()); - } - } - } -} diff --git a/BrazeProject/android/app/src/debug/java/com/brazeproject/ReactNativeFlipper.kt b/BrazeProject/android/app/src/debug/java/com/brazeproject/ReactNativeFlipper.kt new file mode 100644 index 00000000..564e93b9 --- /dev/null +++ b/BrazeProject/android/app/src/debug/java/com/brazeproject/ReactNativeFlipper.kt @@ -0,0 +1,68 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * + * This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + */ +package com.brazeproject + +import android.content.Context +import com.facebook.flipper.android.AndroidFlipperClient +import com.facebook.flipper.android.utils.FlipperUtils +import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin +import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin +import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin +import com.facebook.flipper.plugins.inspector.DescriptorMapping +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin +import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor +import com.facebook.flipper.plugins.network.NetworkFlipperPlugin +import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin +import com.facebook.react.ReactInstanceEventListener +import com.facebook.react.ReactInstanceManager +import com.facebook.react.bridge.ReactContext +import com.facebook.react.modules.network.NetworkingModule + +/** + * Class responsible of loading Flipper inside your React Native application. This is the debug + * flavor of it. Here you can add your own plugins and customize the Flipper setup. + */ +object ReactNativeFlipper { + @JvmStatic + fun initializeFlipper(context: Context?, reactInstanceManager: ReactInstanceManager) { + if (FlipperUtils.shouldEnableFlipper(context)) { + val client = AndroidFlipperClient.getInstance(context) + client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())) + client.addPlugin(DatabasesFlipperPlugin(context)) + client.addPlugin(SharedPreferencesFlipperPlugin(context)) + client.addPlugin(CrashReporterPlugin.getInstance()) + val networkFlipperPlugin = NetworkFlipperPlugin() + NetworkingModule.setCustomClientBuilder { builder -> + builder.addNetworkInterceptor( + FlipperOkhttpInterceptor(networkFlipperPlugin) + ) + } + client.addPlugin(networkFlipperPlugin) + client.start() + + // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized + // Hence we run if after all native modules have been initialized + val reactContext = reactInstanceManager.currentReactContext + if (reactContext == null) { + reactInstanceManager.addReactInstanceEventListener( + object : ReactInstanceEventListener { + override fun onReactContextInitialized(reactContext: ReactContext) { + reactInstanceManager.removeReactInstanceEventListener(this) + reactContext.runOnNativeModulesQueueThread { + client.addPlugin( + FrescoFlipperPlugin() + ) + } + } + }) + } else { + client.addPlugin(FrescoFlipperPlugin()) + } + } + } +} diff --git a/BrazeProject/android/app/src/main/java/com/brazeproject/MainActivity.java b/BrazeProject/android/app/src/main/java/com/brazeproject/MainActivity.java deleted file mode 100644 index f4d0785e..00000000 --- a/BrazeProject/android/app/src/main/java/com/brazeproject/MainActivity.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.brazeproject; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; -import android.widget.Toast; - -import com.braze.support.BrazeLogger; -import com.facebook.react.ReactActivity; -import com.facebook.react.ReactActivityDelegate; -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; -import com.facebook.react.defaults.DefaultReactActivityDelegate; - -public class MainActivity extends ReactActivity { - private static final String TAG = BrazeLogger.getBrazeLogTag(MainActivity.class); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Intent intent = getIntent(); - Uri data = intent.getData(); - if (data != null) { - Toast.makeText(this, "Activity opened by deep link: " + data.toString(), Toast.LENGTH_LONG).show(); - Log.i(TAG, "Deep link is " + data.toString()); - } - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - } - - /** - * Returns the name of the main component registered from JavaScript. This is used to schedule - * rendering of the component. - */ - @Override - protected String getMainComponentName() { - return "BrazeProject"; - } - - /** - * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link - * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React - * (aka React 18) with two boolean flags. - */ - @Override - protected ReactActivityDelegate createReactActivityDelegate() { - return new DefaultReactActivityDelegate( - this, - getMainComponentName(), - // If you opted-in for the New Architecture, we enable the Fabric Renderer. - DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled - // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18). - DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled - ); - } -} diff --git a/BrazeProject/android/app/src/main/java/com/brazeproject/MainActivity.kt b/BrazeProject/android/app/src/main/java/com/brazeproject/MainActivity.kt new file mode 100644 index 00000000..ef9ccbcc --- /dev/null +++ b/BrazeProject/android/app/src/main/java/com/brazeproject/MainActivity.kt @@ -0,0 +1,50 @@ +package com.brazeproject + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import com.braze.support.BrazeLogger.getBrazeLogTag +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.concurrentReactEnabled +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +class MainActivity : ReactActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + intent.data?.let { data -> + Toast.makeText(this, "Activity opened by deep link: $data", Toast.LENGTH_LONG).show() + Log.i(TAG, "Deep link is $data") + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + } + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "BrazeProject" + + /** + * Returns the instance of the [ReactActivityDelegate]. Here we use a util class [ ] which allows + * you to easily enable Fabric and Concurrent React (aka React 18) with two boolean flags. + */ + override fun createReactActivityDelegate(): ReactActivityDelegate { + return DefaultReactActivityDelegate( + activity = this, + mainComponentName = mainComponentName, + fabricEnabled = fabricEnabled, + concurrentReactEnabled = concurrentReactEnabled + ) + } + + companion object { + private val TAG = getBrazeLogTag(MainActivity::class.java) + } +} diff --git a/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.java b/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.java deleted file mode 100644 index 9d124768..00000000 --- a/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.brazeproject; - -import android.app.Application; -import android.util.Log; - -import com.braze.BrazeActivityLifecycleCallbackListener; -import com.braze.support.BrazeLogger; -import com.facebook.react.PackageList; -import com.facebook.react.ReactApplication; -import com.facebook.react.ReactNativeHost; -import com.facebook.react.ReactPackage; -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; -import com.facebook.react.defaults.DefaultReactNativeHost; -import com.facebook.soloader.SoLoader; -import java.util.List; - -public class MainApplication extends Application implements ReactApplication { - - private final ReactNativeHost mReactNativeHost = - new DefaultReactNativeHost(this) { - @Override - public boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } - - @Override - protected List<ReactPackage> getPackages() { - @SuppressWarnings("UnnecessaryLocalVariable") - List<ReactPackage> packages = new PackageList(this).getPackages(); - // Packages that cannot be autolinked yet can be added manually here, for example: - // packages.add(new MyReactNativePackage()); - return packages; - } - - @Override - protected String getJSMainModuleName() { - return "index"; - } - - @Override - protected boolean isNewArchEnabled() { - return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; - } - - @Override - protected Boolean isHermesEnabled() { - return BuildConfig.IS_HERMES_ENABLED; - } - }; - - @Override - public ReactNativeHost getReactNativeHost() { - return mReactNativeHost; - } - - @Override - public void onCreate() { - super.onCreate(); - - registerActivityLifecycleCallbacks(new BrazeActivityLifecycleCallbackListener()); - BrazeLogger.setLogLevel(Log.VERBOSE); - SoLoader.init(this, /* native exopackage */ false); - - if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - // If you opted-in for the New Architecture, we load the native entry point for this app. - DefaultNewArchitectureEntryPoint.load(); - } - ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); - } -} diff --git a/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.kt b/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.kt new file mode 100644 index 00000000..670acd83 --- /dev/null +++ b/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.kt @@ -0,0 +1,52 @@ +package com.brazeproject + +import android.app.Application +import android.util.Log +import com.braze.BrazeActivityLifecycleCallbackListener +import com.braze.support.BrazeLogger.logLevel +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.soloader.SoLoader + +class MainApplication : Application(), ReactApplication { + private val reactNativeHost = object: DefaultReactNativeHost(this) { + override fun getUseDeveloperSupport(): Boolean { + return BuildConfig.DEBUG + } + + override fun getPackages(): List<ReactPackage> { + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(new MyReactNativePackage()); + return PackageList(this).packages + } + + override fun getJSMainModuleName(): String { + return "index" + } + + override val isNewArchEnabled: Boolean + get() = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean + get() = BuildConfig.IS_HERMES_ENABLED + } + + override fun getReactNativeHost(): ReactNativeHost { + return reactNativeHost + } + + override fun onCreate() { + super.onCreate() + registerActivityLifecycleCallbacks(BrazeActivityLifecycleCallbackListener()) + logLevel = Log.VERBOSE + SoLoader.init(this,false) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager) + } +} diff --git a/BrazeProject/android/app/src/release/java/com/brazeproject/ReactNativeFlipper.java b/BrazeProject/android/app/src/release/java/com/brazeproject/ReactNativeFlipper.java deleted file mode 100644 index 476d7ec6..00000000 --- a/BrazeProject/android/app/src/release/java/com/brazeproject/ReactNativeFlipper.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * <p>This source code is licensed under the MIT license found in the LICENSE file in the root - * directory of this source tree. - */ -package com.brazeproject; - -import android.content.Context; -import com.facebook.react.ReactInstanceManager; - -/** - * Class responsible of loading Flipper inside your React Native application. This is the release - * flavor of it so it's empty as we don't want to load Flipper. - */ -public class ReactNativeFlipper { - public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { - // Do nothing as we don't want to initialize Flipper on Release. - } -} diff --git a/BrazeProject/android/app/src/release/java/com/brazeproject/ReactNativeFlipper.kt b/BrazeProject/android/app/src/release/java/com/brazeproject/ReactNativeFlipper.kt new file mode 100644 index 00000000..a2833046 --- /dev/null +++ b/BrazeProject/android/app/src/release/java/com/brazeproject/ReactNativeFlipper.kt @@ -0,0 +1,22 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * + * This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + */ +package com.brazeproject + +import android.content.Context +import com.facebook.react.ReactInstanceManager + +/** + * Class responsible of loading Flipper inside your React Native application. This is the release + * flavor of it so it's empty as we don't want to load Flipper. + */ +object ReactNativeFlipper { + @JvmStatic + fun initializeFlipper(context: Context?, reactInstanceManager: ReactInstanceManager) { + // Do nothing as we don't want to initialize Flipper on Release. + } +} diff --git a/BrazeProject/android/build.gradle b/BrazeProject/android/build.gradle index c1c7a760..dbb7670a 100644 --- a/BrazeProject/android/build.gradle +++ b/BrazeProject/android/build.gradle @@ -2,13 +2,14 @@ buildscript { ext { - buildToolsVersion = "33.0.0" + buildToolsVersion = "34.0.0" minSdkVersion = 21 - compileSdkVersion = 33 - targetSdkVersion = 33 + compileSdkVersion = 34 + targetSdkVersion = 34 // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. ndkVersion = "23.1.7779620" + kotlin_version = "1.8.10" } repositories { google() @@ -18,6 +19,7 @@ buildscript { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") classpath "com.google.gms:google-services:4.3.4" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10" + + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}" } } diff --git a/BrazeProject/ios/BrazeProject/AppDelegate.mm b/BrazeProject/ios/BrazeProject/AppDelegate.mm index 54d8162d..c5731760 100644 --- a/BrazeProject/ios/BrazeProject/AppDelegate.mm +++ b/BrazeProject/ios/BrazeProject/AppDelegate.mm @@ -18,6 +18,9 @@ @implementation AppDelegate static NSString *const apiKey = @"b7271277-0fec-4187-beeb-3ae6e6fbed11"; static NSString *const endpoint = @"sondheim.braze.com"; +// User Defaults keys +static NSString *const iOSPushAutoEnabledKey = @"iOSPushAutoEnabled"; + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.moduleName = @"BrazeProject"; @@ -27,17 +30,34 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( BRZConfiguration *configuration = [[BRZConfiguration alloc] initWithApiKey:apiKey endpoint:endpoint]; configuration.triggerMinimumTimeInterval = 1; configuration.logger.level = BRZLoggerLevelInfo; + + // Default to automatically setting up push notifications + BOOL pushAutoEnabled = YES; + if ([[NSUserDefaults standardUserDefaults] objectForKey:iOSPushAutoEnabledKey]) { + pushAutoEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:iOSPushAutoEnabledKey]; + } + if (pushAutoEnabled) { + NSLog(@"iOS Push Auto enabled."); + configuration.push.automation = + [[BRZConfigurationPushAutomation alloc] initEnablingAllAutomations:YES]; + } + Braze *braze = [BrazeReactBridge initBraze:configuration]; braze.delegate = [[BrazeReactDelegate alloc] init]; AppDelegate.braze = braze; - [self registerForPushNotifications]; + if (!pushAutoEnabled) { + // If the user explicitly disables Push Auto, register for push manually + NSLog(@"iOS Push Auto disabled - Registering for push manually."); + [self registerForPushNotifications]; + } + [[BrazeReactUtils sharedInstance] populateInitialUrlFromLaunchOptions:launchOptions]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } -#pragma mark - Push Notifications +#pragma mark - Push Notifications (manual integration) - (void)registerForPushNotifications { UNUserNotificationCenter *center = UNUserNotificationCenter.currentNotificationCenter; diff --git a/BrazeProject/ios/Podfile.lock b/BrazeProject/ios/Podfile.lock index a9cec9e0..bb941367 100644 --- a/BrazeProject/ios/Podfile.lock +++ b/BrazeProject/ios/Podfile.lock @@ -1,20 +1,20 @@ PODS: - boost (1.76.0) - - braze-react-native-sdk (6.0.1): - - BrazeKit (~> 6.4.0) - - BrazeLocation (~> 6.4.0) - - BrazeUI (~> 6.4.0) + - braze-react-native-sdk (7.0.0): + - BrazeKit (~> 6.6.0) + - BrazeLocation (~> 6.6.0) + - BrazeUI (~> 6.6.0) - RCT-Folly - RCTRequired - RCTTypeSafety - React-Codegen - React-Core - ReactCommon/turbomodule/core - - BrazeKit (6.4.0) - - BrazeLocation (6.4.0): - - BrazeKit (= 6.4.0) - - BrazeUI (6.4.0): - - BrazeKit (= 6.4.0) + - BrazeKit (6.6.0) + - BrazeLocation (6.6.0): + - BrazeKit (= 6.6.0) + - BrazeUI (6.6.0): + - BrazeKit (= 6.6.0) - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) - FBLazyVector (0.72.3) @@ -1259,10 +1259,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 57d2868c099736d80fcd648bf211b4431e51a558 - braze-react-native-sdk: 2b73f41c98a2ec56ea2ff9fb0ac521f8900f2a54 - BrazeKit: 47ebb49398e645ef8cd1df40be9ea7f648029283 - BrazeLocation: 4e25584206096cf060c4e22e14d8a2eb541152d4 - BrazeUI: 56929e8d9825a532e1cbf3b713df7c5f5791ff17 + braze-react-native-sdk: 0c255c9cf2cd2a298d1748f97617cbea8322b184 + BrazeKit: 3c25fd2317ef39059c7305074c3662971f6373cc + BrazeLocation: bbf48b75305580157977e4d10466b7b849eaac83 + BrazeUI: cdd5fef144df0acfda9c46a91ff8bc3217a5c7e1 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 4cce221dd782d3ff7c4172167bba09d58af67ccb @@ -1301,7 +1301,7 @@ SPEC CHECKSUMS: React-perflogger: 6bd153e776e6beed54c56b0847e1220a3ff92ba5 React-RCTActionSheet: c0b62af44e610e69d9a2049a682f5dba4e9dff17 React-RCTAnimation: f9bf9719258926aea9ecb8a2aa2595d3ff9a6022 - React-RCTAppDelegate: bfa7bc41b4e3d8bcdbe0556a7f110f74dee628d1 + React-RCTAppDelegate: 2c72015d97aa99eee3614880b92167632c5413d6 React-RCTBlob: c4f1e69a6ef739aa42586b876d637dab4e3b5bed React-RCTFabric: 125e7d77057cb62f94e79ac977fd68be042a3bc5 React-RCTImage: e5798f01aba248416c02a506cf5e6dfcba827638 @@ -1321,4 +1321,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: a5a205f87842532db01e7cc5eee03c5db5604934 -COCOAPODS: 1.12.0 +COCOAPODS: 1.12.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index bebec1ea..55576a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## 7.0.0 + +##### Breaking +- Updates the native Android bridge [from Braze Android SDK 26.3.2 to 27.0.1](https://github.com/braze-inc/braze-android-sdk/blob/master/CHANGELOG.md#2701). + +##### Fixed +- Fixes the Android layer to record date custom user attributes as ISO strings instead of integers. +- Fixes a bug introduced in `6.0.0` where `Braze.getInitialUrl()` may not trigger the callback on Android. + +##### Added +- Updates the native iOS bridge [from Braze Swift SDK 6.4.0 to 6.6.0](https://github.com/braze-inc/braze-swift-sdk/compare/6.4.0...6.6.0#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed). +- Adds support for nested custom user attributes. + - The `setCustomUserAttribute` now accepts objects and arrays of objects. + - Adds an optional `merge` parameter to the `setCustomUserAttribute` method. This is a non-breaking change. + - Reference our [public docs](https://www.braze.com/docs/user_guide/data_and_analytics/custom_data/custom_attributes/nested_custom_attribute_support/) for more information. +- Adds `Braze.setLastKnownLocation()` to set the last known location for the user. +- Adds `Braze.registerPushToken()` in the JavaScript layer to post a push token to Braze's servers. + - Deprecates `Braze.registerAndroidPushToken()` in favor of `Braze.registerPushToken()`. +- Adds `Braze.getCachedContentCards()` to get the most recent content cards from the cache, without a refresh. +- Adds support for the Feature Flag method `logFeatureFlagImpression(id)`. + ## 6.0.2 ##### Fixed @@ -14,6 +35,8 @@ - If you are using the New Architecture, this version requires React Native `0.70` or higher. - Fixes the sample setup steps for iOS apps conforming to `RCTAppDelegate`. - ⚠️ If your app conforms to `RCTAppDelegate` and was following our previous `AppDelegate` setup in the sample project or [Braze documentation](https://www.braze.com/docs/developer_guide/platform_integration_guides/react_native/react_sdk_setup/?tab=ios#step-2-complete-native-setup), you will need to reference our [updated samples](https://github.com/braze-inc/braze-react-native-sdk/blob/master/BrazeProject/ios/BrazeProject/AppDelegate.mm) to prevent any crashes from occurring when subscribing to events in the new Turbo Module. ⚠️ +- If your project contains unit tests that depend on the Braze React Native module, you will need to update your imports to the `NativeBrazeReactModule` file to properly mock the Turbo Module functions in Jest. + - For an example, refer to the sample test setup [here](https://github.com/braze-inc/braze-react-native-sdk/tree/master/__tests__). - Updates the native Android bridge [from Braze Android SDK 25.0.0 to 26.3.1](https://github.com/braze-inc/braze-android-sdk/compare/v25.0.0...v26.3.1#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed). - Fixes the presentation of in-app messages to match the documented behavior. - Calling `subscribeToInAppMessages` or `addListener` in the Javascript layer will no longer cause a custom `BrazeInAppMessageUIDelegate` implementation on iOS to be ignored. diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 5d64746d..3553eecc 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -2,6 +2,7 @@ const NativeEventEmitter = require('react-native').NativeEventEmitter; const NativeBrazeReactModule = require('../src/NativeBrazeReactModule').default; import Braze from '../src/index'; +import { Platform } from 'react-native'; console.log = jest.fn(); const testCallback = jest.fn(); @@ -15,8 +16,19 @@ afterEach(() => { test('it calls BrazeReactBridge.registerAndroidPushToken', () => { const token = "some_token"; Braze.registerAndroidPushToken(token); - expect(NativeBrazeReactModule.registerAndroidPushToken).toBeCalledWith(token); - expect(NativeBrazeReactModule.registerAndroidPushToken).toBeCalledWith(token); + expect(NativeBrazeReactModule.registerPushToken).toBeCalledWith(token); +}); + +test('it calls BrazeReactBridge.registerPushToken', () => { + const token = "some_token"; + Braze.registerPushToken(token); + expect(NativeBrazeReactModule.registerPushToken).toBeCalledWith(token); +}); + +test('it calls BrazeReactBridge.registerPushToken with null', () => { + const token = null; + Braze.registerPushToken(token); + expect(NativeBrazeReactModule.registerPushToken).toBeCalledWith(token); }); test('it calls BrazeReactBridge.setGoogleAdvertisingId', () => { @@ -105,6 +117,11 @@ test('it calls BrazeReactBridge.getContentCards', () => { expect(NativeBrazeReactModule.getContentCards).toBeCalled(); }); +test('it calls BrazeReactBridge.getCachedContentCards', () => { + Braze.getCachedContentCards(); + expect(NativeBrazeReactModule.getCachedContentCards).toBeCalled(); +}); + test('it calls BrazeReactBridge.logContentCardClicked', () => { const id = "1234"; Braze.logContentCardClicked(id); @@ -163,6 +180,46 @@ test('it calls BrazeReactBridge.setLocationCustomAttribute', () => { expect(NativeBrazeReactModule.setLocationCustomAttribute).toBeCalledWith(key, latitude, longitude, testCallback); }); +test('it calls BrazeReactBridge.setLastKnownLocation', () => { + const latitude = 40.7128; + const longitude = 74.0060; + const altitude = 24.0; + const horizontalAccuracy = 25.0; + const verticalAccuracy = 26.0; + Braze.setLastKnownLocation(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); + expect(NativeBrazeReactModule.setLastKnownLocation).toBeCalledWith(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); +}); + +test('it calls BrazeReactBridge.setLastKnownLocation with 3 null', () => { + const latitude = 40.7128; + const longitude = 74.0060; + const altitude = null; + const horizontalAccuracy = null; + const verticalAccuracy = null; + Braze.setLastKnownLocation(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); + expect(NativeBrazeReactModule.setLastKnownLocation).toBeCalledWith(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); +}); + +test('it calls BrazeReactBridge.setLastKnownLocation with 2 null', () => { + const latitude = 40.7128; + const longitude = 74.0060; + const altitude = null; + const horizontalAccuracy = 25.0; + const verticalAccuracy = null; + Braze.setLastKnownLocation(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); + expect(NativeBrazeReactModule.setLastKnownLocation).toBeCalledWith(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); +}); + +test('it calls BrazeReactBridge.setLastKnownLocation with 1 null', () => { + const latitude = 40.7128; + const longitude = 74.0060; + const altitude = 24.0; + const horizontalAccuracy = 25.0; + const verticalAccuracy = null; + Braze.setLastKnownLocation(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); + expect(NativeBrazeReactModule.setLastKnownLocation).toBeCalledWith(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); +}); + test('it calls BrazeReactBridge.requestContentCardsRefresh', () => { Braze.requestContentCardsRefresh(); expect(NativeBrazeReactModule.requestContentCardsRefresh).toBeCalled(); @@ -235,6 +292,32 @@ test('it calls BrazeReactBridge.setCustomUserAttributeArray', () => { expect(NativeBrazeReactModule.setCustomUserAttributeArray).toBeCalledWith(key, array, testCallback); }); +test('it calls BrazeReactBridge.setCustomUserAttributeObjectArray', () => { + const key = "some_key"; + const array = [{'one': 1, 'two': 2, 'three': 3}, {'eight': 8, 'nine': 9}]; + Braze.setCustomUserAttribute(key, array, testCallback); + expect(NativeBrazeReactModule.setCustomUserAttributeObjectArray).toBeCalledWith(key, array, testCallback); +}); + +test('it calls BrazeReactBridge.setCustomUserAttributeObject 4 parameters', () => { + const key = "some_key"; + const hash = {'do': 're', 'mi': 'fa'} + const merge = true + Braze.setCustomUserAttribute(key, hash, merge, testCallback); + + expect(NativeBrazeReactModule.setCustomUserAttributeObject).toBeCalledWith(key, hash, merge, testCallback); +}); + +test('it calls BrazeReactBridge.setCustomUserAttributeObject 3 parameters', () => { + const key = "some_key"; + const hash = {'do': 're', 'mi': 'fa'} + // When not given, merge defaults to 'false' + const merge = false + Braze.setCustomUserAttribute(key, hash, testCallback); + + expect(NativeBrazeReactModule.setCustomUserAttributeObject).toBeCalledWith(key, hash, merge, testCallback); +}); + test('it calls BrazeReactBridge.setBoolCustomUserAttribute', () => { const key = "some_key"; const bool_value = true; @@ -360,10 +443,12 @@ test('it calls the callback with null and logs the error if BrazeReactBridge.get expect(console.log).toBeCalledWith("error"); }); -test('it calls the callback with null if BrazeReactBridge.getInitialUrl is not defined', () => { - NativeBrazeReactModule.getInitialURL = null; +test('it calls the callback with null if BrazeReactBridge.getInitialUrl is running on Android', () => { + const platform = Platform.OS; + Platform.OS = 'android'; Braze.getInitialURL(testCallback); expect(testCallback).toBeCalledWith(null); + Platform.OS = platform; }); test('it calls BrazeReactBridge.subscribeToInAppMessage', () => { @@ -538,6 +623,11 @@ test('it calls BrazeReactBridge.refreshFeatureFlags', () => { expect(NativeBrazeReactModule.refreshFeatureFlags).toBeCalled(); }); +test('it calls BrazeReactBridge.logFeatureFlagImpression', () => { + Braze.logFeatureFlagImpression('test'); + expect(NativeBrazeReactModule.logFeatureFlagImpression).toBeCalled(); +}) + test('it calls BrazeReactBridge.getFeatureFlagBooleanProperty', () => { Braze.getFeatureFlagBooleanProperty('id', 'key'); expect(NativeBrazeReactModule.getFeatureFlagBooleanProperty).toBeCalled(); diff --git a/__tests__/jest.setup.js b/__tests__/jest.setup.js index d686fcf9..fe21d095 100644 --- a/__tests__/jest.setup.js +++ b/__tests__/jest.setup.js @@ -18,6 +18,7 @@ jest.mock('react-native/Libraries/TurboModule/TurboModuleRegistry', () => { // Mocks the Turbo Module spec with Jest method stubs. NativeBrazeReactModule = { registerAndroidPushToken: jest.fn(), + registerPushToken: jest.fn(), setGoogleAdvertisingId: jest.fn(), setFirstName: jest.fn(), setLastName: jest.fn(), @@ -39,6 +40,7 @@ jest.mock('react-native/Libraries/TurboModule/TurboModuleRegistry', () => { logNewsFeedCardImpression: jest.fn(), launchContentCards: jest.fn(), getContentCards: jest.fn(), + getCachedContentCards: jest.fn(), logContentCardClicked: jest.fn(), logContentCardDismissed: jest.fn(), logContentCardImpression: jest.fn(), @@ -49,6 +51,8 @@ jest.mock('react-native/Libraries/TurboModule/TurboModuleRegistry', () => { wipeData: jest.fn(), setDateCustomUserAttribute: jest.fn(), setCustomUserAttributeArray: jest.fn(), + setCustomUserAttributeObject: jest.fn(), + setCustomUserAttributeObjectArray: jest.fn(), setBoolCustomUserAttribute: jest.fn(), setStringCustomUserAttribute: jest.fn(), setIntCustomUserAttribute: jest.fn(), @@ -80,6 +84,7 @@ jest.mock('react-native/Libraries/TurboModule/TurboModuleRegistry', () => { getFeatureFlag: jest.fn(), getAllFeatureFlags: jest.fn(), refreshFeatureFlags: jest.fn(), + logFeatureFlagImpression: jest.fn(), getFeatureFlagBooleanProperty: jest.fn(), getFeatureFlagNumberProperty: jest.fn(), getFeatureFlagStringProperty: jest.fn(), diff --git a/android/build.gradle b/android/build.gradle index c05bc14d..fe274e1f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -52,6 +52,6 @@ android { } dependencies { - api 'com.braze:android-sdk-ui:26.3.2' + api 'com.braze:android-sdk-ui:27.0.1' api 'com.facebook.react:react-native:+' } diff --git a/android/src/main/java/com/braze/reactbridge/BrazeReactBridgeImpl.kt b/android/src/main/java/com/braze/reactbridge/BrazeReactBridgeImpl.kt index d62127eb..c30a55dc 100644 --- a/android/src/main/java/com/braze/reactbridge/BrazeReactBridgeImpl.kt +++ b/android/src/main/java/com/braze/reactbridge/BrazeReactBridgeImpl.kt @@ -34,10 +34,13 @@ import com.braze.ui.inappmessage.InAppMessageOperation import com.braze.ui.inappmessage.listeners.DefaultInAppMessageManagerListener import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter +import org.json.JSONArray import org.json.JSONObject import java.math.BigDecimal import java.util.* import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock @Suppress("TooManyFunctions", "LargeClass") class BrazeReactBridgeImpl( @@ -49,6 +52,7 @@ class BrazeReactBridgeImpl( private val newsFeedCards = mutableListOf<Card>() private val newsFeedSubscriberMap: MutableMap<Callback, IEventSubscriber<FeedUpdatedEvent>?> = ConcurrentHashMap() private val callbackCallMap = ConcurrentHashMap<Callback, Boolean?>() + private val contentCardsLock = ReentrantLock() private var contentCardsUpdatedAt: Long = 0 private var newsFeedCardsUpdatedAt: Long = 0 private var inAppMessageDisplayOperation: InAppMessageOperation = InAppMessageOperation.DISPLAY_NOW @@ -92,7 +96,7 @@ class BrazeReactBridgeImpl( } } - fun registerAndroidPushToken(token: String?) { + fun registerPushToken(token: String?) { braze.registeredPushToken = token } @@ -166,7 +170,7 @@ class BrazeReactBridgeImpl( return } runOnUser { - callback.reportResult(it.setCustomUserAttribute(key, timeStamp.toLong())) + callback.reportResult(it.setCustomUserAttributeToSecondsFromEpoch(key, timeStamp.toLong())) } } @@ -190,6 +194,21 @@ class BrazeReactBridgeImpl( } } + fun setCustomUserAttributeObjectArray(key: String?, value: ReadableArray?, callback: Callback?) { + if (key == null) { + brazelog { "Key was null. Not logging setCustomUserAttributeObjectArray." } + return + } + if (value == null) { + brazelog { "Value was null. Not logging setCustomUserAttributeObjectArray." } + return + } + val attributeArray = JSONArray(parseReadableArray(value)) + runOnUser { + callback.reportResult(it.setCustomUserAttribute(key, attributeArray)) + } + } + fun setCustomUserAttributeArray(key: String?, value: ReadableArray?, callback: Callback?) { if (key == null) { brazelog { "Key was null. Not logging setCustomUserAttributeArray." } @@ -209,6 +228,17 @@ class BrazeReactBridgeImpl( } } + fun setCustomUserAttributeObject(key: String?, value: ReadableMap, merge: Boolean, callback: Callback?) { + if (key == null) { + brazelog { "Key was null. Not logging setCustomUserAttributeObject." } + return + } + val json = JSONObject(parseReadableMap(value)) + runOnUser { + callback.reportResult(it.setCustomAttribute(key, json, merge)) + } + } + fun addToCustomAttributeArray(key: String?, value: String?, callback: Callback?) { if (key == null || value == null) { brazelog { "Key or value was null. Not logging custom user attribute." } @@ -355,6 +385,12 @@ class BrazeReactBridgeImpl( braze.requestContentCardsRefresh() } + fun getCachedContentCards(promise: Promise?) { + contentCardsLock.withLock { + promise?.resolve(mapContentCards(contentCards)) + } + } + fun setSdkAuthenticationSignature(token: String?) { if (token != null) { braze.setSdkAuthenticationSignature(token) @@ -632,6 +668,12 @@ class BrazeReactBridgeImpl( } } + fun setLastKnownLocation(latitude: Double, longitude: Double, altitude: Double?, horizontalAccuracy: Double?, verticalAccuracy: Double?) { + runOnUser { + it.setLastKnownLocation(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy) + } + } + fun subscribeToInAppMessage(useBrazeUI: Boolean) { inAppMessageDisplayOperation = if (useBrazeUI) { InAppMessageOperation.DISPLAY_NOW @@ -706,9 +748,11 @@ class BrazeReactBridgeImpl( */ private fun updateContentCardsIfNeeded(event: ContentCardsUpdatedEvent) { if (event.timestampSeconds > contentCardsUpdatedAt) { - contentCardsUpdatedAt = event.timestampSeconds - contentCards.clear() - contentCards.addAll(event.allCards) + contentCardsLock.withLock { + contentCardsUpdatedAt = event.timestampSeconds + contentCards.clear() + contentCards.addAll(event.allCards) + } } } @@ -726,8 +770,10 @@ class BrazeReactBridgeImpl( private fun getNewsFeedCardById(id: String): Card? = newsFeedCards.firstOrNull { it.id == id } - private fun getContentCardById(id: String): Card? = - contentCards.firstOrNull { it.id == id } + private fun getContentCardById(id: String): Card? = + contentCardsLock.withLock { + contentCards.firstOrNull { it.id == id } + } fun getAllFeatureFlags(promise: Promise?) { val ffs = braze.getAllFeatureFlags() @@ -749,6 +795,10 @@ class BrazeReactBridgeImpl( braze.refreshFeatureFlags() } + fun logFeatureFlagImpression(id: String) { + braze.logFeatureFlagImpression(id) + } + fun getFeatureFlagBooleanProperty(id: String?, key: String?, promise: Promise?) { if (id != null && key != null && promise != null) { promise.resolve(braze.getFeatureFlag(id).getBooleanProperty(key)) diff --git a/android/src/newarch/com/braze/reactbridge/BrazeReactBridge.kt b/android/src/newarch/com/braze/reactbridge/BrazeReactBridge.kt index 944ab240..f41c6408 100644 --- a/android/src/newarch/com/braze/reactbridge/BrazeReactBridge.kt +++ b/android/src/newarch/com/braze/reactbridge/BrazeReactBridge.kt @@ -65,8 +65,8 @@ class BrazeReactBridge(reactContext: ReactApplicationContext): NativeBrazeReactM brazeImpl.setDateOfBirth(year.toInt(), month.toInt(), day.toInt()) } - override fun registerAndroidPushToken(token: String?) { - brazeImpl.registerAndroidPushToken(token) + override fun registerPushToken(token: String?) { + brazeImpl.registerPushToken(token) } override fun setGoogleAdvertisingId(googleAdvertisingId: String?, adTrackingEnabled: Boolean) { @@ -133,10 +133,22 @@ class BrazeReactBridge(reactContext: ReactApplicationContext): NativeBrazeReactM brazeImpl.setCustomUserAttributeArray(key, value, callback) } + override fun setCustomUserAttributeObjectArray( + key: String?, + value: ReadableArray?, + callback: Callback? + ) { + brazeImpl.setCustomUserAttributeObjectArray(key, value, callback) + } + override fun setDateCustomUserAttribute(key: String?, value: Double, callback: Callback?) { brazeImpl.setDateCustomUserAttribute(key, value.toInt(), callback) } + override fun setCustomUserAttributeObject(key: String?, value: ReadableMap, merge: Boolean, callback: Callback?) { + brazeImpl.setCustomUserAttributeObject(key, value, merge, callback) + } + override fun addToCustomUserAttributeArray(key: String?, value: String?, callback: Callback?) { brazeImpl.addToCustomAttributeArray(key, value, callback) } @@ -206,6 +218,10 @@ class BrazeReactBridge(reactContext: ReactApplicationContext): NativeBrazeReactM brazeImpl.getContentCards(promise) } + override fun getCachedContentCards(promise: Promise?) { + brazeImpl.getCachedContentCards(promise) + } + override fun getCardCountForCategories(category: String?, callback: Callback?) { brazeImpl.getCardCountForCategories(category, callback) } @@ -251,6 +267,16 @@ class BrazeReactBridge(reactContext: ReactApplicationContext): NativeBrazeReactM brazeImpl.setLocationCustomAttribute(key, latitude, longitude, callback) } + override fun setLastKnownLocation( + latitude: Double, + longitude: Double, + altitude: Double?, + horizontalAccuracy: Double?, + verticalAccuracy: Double? + ) { + brazeImpl.setLastKnownLocation(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy) + } + override fun subscribeToInAppMessage(useBrazeUI: Boolean, callback: Callback?) { brazeImpl.subscribeToInAppMessage(useBrazeUI) } @@ -299,6 +325,10 @@ class BrazeReactBridge(reactContext: ReactApplicationContext): NativeBrazeReactM brazeImpl.refreshFeatureFlags() } + override fun logFeatureFlagImpression(id: String) { + brazeImpl.logFeatureFlagImpression(id) + } + override fun addListener(eventType: String?) { eventType?.let { brazeImpl.addListener(it) diff --git a/android/src/oldarch/com/braze/reactbridge/BrazeReactBridge.kt b/android/src/oldarch/com/braze/reactbridge/BrazeReactBridge.kt index ca67c104..04e2dc30 100644 --- a/android/src/oldarch/com/braze/reactbridge/BrazeReactBridge.kt +++ b/android/src/oldarch/com/braze/reactbridge/BrazeReactBridge.kt @@ -79,8 +79,8 @@ class BrazeReactBridge(reactContext: ReactApplicationContext?) : ReactContextBas } @ReactMethod - fun registerAndroidPushToken(token: String?) { - brazeImpl.registerAndroidPushToken(token) + fun registerPushToken(token: String?) { + brazeImpl.registerPushToken(token) } @ReactMethod @@ -159,11 +159,25 @@ class BrazeReactBridge(reactContext: ReactApplicationContext?) : ReactContextBas brazeImpl.setCustomUserAttributeArray(key, value, callback) } + @ReactMethod + fun setCustomUserAttributeObjectArray( + key: String?, + value: ReadableArray?, + callback: Callback? + ) { + brazeImpl.setCustomUserAttributeObjectArray(key, value, callback) + } + @ReactMethod fun setDateCustomUserAttribute(key: String?, value: Double, callback: Callback?) { brazeImpl.setDateCustomUserAttribute(key, value.toInt(), callback) } + @ReactMethod + fun setCustomUserAttributeObject(key: String?, value: ReadableMap, merge: Boolean, callback: Callback?) { + brazeImpl.setCustomUserAttributeObject(key, value, merge, callback) + } + @ReactMethod fun addToCustomUserAttributeArray(key: String?, value: String?, callback: Callback?) { brazeImpl.addToCustomAttributeArray(key, value, callback) @@ -248,6 +262,11 @@ class BrazeReactBridge(reactContext: ReactApplicationContext?) : ReactContextBas brazeImpl.getContentCards(promise) } + @ReactMethod + fun getCachedContentCards(promise: Promise?) { + brazeImpl.getCachedContentCards(promise) + } + @ReactMethod fun getCardCountForCategories(category: String?, callback: Callback?) { brazeImpl.getCardCountForCategories(category, callback) @@ -303,6 +322,17 @@ class BrazeReactBridge(reactContext: ReactApplicationContext?) : ReactContextBas brazeImpl.setLocationCustomAttribute(key, latitude, longitude, callback) } + @ReactMethod + fun setLastKnownLocation( + latitude: Double, + longitude: Double, + altitude: Double?, + horizontalAccuracy: Double?, + verticalAccuracy: Double? + ) { + brazeImpl.setLastKnownLocation(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy) + } + @ReactMethod fun subscribeToInAppMessage(useBrazeUI: Boolean, callback: Callback?) { brazeImpl.subscribeToInAppMessage(useBrazeUI) @@ -363,6 +393,11 @@ class BrazeReactBridge(reactContext: ReactApplicationContext?) : ReactContextBas brazeImpl.refreshFeatureFlags() } + @ReactMethod + fun logFeatureFlagImpression(id: String) { + brazeImpl.logFeatureFlagImpression(id) + } + @ReactMethod fun addListener(eventType: String?) { eventType?.let { diff --git a/braze-react-native-sdk.podspec b/braze-react-native-sdk.podspec index 2f34e479..9a706ab6 100644 --- a/braze-react-native-sdk.podspec +++ b/braze-react-native-sdk.podspec @@ -20,9 +20,9 @@ Pod::Spec.new do |s| s.preserve_paths = 'LICENSE.md', 'README.md', 'package.json', 'index.js', 'iOS/replace-at-import-statements.sh' s.source_files = 'iOS/**/*.{h,m,mm,swift}' - s.dependency 'BrazeKit', '~> 6.4.0' - s.dependency 'BrazeLocation', '~> 6.4.0' - s.dependency 'BrazeUI', '~> 6.4.0' + s.dependency 'BrazeKit', '~> 6.6.0' + s.dependency 'BrazeLocation', '~> 6.6.0' + s.dependency 'BrazeUI', '~> 6.6.0' s.dependency 'React-Core' # Swift/Objective-C compatibility diff --git a/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm b/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm index d7a3cbe0..3a9d9a1f 100644 --- a/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm +++ b/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm @@ -166,8 +166,10 @@ - (void)reportResultWithCallback:(RCTResponseSenderBlock)callback andError:(NSSt [braze.user addAlias:aliasName label:aliasLabel]; } -RCT_EXPORT_METHOD(registerAndroidPushToken:(NSString *)token) { - RCTLogInfo(@"Warning: This is an Android only feature."); +RCT_EXPORT_METHOD(registerPushToken:(NSString *)token) { + RCTLogInfo(@"braze registerPushToken with token %@", token); + NSData* tokenData = [token dataUsingEncoding:NSUTF8StringEncoding]; + [braze.notifications registerDeviceToken:tokenData]; } RCT_EXPORT_METHOD(setGoogleAdvertisingId:(NSString *)googleAdvertisingId adTrackingEnabled:(BOOL)adTrackingEnabled) { @@ -408,14 +410,51 @@ - (NSArray *)parseArray:(NSArray *)array { andResult:@(YES)]; } +RCT_EXPORT_METHOD(setCustomUserAttributeObject:(NSString *)key + value:(NSDictionary *)value + merge:(BOOL)merge + callback:(RCTResponseSenderBlock)callback) { + RCTLogInfo(@"braze.user setCustomUserAttributeObject:object:: = %@", key); + [braze.user setNestedCustomAttributeDictionaryWithKey:key value:value merge:merge]; + [self reportResultWithCallback:callback + andError:nil + andResult:@(YES)]; +} + RCT_EXPORT_METHOD(setCustomUserAttributeArray:(NSString *)key value:(NSArray *)value callback:(RCTResponseSenderBlock)callback) { - RCTLogInfo(@"braze.user setCustomAttributeArrayWithKey:array:: = %@", key); + RCTLogInfo(@"braze.user setCustomUserAttributeArray:array:: = %@", key); + for (id item in value) { + if (![item isKindOfClass:[NSString class]]) { + RCTLogInfo(@"Custom attribute array contains element that is not of type string. Aborting."); + [self reportResultWithCallback:callback + andError:nil + andResult:@(NO)]; + return; + } + } [braze.user setCustomAttributeArrayWithKey:key array:value]; [self reportResultWithCallback:callback andError:nil andResult:@(YES)]; } +RCT_EXPORT_METHOD(setCustomUserAttributeObjectArray:(NSString *)key value:(NSArray *)value callback:(RCTResponseSenderBlock)callback) { + RCTLogInfo(@"braze.user setCustomUserAttributeObjectArray:array:: = %@", key); + for (id item in value) { + if (![item isKindOfClass:[NSDictionary class]]) { + RCTLogInfo(@"Custom attribute array contains element that is not of type object. Aborting."); + [self reportResultWithCallback:callback + andError:nil + andResult:@(NO)]; + return; + } + } + [braze.user setNestedCustomAttributeArrayWithKey:key value:value]; + [self reportResultWithCallback:callback + andError:nil + andResult:@(YES)]; +} + RCT_EXPORT_METHOD(unsetCustomUserAttribute:(NSString *)key callback:(RCTResponseSenderBlock)callback) { RCTLogInfo(@"braze.user unsetCustomUserAttribute: = %@", key); [braze.user unsetCustomAttributeWithKey:key]; @@ -458,6 +497,21 @@ - (NSArray *)parseArray:(NSArray *)array { [braze.user setAttributionData:attributionData]; } +RCT_EXPORT_METHOD(setLastKnownLocation:(double)latitude + longitude:(double)longitude + altitude:(NSNumber *)altitude + horizontalAccuracy:(NSNumber *)horizontalAccuracy + verticalAccuracy:(NSNumber *)verticalAccuracy) { + RCTLogInfo(@"setLastKnownLocationWithLatitude called with latitude: %g, longitude: %g, horizontalAccuracy: %g, altitude: %g, and verticalAccuracy: %g", latitude, longitude, horizontalAccuracy.doubleValue, altitude.doubleValue, verticalAccuracy.doubleValue); + if (!horizontalAccuracy) { + RCTLogInfo(@"Horizontal accuracy is nil. This is a no-op in iOS."); + } else if (!altitude && !verticalAccuracy) { + [braze.user setLastKnownLocationWithLatitude:latitude longitude:longitude horizontalAccuracy:horizontalAccuracy.doubleValue]; + } else { + [braze.user setLastKnownLocationWithLatitude:latitude longitude:longitude altitude:altitude.doubleValue horizontalAccuracy:horizontalAccuracy.doubleValue verticalAccuracy:verticalAccuracy.doubleValue]; + } +} + #pragma mark - News Feed RCT_EXPORT_METHOD(launchNewsFeed) { @@ -582,6 +636,11 @@ - (nullable BRZContentCardRaw *)getContentCardById:(NSString *)idString { }]; } +RCT_EXPORT_METHOD(getCachedContentCards:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + RCTLogInfo(@"getCachedContentCards called"); + resolve(RCTFormatContentCards([braze.contentCards cards])); +} + RCT_EXPORT_METHOD(logContentCardClicked:(NSString *)idString) { BRZContentCardRaw *cardToClick = [self getContentCardById:idString]; if (cardToClick) { @@ -828,6 +887,11 @@ - (BRZInAppMessageRaw *)getInAppMessageFromString:(NSString *)inAppMessageJSONSt [braze.featureFlags requestRefresh]; } +RCT_EXPORT_METHOD(logFeatureFlagImpression:(NSString *)flagId) { + RCTLogInfo(@"logFeatureFlagImpression called for ID %@", flagId); + [braze.featureFlags logFeatureFlagImpressionWithId:flagId]; +} + RCT_EXPORT_METHOD(getFeatureFlag:(NSString *)flagId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { diff --git a/iOS/replace-at-import-statements.sh b/iOS/replace-at-import-statements.sh index 9967cba9..5915cee7 100644 --- a/iOS/replace-at-import-statements.sh +++ b/iOS/replace-at-import-statements.sh @@ -27,7 +27,8 @@ for header in "${headers[@]}"; do # BrazeKit if [[ $header == *"BrazeKit"* ]]; then # Add the required imports - perl -0777 -i -pe 's/^\@class NSString;/#import <Foundation\/Foundation.h>\n#import <UIKit\/UIKit.h>\n#import <WebKit\/WebKit.h>\n\n\@class NSString;/gm' "$header" + perl -0777 -i -pe 's/^\@class NSString;/#import <Foundation\/Foundation.h>\n#import <UIKit\/UIKit.h>\n#import <WebKit\/WebKit.h>\n\n\@class NSString;\n#import <UserNotifications\/UserNotifications.h> +/gm' "$header" fi done diff --git a/package.json b/package.json index 31fd8955..379942df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@braze/react-native-sdk", - "version": "6.0.2", + "version": "7.0.0", "description": "Braze SDK for React Native.", "main": "src/index.js", "types": "src/index.d.ts", diff --git a/src/NativeBrazeReactModule.ts b/src/NativeBrazeReactModule.ts index 7bf99962..04efff24 100644 --- a/src/NativeBrazeReactModule.ts +++ b/src/NativeBrazeReactModule.ts @@ -24,7 +24,7 @@ export interface Spec extends TurboModule { month: number, day: number ): void; - registerAndroidPushToken(token: string): void; + registerPushToken(token: string): void; setGoogleAdvertisingId( googleAdvertisingId: string, adTrackingEnabled: boolean @@ -76,11 +76,29 @@ export interface Spec extends TurboModule { value: string, callback?: ((error?: Object, result?: boolean) => void) | null ): void; + + // Sets a dictionary object as a custom user attribute. + // `merge` indicates whether to override the existing value (false) or combine its fields (true). + setCustomUserAttributeObject( + key: string, + value: Object, + merge: boolean, + callback?: ((error?: Object, result?: boolean) => void) | null + ): void; + + // Sets an array of strings as a custom user attribute. setCustomUserAttributeArray( key: string, value: string[], callback?: ((error?: Object, result?: boolean) => void) | null ): void; + + // Sets an array of objects as a custom user attribute. + setCustomUserAttributeObjectArray( + key: string, + value: object[], + callback?: ((error?: Object, result?: boolean) => void) | null + ): void; setDateCustomUserAttribute( key: string, value: number, @@ -121,6 +139,7 @@ export interface Spec extends TurboModule { logContentCardDismissed(cardId: string): void; logContentCardImpression(cardId: string): void; getContentCards(): Promise<ContentCard[]>; + getCachedContentCards(): Promise<ContentCard[]>; getCardCountForCategories( category: string, callback: ((error?: Object, result?: number) => void) | null @@ -136,6 +155,13 @@ export interface Spec extends TurboModule { enableSDK(): void; requestLocationInitialization(): void; requestGeofences(latitude: number, longitude: number): void; + setLastKnownLocation( + latitude: number, + longitude: number, + altitude: number | null, + horizontalAccuracy: number | null, + verticalAccuracy: number | null + ): void; setLocationCustomAttribute( key: string, latitude: number, @@ -164,6 +190,7 @@ export interface Spec extends TurboModule { getFeatureFlagStringProperty(flagId: string, key: string): Promise<string | null>; getFeatureFlagNumberProperty(flagId: string, key: string): Promise<number | null>; refreshFeatureFlags(): void; + logFeatureFlagImpression(flagId: string): void; // NativeEventEmitter methods for the New Architecture. // The implementations are handled implicitly by React Native. diff --git a/src/braze.js b/src/braze.js index 11a4da0e..3fa89fb6 100644 --- a/src/braze.js +++ b/src/braze.js @@ -46,7 +46,7 @@ export class Braze { * @param {function(string)} callback - A callback that retuns the deep link as a string. If there is no deep link, returns null. */ static getInitialURL(callback) { - if (this.bridge.getInitialURL) { + if (Platform.OS === 'ios') { this.bridge.getInitialURL((err, res) => { if (err) { console.log(err); @@ -159,14 +159,18 @@ export class Braze { } /** - * This method posts a token to Braze's servers to associate the token with the current device. - * - * No-op on iOS. - * - * @param {string} token - The device's push token. + * @deprecated This method is deprecated in favor of `registerPushToken`. */ static registerAndroidPushToken(token) { - this.bridge.registerAndroidPushToken(token); + this.bridge.registerPushToken(token); + } + + /** + * This method posts a token to Braze's servers to associate the token with the current device. + * @param {string} token - The device's push token. + */ + static registerPushToken(token) { + this.bridge.registerPushToken(token); } /** @@ -252,7 +256,10 @@ export class Braze { * Passing a null value will remove this custom attribute from the user. * @param {function(error, result)} callback - A callback that receives the function call result. */ - static setCustomUserAttribute(key, value, callback) { + static setCustomUserAttribute(key, value, thirdParam, fourthParam) { + const merge = typeof thirdParam === 'boolean' ? thirdParam : false; + const callback = typeof thirdParam === 'boolean' ? fourthParam : thirdParam; + var valueType = typeof value; if (value instanceof Date) { callFunctionWithCallback( @@ -261,9 +268,25 @@ export class Braze { callback ); } else if (value instanceof Array) { + if (value.every(item => typeof item === 'string')) { + callFunctionWithCallback( + this.bridge.setCustomUserAttributeArray, + [key, value], + callback + ); + } else if (value.every(item => typeof item === 'object')) { + callFunctionWithCallback( + this.bridge.setCustomUserAttributeObjectArray, + [key, value], + callback + ); + } else { + console.log(`User attribute ${value} was not a valid array. Custom attribute arrays can only contain all strings or all objects.`); + } + } else if (valueType === 'object') { callFunctionWithCallback( - this.bridge.setCustomUserAttributeArray, - [key, value], + this.bridge.setCustomUserAttributeObject, + [key, value, merge], callback ); } else if (valueType === 'boolean') { @@ -565,6 +588,14 @@ export class Braze { return this.bridge.getContentCards(); } + /** + * Returns the most recent Content Cards array from the cache. + * @returns {Promise<ContentCard[]>} + */ + static getCachedContentCards() { + return this.bridge.getCachedContentCards(); + } + /** * Manually log a click to Braze for a particular card. * The SDK will only log a card click when the card has the url property with a valid value. @@ -694,6 +725,20 @@ export class Braze { ); } + /** + * Sets the last known location for the user. For Android, latitude and longitude are required, with altitude and horizontal accuracy being optional parameters, and vertical accuracy being a no-op. + * For iOS, latitude, longitude, and horizontal accuracy are required, with altitude and vertical accuracy being optional parameters. + * Calling this method with invalid parameters for a specific platform is a no-op. Latitude, longitude, and horizontal accuracy are the minimum required parameters to work for all platforms. + * @param {number} latitude - Location latitude. May not be null. + * @param {number} longitude - Location longitude. May not be null. + * @param {number} altitude - Location altitude. May be null for both platforms. + * @param {number} horizontalAccuracy - Location horizontal accuracy. Equivalent to accuracy for Android. May be null for Android only; may not be null for iOS. + * @param {number} verticalAccuracy - Location vertical accuracy. May be null for iOS. No-op for Android. + */ + static setLastKnownLocation(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy) { + this.bridge.setLastKnownLocation(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); + } + // Refresh Content Cards /** * Requests a refresh of the content cards from Braze's servers. @@ -799,6 +844,13 @@ export class Braze { this.bridge.refreshFeatureFlags(); } + /** + * Logs an impression for the Feature Flag with the provided ID. + */ + static logFeatureFlagImpression(id) { + this.bridge.logFeatureFlagImpression(id); + } + /** * Returns the boolean property for the given feature flag ID. * @returns {Promise<boolean|null>} diff --git a/src/index.d.ts b/src/index.d.ts index a3fb8804..e2dba2a0 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -154,14 +154,16 @@ export function setDateOfBirth( day: number ): void; +/** + * @deprecated This method is deprecated in favor of `registerPushToken`. + */ +export function registerAndroidPushToken(token: string): void; + /** * This method posts a token to Braze's servers to associate the token with the current device. - * - * No-op on iOS. - * * @param {string} token - The device's push token. */ -export function registerAndroidPushToken(token: string): void; +export function registerPushToken(token: string): void; /** * This method sets the Google Advertising ID and associated ad-tracking enabled field for this device. Note that the @@ -261,14 +263,34 @@ export function logPurchase( * user. * @param {string} key - The identifier of the custom attribute. Limited to 255 characters in length, cannot begin with * a $, and can only contain alphanumeric characters and punctuation. - * @param value - Can be numeric, boolean, a Date object, a string, or an array of strings. Strings are limited to - * 255 characters in length, cannot begin with a $, and can only contain alphanumeric characters and punctuation. + * @param value - Can be numeric, boolean, a Date object, a string, an array of strings, an object, or an array of objects. + * Strings are limited to 255 characters in length, cannot begin with a $, and can only contain alphanumeric + * characters and punctuation. + * Passing a null value will remove this custom attribute from the user. + * @param {function(error, result)} callback - A callback that receives the function call result. + */ +export function setCustomUserAttribute( + key: string, + value: number | boolean | string | string[] | Date | null | object | object[], + callback?: Callback<boolean> +): void; + +/** + * Sets a custom user attribute. This can be any key/value pair and is used to collect extra information about the + * user. + * @param {string} key - The identifier of the custom attribute. Limited to 255 characters in length, cannot begin with + * a $, and can only contain alphanumeric characters and punctuation. + * @param value - Can be numeric, boolean, a Date object, a string, an array of strings, an object, or an array of objects. + * Strings are limited to 255 characters in length, cannot begin with a $, and can only contain alphanumeric + * characters and punctuation. * Passing a null value will remove this custom attribute from the user. + * @param merge - If the value is object, this boolean indicates if the value should be merged into the existing key. * @param {function(error, result)} callback - A callback that receives the function call result. */ export function setCustomUserAttribute( key: string, - value: number | boolean | string | string[] | Date | null, + value: number | boolean | string | string[] | Date | null | object | object[], + merge: boolean, callback?: Callback<boolean> ): void; @@ -480,6 +502,12 @@ export function logContentCardImpression(id: string): void; */ export function getContentCards(): Promise<ContentCard[]>; +/** + * Returns the most recent Content Cards array from the cache. + * @returns {Promise<ContentCard[]>} + */ +export function getCachedContentCards(): Promise<ContentCard[]>; + /** * @deprecated This method is a no-op on iOS. */ @@ -553,6 +581,24 @@ export function setLocationCustomAttribute( callback?: Callback<undefined> ): void; +/** + * Sets the last known location for the user. For Android, latitude and longitude are required, with altitude and horizontal accuracy being optional parameters, and vertical accuracy being a no-op. + * For iOS, latitude, longitude, and horizontal accuracy are required, with altitude and vertical accuracy being optional parameters. + * Calling this method with invalid parameters for a specific platform is a no-op. Latitude, longitude, and horizontal accuracy are the minimum required parameters to work for all platforms. + * @param {number} latitude - Location latitude. Required. + * @param {number} longitude - Location longitude. Required. + * @param {number} altitude - Location altitude. May be null for both platforms. + * @param {number} horizontalAccuracy - Location horizontal accuracy. Equivalent to accuracy for Android. May be null for Android only; required for iOS. + * @param {number} verticalAccuracy - Location vertical accuracy. May be null for iOS. No-op for Android. + */ + export function setLastKnownLocation( + latitude: number, + longitude: number, + altitude?: number | null, + horizontalAccuracy?: number | null, + verticalAccuracy?: number | null +): void; + /** * Call this method to have the SDK publish an "inAppMessageReceived" event containing the in-app message data to the * Javascript layer. You can listen to this event with `Braze.addListener()`. @@ -692,6 +738,13 @@ export function getFeatureFlagNumberProperty(id: string, key: string): Promise<n */ export function refreshFeatureFlags(): void; +/** + * Logs an impression for the Feature Flag with the provided ID. + * + * @param id - The ID of the feature flag. + */ +export function logFeatureFlagImpression(id: string): void; + export class BrazeInAppMessage { constructor(_data: string); inAppMessageJsonString: string;