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;