diff --git a/BrazeProject/.env b/BrazeProject/.env new file mode 100644 index 0000000..38149c5 --- /dev/null +++ b/BrazeProject/.env @@ -0,0 +1,7 @@ +BRAZE_API_KEY_ANDROID_ROI= +BRAZE_API_KEY_ANDROID_UK= +BRAZE_API_KEY_IOS_ROI= +BRAZE_API_KEY_IOS_UK= +BRAZE_ENDPOINT_ROI= +BRAZE_ENDPOINT_UK= +BRAZE_FIREBASE_CLOUD_MESSAGING_SENDER_ID_KEY_ANDROID= diff --git a/BrazeProject/BrazeProject.tsx b/BrazeProject/BrazeProject.tsx index 09371e1..4569cdb 100644 --- a/BrazeProject/BrazeProject.tsx +++ b/BrazeProject/BrazeProject.tsx @@ -15,6 +15,9 @@ import { import RadioGroup from 'react-native-radio-buttons-group'; import Braze from '@braze/react-native-sdk'; +import { useConfigLoader } from './hooks/useConfigLoader'; +import { useCountrySelector } from './hooks/useCountrySelector'; + // Change to `true` to automatically log clicks, button clicks, // and impressions for in-app messages and content cards. const automaticallyInteract = false; @@ -807,6 +810,25 @@ export const BrazeProject = (): ReactElement => { Braze.registerPushToken(pushToken); }; + const [isLoading, setLoading] = useState(true); + const { loadConfig } = useConfigLoader(); + const { + onSelectROIPress, + onSelectUKPress + } = useCountrySelector(); + + useEffect(() => { + setLoading(true); + + loadConfig().then(() => { + setLoading(false) + }) + }, []); + + if (isLoading) { + return null; + } + return ( { {/* Location */} + + + + Select ROI + + + + Select UK + + {Platform.OS === 'android' ? ( diff --git a/BrazeProject/android/app/build.gradle b/BrazeProject/android/app/build.gradle index 2504753..5ec02b0 100644 --- a/BrazeProject/android/app/build.gradle +++ b/BrazeProject/android/app/build.gradle @@ -131,6 +131,8 @@ android { dependencies { implementation "com.google.firebase:firebase-messaging:22.0.0" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3' + // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") diff --git a/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.kt b/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.kt index 3711ae8..3ca5a7e 100644 --- a/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.kt +++ b/BrazeProject/android/app/src/main/java/com/brazeproject/MainApplication.kt @@ -14,13 +14,16 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.soloader.SoLoader +import com.brazeproject.braze.BrazeDynamicConfiguration +import com.brazeproject.brazeDynamicConfigurationBridge.BrazeDynamicConfigurationPackage + class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { override fun getPackages(): List = PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: - // add(MyReactNativePackage()) + add(BrazeDynamicConfigurationPackage()) } override fun getJSMainModuleName(): String = "index" @@ -43,5 +46,7 @@ class MainApplication : Application(), ReactApplication { // If you opted-in for the New Architecture, we load the native entry point for this app. load() } + + BrazeDynamicConfiguration(this) } } diff --git a/BrazeProject/android/app/src/main/java/com/brazeproject/braze/BrazeDynamicConfiguration.kt b/BrazeProject/android/app/src/main/java/com/brazeproject/braze/BrazeDynamicConfiguration.kt new file mode 100644 index 0000000..99c443a --- /dev/null +++ b/BrazeProject/android/app/src/main/java/com/brazeproject/braze/BrazeDynamicConfiguration.kt @@ -0,0 +1,186 @@ +package com.brazeproject.braze + +import android.app.Application +import android.content.Context +import com.braze.Braze +import com.braze.BrazeActivityLifecycleCallbackListener +import com.braze.configuration.BrazeConfig +import com.braze.configuration.BrazeConfigurationProvider +import com.braze.support.BrazeLogger +import com.facebook.react.bridge.ReadableMap +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.tasks.await +import org.json.JSONObject + +data class ConfigData( + private val _apiKey: String?, + private val _endpoint: String?, + private val _logLevel: Int?, + private val _firebaseCloudMessagingSenderIdKey: String? +) { + var apiKey: String + var endpoint: String + var logLevel: Int + var firebaseCloudMessagingSenderIdKey: String + + init { + if ( + _apiKey == null || + _endpoint == null || + _logLevel == null || + _firebaseCloudMessagingSenderIdKey == null + ) { + throw IllegalArgumentException( + "Given config attributes are invalid" + ) + } + + this.apiKey = _apiKey + this.endpoint = _endpoint + this.logLevel = _logLevel + this.firebaseCloudMessagingSenderIdKey = _firebaseCloudMessagingSenderIdKey + } + + companion object { + fun fromJsonString(jsonString: String): ConfigData { + return with(JSONObject(jsonString)) { + ConfigData( + getString("apiKey"), + getString("endpoint"), + getInt("logLevel"), + getString("firebaseCloudMessagingSenderIdKey") + ) + } + } + } + + fun toJSONString(): String { + return JSONObject().apply { + put("apiKey", apiKey) + put("endpoint", endpoint) + put("logLevel", logLevel) + put("firebaseCloudMessagingSenderIdKey", firebaseCloudMessagingSenderIdKey) + }.toString() + } +} + +const val SAVED_CONFIG_KEY = "braze_saved_config" + +class BrazeDynamicConfiguration(private val application: Application) { + private val sharedPref = application.getSharedPreferences("BrazeDynamicConfiguration", Context.MODE_PRIVATE) + + companion object { + var sharedInstance: BrazeDynamicConfiguration? = null + } + + init { + sharedInstance = this; + } + + private val activityLifecycleCallbackListener = BrazeActivityLifecycleCallbackListener() + + @Throws + fun saveConfig(map: ReadableMap) { + if (sharedPref == null) { + throw IllegalAccessException( + "Trying to save into nullish shared preferences" + ) + } + + val config = with(map) { + ConfigData( + getString("apiKey"), + getString("endpoint"), + getInt("logLevel"), + getString("firebaseCloudMessagingSenderIdKey") + ) + } + + with(sharedPref.edit()) { + putString(SAVED_CONFIG_KEY, config.toJSONString()) + apply() + } + } + + @Throws + fun getSavedConfig(): ConfigData? { + if (sharedPref == null) { + throw IllegalAccessException( + "Trying to read from nullish shared preferences" + ) + } + + val jsonString = sharedPref.getString(SAVED_CONFIG_KEY, null) + ?: return null + + return ConfigData.fromJsonString(jsonString) + } + + @Throws + suspend fun initializeWithSavedConfig() { + val savedConfig = getSavedConfig() + ?: throw IllegalAccessException( + "No saved config" + ) + + initialize(savedConfig) + } + + @Throws + private suspend fun initialize(config: ConfigData) { + val configurationProvider = BrazeConfigurationProvider(application); + + val configuredApiKey = Braze.getConfiguredApiKey(configurationProvider); + + // To avoid creating an instance with the same config as the existing one + if (config.apiKey == configuredApiKey) { + return; + } + + /* + If the api key is configured, then calling "initialize" means changing configurations + and therefore preparing for creating a new instance + */ + if (configuredApiKey != null) { + // Delete previous push token to avoid receiving push notifications originating from previous instance + FirebaseMessaging.getInstance().deleteToken().await() + + Braze.apply { + // Wipe all the data created by previous instance, to avoid unexpected behavior of the new instance + getInstance(application).registeredPushToken = null + wipeData(application) + } + + // Unregister previous callback listener to avoid callbacks execution duplications + application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbackListener) + } + + BrazeLogger.logLevel = config.logLevel + + val brazeConfig = BrazeConfig.Builder().apply { + setApiKey(config.apiKey) + setCustomEndpoint(config.endpoint) + setAutomaticGeofenceRequestsEnabled(false) + setIsLocationCollectionEnabled(true) + setTriggerActionMinimumTimeIntervalSeconds(1) + setHandlePushDeepLinksAutomatically(true) + }.build() + + application.registerActivityLifecycleCallbacks(activityLifecycleCallbackListener) + + Braze.apply { + configure(application, brazeConfig) + // Need to enable the new instance after wiping up the data + enableSdk(application) + + /* + Creating new push token: get token creates a new one if the previous one is deleted + */ + val newToken = FirebaseMessaging.getInstance().token.await() + + getInstance(application).apply { + registeredPushToken = newToken + } + } + } +} \ No newline at end of file diff --git a/BrazeProject/android/app/src/main/java/com/brazeproject/brazeDynamicConfigurationBridge/BrazeDynamicConfigurationBridge.kt b/BrazeProject/android/app/src/main/java/com/brazeproject/brazeDynamicConfigurationBridge/BrazeDynamicConfigurationBridge.kt new file mode 100644 index 0000000..f4585b8 --- /dev/null +++ b/BrazeProject/android/app/src/main/java/com/brazeproject/brazeDynamicConfigurationBridge/BrazeDynamicConfigurationBridge.kt @@ -0,0 +1,48 @@ +package com.brazeproject.brazeDynamicConfigurationBridge + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.brazeproject.braze.BrazeDynamicConfiguration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class BrazeDynamicConfigurationBridge internal constructor(context: ReactApplicationContext) : + ReactContextBaseJavaModule(context) { + override fun getName(): String { + return "BrazeDynamicConfigurationBridge" + } + + @ReactMethod + fun saveConfig(config: ReadableMap, promise: Promise) { + try { + val brazeDynamicConfiguration = BrazeDynamicConfiguration.sharedInstance + ?: throw IllegalStateException("No shared instance for BrazeDynamicConfiguration") + + brazeDynamicConfiguration.saveConfig(config) + + promise.resolve(null) + } catch (e: Exception) { + promise.reject("Error", e) + } + } + + @ReactMethod + fun initializeWithSavedConfig(promise: Promise) { + CoroutineScope(Dispatchers.IO).launch { + try { + val brazeDynamicConfiguration = BrazeDynamicConfiguration.sharedInstance + ?: throw IllegalStateException("No shared instance for BrazeDynamicConfiguration") + + brazeDynamicConfiguration.initializeWithSavedConfig() + + promise.resolve(null) + } catch (e: Exception) { + promise.reject("Error", e) + } + } + } +} \ No newline at end of file diff --git a/BrazeProject/android/app/src/main/java/com/brazeproject/brazeDynamicConfigurationBridge/BrazeDynamicConfigurationPackage.kt b/BrazeProject/android/app/src/main/java/com/brazeproject/brazeDynamicConfigurationBridge/BrazeDynamicConfigurationPackage.kt new file mode 100644 index 0000000..0ed8929 --- /dev/null +++ b/BrazeProject/android/app/src/main/java/com/brazeproject/brazeDynamicConfigurationBridge/BrazeDynamicConfigurationPackage.kt @@ -0,0 +1,20 @@ +package com.brazeproject.brazeDynamicConfigurationBridge + +import android.view.View +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ReactShadowNode +import com.facebook.react.uimanager.ViewManager + +class BrazeDynamicConfigurationPackage : ReactPackage { + override fun createViewManagers( + reactContext: ReactApplicationContext + ): MutableList>> = mutableListOf() + + override fun createNativeModules( + reactContext: ReactApplicationContext + ): MutableList = listOf( + BrazeDynamicConfigurationBridge(reactContext), + ).toMutableList() +} diff --git a/BrazeProject/android/app/src/main/res/values/braze.xml b/BrazeProject/android/app/src/main/res/values/braze.xml deleted file mode 100644 index e4d26d8..0000000 --- a/BrazeProject/android/app/src/main/res/values/braze.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - c04cb57f-8d5f-419a-8807-6f277102374b - STAGING - - true - 901477453852 - - 1 - - true - diff --git a/BrazeProject/babel.config.js b/BrazeProject/babel.config.js index f7b3da3..11ffc27 100644 --- a/BrazeProject/babel.config.js +++ b/BrazeProject/babel.config.js @@ -1,3 +1,13 @@ module.exports = { presets: ['module:@react-native/babel-preset'], + plugins: [ + ['module:react-native-dotenv', { + moduleName: 'react-native-dotenv', + path: '.env', + blocklist: null, + allowlist: null, + safe: true, + allowUndefined: true + }], + ] }; diff --git a/BrazeProject/global.d.ts b/BrazeProject/global.d.ts new file mode 100644 index 0000000..1d442a9 --- /dev/null +++ b/BrazeProject/global.d.ts @@ -0,0 +1,9 @@ +declare module 'react-native-dotenv' { + export const BRAZE_API_KEY_IOS_UK: string; + export const BRAZE_API_KEY_ANDROID_UK: string; + export const BRAZE_API_KEY_IOS_ROI: string; + export const BRAZE_API_KEY_ANDROID_ROI: string; + export const BRAZE_ENDPOINT_UK: string; + export const BRAZE_ENDPOINT_ROI: string; + export const BRAZE_FIREBASE_CLOUD_MESSAGING_SENDER_ID_KEY_ANDROID: string; +} diff --git a/BrazeProject/hooks/useConfigLoader.ts b/BrazeProject/hooks/useConfigLoader.ts new file mode 100644 index 0000000..4bd7bc4 --- /dev/null +++ b/BrazeProject/hooks/useConfigLoader.ts @@ -0,0 +1,30 @@ +import { PersistentStorageService } from '../services/PersistentStorageService'; +import { NotificationsService } from '../services/NotificationsService' +import { DeviceInfoService } from '../services/DeviceInfoService' + +export const useConfigLoader = () => { + const loadConfig = async () => { + let selectedCountry = await PersistentStorageService.getSafe<'UK' | 'ROI' | undefined>( + 'selected_country', + undefined, + ); + + if (!selectedCountry) { + selectedCountry = DeviceInfoService.getSelectedCountry(); + + // Save config only for the first launch + await NotificationsService.saveConfig({ + country: selectedCountry, + }); + + await PersistentStorageService.setSafe('selected_country', selectedCountry); + } + + await NotificationsService.initializeWithSavedConfig(); + // ...other initializations, e.g. accept language header + }; + + return { + loadConfig, + }; +}; diff --git a/BrazeProject/hooks/useCountrySelector.ts b/BrazeProject/hooks/useCountrySelector.ts new file mode 100644 index 0000000..311ca15 --- /dev/null +++ b/BrazeProject/hooks/useCountrySelector.ts @@ -0,0 +1,31 @@ +import { PersistentStorageService } from '../services/PersistentStorageService'; +import { NotificationsService } from '../services/NotificationsService'; +import { DeviceInfoService } from '../services/DeviceInfoService'; + +export const useCountrySelector = () => { + const selectLocation = async (nextSelectedCountry: 'UK' | 'ROI') => { + await PersistentStorageService.setSafe('selected_country', nextSelectedCountry); + await NotificationsService.saveConfig({ + country: nextSelectedCountry, + }); + + /* + Reload the app as this is the most convenient way to re-init all things. + Otherwise we need to put re-init logic directly here + */ + DeviceInfoService.restartBundle(); + }; + + const onSelectUKPress = () => { + selectLocation('UK'); + }; + + const onSelectROIPress = () => { + selectLocation('ROI'); + }; + + return { + onSelectUKPress, + onSelectROIPress, + }; +}; diff --git a/BrazeProject/ios/Braze/BrazeDynamicConfiguration.swift b/BrazeProject/ios/Braze/BrazeDynamicConfiguration.swift new file mode 100644 index 0000000..2156996 --- /dev/null +++ b/BrazeProject/ios/Braze/BrazeDynamicConfiguration.swift @@ -0,0 +1,162 @@ +import Foundation +import BrazeLocation +import braze_react_native_sdk + +class ApplicationBrazeDelegate: BrazeDelegate { + func braze(_ braze: Braze, shouldOpenURL context: Braze.URLContext) -> Bool { + if (!context.isUniversalLink) { + return true + } + + let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) + + userActivity.webpageURL = context.url + + let isHandled = UIApplication.shared.delegate?.application?(UIApplication.shared, continue: userActivity, restorationHandler: { _ in }) ?? false + + return !isHandled + } +} + +struct ConfigData: Codable { + var apiKey: String; + var endpoint: String; + var logLevel: UInt8; + + init(apiKey: String?, endpoint: String?, logLevel: UInt8?) throws { + guard + let apiKey = apiKey, + let endpoint = endpoint, + let logLevel = logLevel + else { + throw NSError( + domain: "BrazeDynamicConfiguration", + code: 0, + userInfo: [NSLocalizedDescriptionKey: "Given config attributes are invalid"] + ) + } + + self.apiKey = apiKey + self.endpoint = endpoint + self.logLevel = logLevel + } + + init(fromDictionary dictionary: NSDictionary) throws { + try self.init( + apiKey: dictionary["apiKey"] as? String, + endpoint: dictionary["endpoint"] as? String, + logLevel: dictionary["logLevel"] as? UInt8 + ) + } +} + +let SAVED_CONFIG_KEY = "braze_saved_config" + +@objc +public class BrazeDynamicConfiguration: NSObject { + @objc static var launchOptions: [AnyHashable : Any]? + + static var brazeInstance: Braze?; + + static func getSavedConfig() -> ConfigData? { + if let savedConfig = UserDefaults.standard.data(forKey: SAVED_CONFIG_KEY) { + do { + return try JSONDecoder().decode(ConfigData.self, from: savedConfig) + } catch { + return nil + } + } + + return nil + } + + static func saveConfig(fromDictionary dictionary: NSDictionary) throws { + let configData = try ConfigData(fromDictionary: dictionary) + let encodedConfig = try JSONEncoder().encode(configData) + + UserDefaults.standard.set(encodedConfig, forKey: SAVED_CONFIG_KEY) + } + + static func initializeWithSavedConfig() async throws { + if let configData = getSavedConfig() { + await initialize(withConfigData: configData) + } else { + throw NSError( + domain: "BrazeDynamicConfiguration", + code: 0, + userInfo: [NSLocalizedDescriptionKey: "No saved config"] + ) + } + } + + static func initialize(withConfigData configData: ConfigData) async { + // To avoid creating an instance with the same config as the existing one + if (configData.apiKey == brazeInstance?.configuration.api.key) { + return + } + + var delay = 0.0 + + /* + If the instance exists, then calling "initialize" means changing configurations + and therefore preparing for creating a new instance + */ + if (brazeInstance != nil) { + /* + To prevent receiving the same push notification more than once, + it's required to unregister the previous device push token + */ + await UIApplication.shared.unregisterForRemoteNotifications() + + // Wipe all the data created by previous instance, to avoid unexpected behavior of the new instance + brazeInstance?.wipeData() + + // Remove references to the previous instance, so GC can deallocate it + brazeInstance = nil + BrazeReactBridge.deinitBraze() + + delay = 1.0 + } + + let configuration = Braze.Configuration(apiKey: configData.apiKey, endpoint: configData.endpoint) + + configuration.logger.level = .init(rawValue: configData.logLevel) ?? .debug + configuration.push.automation = true + configuration.push.automation.requestAuthorizationAtLaunch = false + + configuration.forwardUniversalLinks = true + configuration.triggerMinimumTimeInterval = 5 + + configuration.location.brazeLocationProvider = BrazeLocationProvider() + configuration.location.automaticLocationCollection = true + configuration.location.geofencesEnabled = true + configuration.location.automaticGeofenceRequests = false + + await withCheckedContinuation { continuation in + /* + For some reason need to wait after de-initializing previous instance + and before creating a new instance. + Otherwise, push notifications for the new instance won't work + */ + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + + // A workaround for "Configuration" interfaces Swift<->Objective-C incompatibility + brazeInstance = BrazeInitWorkaround.initBraze(configuration) + + let delegate = ApplicationBrazeDelegate() + + brazeInstance?.delegate = delegate + // Need to enable the new instance after wiping up the data + brazeInstance?.enabled = true + + if let launchOptions = launchOptions { + BrazeReactUtils.sharedInstance().populateInitialUrl(fromLaunchOptions: launchOptions) + } + + launchOptions = nil + + continuation.resume() + } + } + } +} diff --git a/BrazeProject/ios/Braze/BrazeInitWorkaround.h b/BrazeProject/ios/Braze/BrazeInitWorkaround.h new file mode 100644 index 0000000..4ddd6b2 --- /dev/null +++ b/BrazeProject/ios/Braze/BrazeInitWorkaround.h @@ -0,0 +1,6 @@ +#import +#import + +@interface BrazeInitWorkaround: NSObject ++ (Braze *)initBraze:(NSObject *)configuration; +@end diff --git a/BrazeProject/ios/Braze/BrazeInitWorkaround.m b/BrazeProject/ios/Braze/BrazeInitWorkaround.m new file mode 100644 index 0000000..5c69120 --- /dev/null +++ b/BrazeProject/ios/Braze/BrazeInitWorkaround.m @@ -0,0 +1,10 @@ +#import "BrazeInitWorkaround.h" +#import "BrazeReactBridge.h" + +@implementation BrazeInitWorkaround + ++ (Braze *)initBraze:(BRZConfiguration *)configuration { + return [BrazeReactBridge initBraze:configuration]; +} + +@end diff --git a/BrazeProject/ios/BrazeDynamicConfigurationBridge/BrazeDynamicConfigurationBridge.m b/BrazeProject/ios/BrazeDynamicConfigurationBridge/BrazeDynamicConfigurationBridge.m new file mode 100644 index 0000000..eec78d6 --- /dev/null +++ b/BrazeProject/ios/BrazeDynamicConfigurationBridge/BrazeDynamicConfigurationBridge.m @@ -0,0 +1,7 @@ +#import +#import + +@interface RCT_EXTERN_REMAP_MODULE(BrazeDynamicConfigurationBridge, BrazeDynamicConfigurationBridge, NSObject) +RCT_EXTERN_METHOD(saveConfig: (NSDictionary)configOptions resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(initializeWithSavedConfig: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +@end diff --git a/BrazeProject/ios/BrazeDynamicConfigurationBridge/BrazeDynamicConfigurationBridge.swift b/BrazeProject/ios/BrazeDynamicConfigurationBridge/BrazeDynamicConfigurationBridge.swift new file mode 100644 index 0000000..6a9be2c --- /dev/null +++ b/BrazeProject/ios/BrazeDynamicConfigurationBridge/BrazeDynamicConfigurationBridge.swift @@ -0,0 +1,41 @@ +import Foundation + +@objc(BrazeDynamicConfigurationBridge) +public class BrazeDynamicConfigurationBridge: NSObject { + @objc static func requiresMainQueueSetup() -> Bool { + return false + } + + @objc func saveConfig( + _ configOptions: NSDictionary, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) { + do { + try BrazeDynamicConfiguration.saveConfig(fromDictionary: configOptions) + + resolve(nil) + } catch let error as NSError { + reject(error.domain, error.localizedDescription, error) + } catch let error { + reject("BrazeDynamicConfigurationBridge", "Saving failed", error) + } + } + + @objc func initializeWithSavedConfig( + _ resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) { + Task { + do { + try await BrazeDynamicConfiguration.initializeWithSavedConfig() + + resolve(nil) + } catch let error as NSError { + reject(error.domain, error.localizedDescription, error) + } catch let error { + reject("BrazeDynamicConfigurationBridge", "Saving failed", error) + } + } + } +} diff --git a/BrazeProject/ios/BrazeProject-Bridging-Header.h b/BrazeProject/ios/BrazeProject-Bridging-Header.h new file mode 100644 index 0000000..f623874 --- /dev/null +++ b/BrazeProject/ios/BrazeProject-Bridging-Header.h @@ -0,0 +1,2 @@ +#import "BrazeInitWorkaround.h" +#import diff --git a/BrazeProject/ios/BrazeProject.xcodeproj/project.pbxproj b/BrazeProject/ios/BrazeProject.xcodeproj/project.pbxproj index b50f162..efe2f95 100644 --- a/BrazeProject/ios/BrazeProject.xcodeproj/project.pbxproj +++ b/BrazeProject/ios/BrazeProject.xcodeproj/project.pbxproj @@ -13,8 +13,12 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 1C1F39C39078CBA3B1D4BA6B /* libPods-BrazeProjectRichPush.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 31F097B316BD83D92ECEDB44 /* libPods-BrazeProjectRichPush.a */; }; 566709436C351B31403D41A4 /* libPods-BrazeProject.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A1CCEF5F00EE40BE7DCA27D /* libPods-BrazeProject.a */; }; - 7FFA3C438F6ECE230980EE95 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + 7FFA3C438F6ECE230980EE95 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + C6B4EF1A2D074D46004DF732 /* BrazeDynamicConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B4EF162D074D46004DF732 /* BrazeDynamicConfiguration.swift */; }; + C6B4EF1B2D074D46004DF732 /* BrazeInitWorkaround.m in Sources */ = {isa = PBXBuildFile; fileRef = C6B4EF182D074D46004DF732 /* BrazeInitWorkaround.m */; }; + C6B4EF1E2D074E04004DF732 /* BrazeDynamicConfigurationBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = C6B4EF1D2D074E02004DF732 /* BrazeDynamicConfigurationBridge.m */; }; + C6B4EF202D074E14004DF732 /* BrazeDynamicConfigurationBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B4EF1F2D074E11004DF732 /* BrazeDynamicConfigurationBridge.swift */; }; D49243504D0CC62449E7A753 /* libPods-BrazeProject-BrazeProjectTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6966516151504FECB8343E39 /* libPods-BrazeProject-BrazeProjectTests.a */; }; D971B4B9DCDC97E7B7B30086 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0850B22E719905EB0709C451 /* PrivacyInfo.xcprivacy */; }; EC8E1B432C93543400F4A255 /* BrazeReactGIFHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = EC8E1B422C93543400F4A255 /* BrazeReactGIFHelper.m */; }; @@ -27,7 +31,6 @@ EC9B92102B1F762C00C6BD5D /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9B920F2B1F762C00C6BD5D /* NotificationViewController.swift */; }; EC9B92172B1F762C00C6BD5D /* BrazeProjectPushStory.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EC9B920A2B1F762B00C6BD5D /* BrazeProjectPushStory.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; EC9B921B2B1F76AC00C6BD5D /* BrazePushStory.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC9B91FC2B1F6A3B00C6BD5D /* BrazePushStory.xcframework */; }; - ECE697B02A3CC00B0060A95D /* BrazeReactDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = ECE697AF2A3CC00B0060A95D /* BrazeReactDelegate.m */; }; F2FEB37853C2F04522A0B900 /* libPods-BrazeProjectPushStory.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCCAA9B93114DFE83F9097D0 /* libPods-BrazeProjectPushStory.a */; }; /* End PBXBuildFile section */ @@ -92,6 +95,12 @@ A247AC1D6385A958A85E8432 /* Pods-BrazeProject.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BrazeProject.debug.xcconfig"; path = "Target Support Files/Pods-BrazeProject/Pods-BrazeProject.debug.xcconfig"; sourceTree = ""; }; A92FD10FA370AE4861599BA4 /* Pods-BrazeProjectPushStory.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BrazeProjectPushStory.release.xcconfig"; path = "Target Support Files/Pods-BrazeProjectPushStory/Pods-BrazeProjectPushStory.release.xcconfig"; sourceTree = ""; }; C0056B0412B5353A45812729 /* Pods-BrazeProjectRichPush.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BrazeProjectRichPush.debug.xcconfig"; path = "Target Support Files/Pods-BrazeProjectRichPush/Pods-BrazeProjectRichPush.debug.xcconfig"; sourceTree = ""; }; + C6B4EF0F2D074741004DF732 /* BrazeProject-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BrazeProject-Bridging-Header.h"; sourceTree = ""; }; + C6B4EF162D074D46004DF732 /* BrazeDynamicConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrazeDynamicConfiguration.swift; sourceTree = ""; }; + C6B4EF172D074D46004DF732 /* BrazeInitWorkaround.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BrazeInitWorkaround.h; sourceTree = ""; }; + C6B4EF182D074D46004DF732 /* BrazeInitWorkaround.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BrazeInitWorkaround.m; sourceTree = ""; }; + C6B4EF1D2D074E02004DF732 /* BrazeDynamicConfigurationBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BrazeDynamicConfigurationBridge.m; sourceTree = ""; }; + C6B4EF1F2D074E11004DF732 /* BrazeDynamicConfigurationBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrazeDynamicConfigurationBridge.swift; sourceTree = ""; }; CCCAA9B93114DFE83F9097D0 /* libPods-BrazeProjectPushStory.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-BrazeProjectPushStory.a"; sourceTree = BUILT_PRODUCTS_DIR; }; E5BE52AA7479C52FDF496B15 /* Pods-BrazeProjectRichPush.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BrazeProjectRichPush.release.xcconfig"; path = "Target Support Files/Pods-BrazeProjectRichPush/Pods-BrazeProjectRichPush.release.xcconfig"; sourceTree = ""; }; EC8E1B412C93542A00F4A255 /* BrazeReactGIFHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BrazeReactGIFHelper.h; sourceTree = ""; }; @@ -108,8 +117,6 @@ EC9B92142B1F762C00C6BD5D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EC9B921E2B1F790A00C6BD5D /* BrazeProjectPushStory.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BrazeProjectPushStory.entitlements; sourceTree = ""; }; ECE697AC2A3CBE680060A95D /* BrazeProject.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = BrazeProject.entitlements; path = BrazeProject/BrazeProject.entitlements; sourceTree = ""; }; - ECE697AD2A3CC0000060A95D /* BrazeReactDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BrazeReactDelegate.h; sourceTree = ""; }; - ECE697AF2A3CC00B0060A95D /* BrazeReactDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BrazeReactDelegate.m; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -118,7 +125,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7FFA3C438F6ECE230980EE95 /* BuildFile in Frameworks */, + 7FFA3C438F6ECE230980EE95 /* (null) in Frameworks */, D49243504D0CC62449E7A753 /* libPods-BrazeProject-BrazeProjectTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -175,16 +182,17 @@ 13B07FAE1A68108700A75B9A /* BrazeProject */ = { isa = PBXGroup; children = ( + C6B4EF1C2D074DF9004DF732 /* BrazeDynamicConfigurationBridge */, + C6B4EF192D074D46004DF732 /* Braze */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.mm */, ECE697AC2A3CBE680060A95D /* BrazeProject.entitlements */, - ECE697AD2A3CC0000060A95D /* BrazeReactDelegate.h */, - ECE697AF2A3CC00B0060A95D /* BrazeReactDelegate.m */, EC8E1B412C93542A00F4A255 /* BrazeReactGIFHelper.h */, EC8E1B422C93543400F4A255 /* BrazeReactGIFHelper.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, + C6B4EF0F2D074741004DF732 /* BrazeProject-Bridging-Header.h */, 13B07FB71A68108700A75B9A /* main.m */, 0850B22E719905EB0709C451 /* PrivacyInfo.xcprivacy */, ); @@ -257,6 +265,25 @@ path = Pods; sourceTree = ""; }; + C6B4EF192D074D46004DF732 /* Braze */ = { + isa = PBXGroup; + children = ( + C6B4EF162D074D46004DF732 /* BrazeDynamicConfiguration.swift */, + C6B4EF172D074D46004DF732 /* BrazeInitWorkaround.h */, + C6B4EF182D074D46004DF732 /* BrazeInitWorkaround.m */, + ); + path = Braze; + sourceTree = ""; + }; + C6B4EF1C2D074DF9004DF732 /* BrazeDynamicConfigurationBridge */ = { + isa = PBXGroup; + children = ( + C6B4EF1F2D074E11004DF732 /* BrazeDynamicConfigurationBridge.swift */, + C6B4EF1D2D074E02004DF732 /* BrazeDynamicConfigurationBridge.m */, + ); + path = BrazeDynamicConfigurationBridge; + sourceTree = ""; + }; EC9B91F12B1F698600C6BD5D /* BrazeProjectRichPush */ = { isa = PBXGroup; children = ( @@ -376,7 +403,7 @@ TestTargetID = 13B07F861A680F5B00A75B9A; }; 13B07F861A680F5B00A75B9A = { - LastSwiftMigration = 1120; + LastSwiftMigration = 1610; }; EC9B91EF2B1F698500C6BD5D = { CreatedOnToolsVersion = 15.0.1; @@ -665,9 +692,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C6B4EF1A2D074D46004DF732 /* BrazeDynamicConfiguration.swift in Sources */, + C6B4EF202D074E14004DF732 /* BrazeDynamicConfigurationBridge.swift in Sources */, + C6B4EF1B2D074D46004DF732 /* BrazeInitWorkaround.m in Sources */, + C6B4EF1E2D074E04004DF732 /* BrazeDynamicConfigurationBridge.m in Sources */, 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, - ECE697B02A3CC00B0060A95D /* BrazeReactDelegate.m in Sources */, EC8E1B432C93543400F4A255 /* BrazeReactGIFHelper.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -766,14 +796,15 @@ isa = XCBuildConfiguration; baseConfigurationReference = A247AC1D6385A958A85E8432 /* Pods-BrazeProject.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = BrazeProject/BrazeProject.entitlements; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; DEVELOPMENT_TEAM = 5GLZKGNWQ3; ENABLE_BITCODE = NO; INFOPLIST_FILE = BrazeProject/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -786,6 +817,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.braze.helloreact; PRODUCT_NAME = BrazeProject; + SWIFT_OBJC_BRIDGING_HEADER = "BrazeProject-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -796,13 +828,14 @@ isa = XCBuildConfiguration; baseConfigurationReference = 3C59986EA080ACA50322D9B0 /* Pods-BrazeProject.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = BrazeProject/BrazeProject.entitlements; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; DEVELOPMENT_TEAM = 5GLZKGNWQ3; INFOPLIST_FILE = BrazeProject/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -815,6 +848,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.braze.helloreact; PRODUCT_NAME = BrazeProject; + SWIFT_OBJC_BRIDGING_HEADER = "BrazeProject-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/BrazeProject/ios/BrazeProject/AppDelegate.mm b/BrazeProject/ios/BrazeProject/AppDelegate.mm index 8b6b402..740cd03 100644 --- a/BrazeProject/ios/BrazeProject/AppDelegate.mm +++ b/BrazeProject/ios/BrazeProject/AppDelegate.mm @@ -5,120 +5,20 @@ #import #import -#import "BrazeReactUtils.h" -#import "BrazeReactBridge.h" -#import "BrazeReactDelegate.h" - -#import -#import "BrazeReactGIFHelper.h" - -@interface AppDelegate () - -// Keep a strong reference to the BrazeDelegate to ensure it is not deallocated. -@property (nonatomic, strong) BrazeReactDelegate *brazeDelegate; - -@end +#import "BrazeProject-Swift.h" @implementation AppDelegate -static Braze *_braze; - -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"; self.initialProps = @{}; - - // Setup Braze - BRZConfiguration *configuration = [[BRZConfiguration alloc] initWithApiKey:apiKey endpoint:endpoint]; - configuration.triggerMinimumTimeInterval = 1; - configuration.logger.level = BRZLoggerLevelInfo; - configuration.push.appGroup = @"group.com.braze.helloreact.PushStories"; - - // 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]; - self.brazeDelegate = [[BrazeReactDelegate alloc] init]; - braze.delegate = self.brazeDelegate; - AppDelegate.braze = braze; - // Use SDWebImage as the GIF provider. - // GIFs are non-animating by default until overridden with a provider. - [BrazeReactGIFHelper setGIFProvider]; - - 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] populateInitialPayloadFromLaunchOptions:launchOptions]; + BrazeDynamicConfiguration.launchOptions = launchOptions; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } -#pragma mark - Push Notifications (manual integration) - -- (void)registerForPushNotifications { - UNUserNotificationCenter *center = UNUserNotificationCenter.currentNotificationCenter; - [center setNotificationCategories:BRZNotifications.categories]; - center.delegate = self; - [UIApplication.sharedApplication registerForRemoteNotifications]; - // Authorization is requested later in the JavaScript layer via `Braze.requestPushPermission`. -} - -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { - BOOL processedByBraze = AppDelegate.braze != nil && [AppDelegate.braze.notifications handleBackgroundNotificationWithUserInfo:userInfo - fetchCompletionHandler:completionHandler]; - if (processedByBraze) { - return; - } - - completionHandler(UIBackgroundFetchResultNoData); -} - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center - didReceiveNotificationResponse:(UNNotificationResponse *)response - withCompletionHandler:(void (^)(void))completionHandler { - [[BrazeReactUtils sharedInstance] populateInitialUrlForCategories:response.notification.request.content.userInfo]; - BOOL processedByBraze = AppDelegate.braze != nil && [AppDelegate.braze.notifications handleUserNotificationWithResponse:response - withCompletionHandler:completionHandler]; - if (processedByBraze) { - return; - } - - completionHandler(); -} - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center - willPresentNotification:(UNNotification *)notification - withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { - if (@available(iOS 14.0, *)) { - completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner); - } else { - completionHandler(UNNotificationPresentationOptionAlert); - } -} - -- (void)application:(UIApplication *)application - didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { - [AppDelegate.braze.notifications registerDeviceToken:deviceToken]; -} - #pragma mark - Linking // Deep Linking @@ -140,16 +40,6 @@ - (BOOL)application:(UIApplication *)application restorationHandler:restorationHandler]; } -#pragma mark - Helpers - -+ (Braze *)braze { - return _braze; -} - -+ (void)setBraze:(Braze *)braze { - _braze = braze; -} - #pragma mark - React Native methods /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. diff --git a/BrazeProject/ios/Podfile.lock b/BrazeProject/ios/Podfile.lock index ec14114..15bcfa2 100644 --- a/BrazeProject/ios/Podfile.lock +++ b/BrazeProject/ios/Podfile.lock @@ -1,9 +1,9 @@ PODS: - boost (1.84.0) - - braze-react-native-sdk (13.1.1): - - BrazeKit (~> 11.1.1) - - BrazeLocation (~> 11.1.1) - - BrazeUI (~> 11.1.1) + - braze-react-native-sdk (12.0.0): + - BrazeKit (~> 10.0.0) + - BrazeLocation (~> 10.0.0) + - BrazeUI (~> 10.0.0) - DoubleConversion - glog - hermes-engine @@ -24,13 +24,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - BrazeKit (11.1.1) - - BrazeLocation (11.1.1): - - BrazeKit (= 11.1.1) + - BrazeKit (10.0.0) + - BrazeLocation (10.0.0): + - BrazeKit (= 10.0.0) - BrazeNotificationService (11.1.1) - BrazePushStory (11.1.1) - - BrazeUI (11.1.1): - - BrazeKit (= 11.1.1) + - BrazeUI (10.0.0): + - BrazeKit (= 10.0.0) - DoubleConversion (1.1.6) - FBLazyVector (0.75.2) - fmt (9.1.0) @@ -1268,6 +1268,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-restart (0.0.27): + - React-Core - React-nativeconfig (0.75.2) - React-NativeModulesApple (0.75.2): - glog @@ -1528,6 +1530,8 @@ PODS: - React-logger (= 0.75.2) - React-perflogger (= 0.75.2) - React-utils (= 0.75.2) + - RNCAsyncStorage (2.1.0): + - React-Core - SDWebImage (5.19.7): - SDWebImage/Core (= 5.19.7) - SDWebImage/Core (5.19.7) @@ -1575,6 +1579,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-restart (from `../node_modules/react-native-restart`) - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -1601,6 +1606,7 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - SDWebImage - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -1688,6 +1694,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" React-microtasksnativemodule: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-restart: + :path: "../node_modules/react-native-restart" React-nativeconfig: :path: "../node_modules/react-native/ReactCommon" React-NativeModulesApple: @@ -1740,17 +1748,19 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: boost: 4cb898d0bf20404aab1850c656dcea009429d6c1 - braze-react-native-sdk: 3a9b9ba8502ed0d697441bcaa2c9d8db431a7b17 - BrazeKit: 879da791a0f4e247846a06c6de95f8b93f4579df - BrazeLocation: 59af48eafcf233a8ef16de3f6900955332113fe4 + braze-react-native-sdk: f8b26a6d5f35ead1477dbb00d97720411c31db60 + BrazeKit: e4b29ecd51a0eed8f30e9a365a867f16ffe28378 + BrazeLocation: f79747d5c09219fcdf64505f1e7f85a924783415 BrazeNotificationService: 4e082e73a7b2ed8aa1488a1f9d9177e1051ea4b2 BrazePushStory: eb83b837c290136c06d891d1d430acc02ca40ec1 - BrazeUI: 0785c09df3cc03aa6374ac3307215caef7e49bd3 + BrazeUI: 9d1aabd20f08f16592e131543de9eaf836d0d33e DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 FBLazyVector: 38bb611218305c3bc61803e287b8a81c6f63b619 fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 @@ -1766,16 +1776,16 @@ SPEC CHECKSUMS: React-CoreModules: f92a2cb11d22f6066823ca547c61e900325dfe44 React-cxxreact: f5595a4cbfe5a4e9d401dffa2c1c78bbbbbe75e4 React-debug: 4a91c177b5b2efcc546fb50bc2f676f3f589efab - React-defaultsnativemodule: 6b666572abf5fe7fe87836a42776abd6ad5ed173 - React-domnativemodule: 785d767c4edbb9f011b8c976271077759ca5c4aa + React-defaultsnativemodule: bb94c3db425b01c760f41a253de8536b3f5497f0 + React-domnativemodule: 6c581fd39812cafb024171e091c00905b2c3a3e2 React-Fabric: a33cc1fdc62a3085774783bb30970531589d2028 React-FabricComponents: 98de5f94cbd35d407f4fc78855298b562d8289cb React-FabricImage: 0ce8fd83844d9edef5825116d38f0e208b9ad786 React-featureflags: 37a78859ad71db758e2efdcbdb7384afefa8701e - React-featureflagsnativemodule: f94aacb52c463e200ee185bff90ae3b392e60263 + React-featureflagsnativemodule: 52b46e161a151b4653cf1762285e8e899d534e3f React-graphics: c16f1bab97a5d473831a79360d84300e93a614e5 React-hermes: 7801f8c0e12f326524b461dc368d3e74f3d2a385 - React-idlecallbacksnativemodule: d81bb7b5d26cea9852a8edc6ad1979cd7ed0841f + React-idlecallbacksnativemodule: 58de2ac968ee80947d19dc8fe20def607e5c2de8 React-ImageManager: 98a1e5b0b05528dde47ebcd953d916ac66d46c09 React-jserrorhandler: 08f1c3465a71a6549c27ad82809ce145ad52d4f1 React-jsi: 161428ab2c706d5fcd9878d260ff1513fdb356ab @@ -1784,16 +1794,17 @@ SPEC CHECKSUMS: React-jsitracing: 52b849a77d02e2dc262a3031454c23be8dabb4d9 React-logger: 8db32983d75dc2ad54f278f344ccb9b256e694fc React-Mapbuffer: 1c08607305558666fd16678b85ef135e455d5c96 - React-microtasksnativemodule: f13f03163b6a5ec66665dfe80a0df4468bb766a6 + React-microtasksnativemodule: 87b8de96f937faefece8afd2cb3a518321b2ef99 + react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 React-nativeconfig: 57781b79e11d5af7573e6f77cbf1143b71802a6d React-NativeModulesApple: 7ff2e2cfb2e5fa5bdedcecf28ce37e696c6ef1e1 React-perflogger: 8a360ccf603de6ddbe9ff8f54383146d26e6c936 React-performancetimeline: 3cfec915adcb3653a5a633b41e711903844c35d8 React-RCTActionSheet: 1c0e26a88eec41215089cf4436e38188cfe9f01a React-RCTAnimation: d87207841b1e2ae1389e684262ea8c73c887cb04 - React-RCTAppDelegate: 328e56399c4f1c3d20cfe547ea24ebded2b3a87f + React-RCTAppDelegate: 4ec7824c0cc9cc4b146ca8ee0fd81b10c316a440 React-RCTBlob: 79b42cb7db55f34079297687a480dbcf37f023f6 - React-RCTFabric: 27636a6a5fa5622159297fce26881945d3658cf6 + React-RCTFabric: 1dd1661db93716f8cb116e451bd9c211a8d15716 React-RCTImage: 0c10a75de59f7384a2a55545d5f36fe783e6ecda React-RCTLinking: bf08f4f655bf777af292b8d97449072c8bb196ca React-RCTNetwork: 1b690846b40fc5685af58e088720657db6814637 @@ -1811,10 +1822,11 @@ SPEC CHECKSUMS: React-utils: 81a715d9c0a2a49047e77a86f3a2247408540deb ReactCodegen: 60973d382704c793c605b9be0fc7f31cb279442f ReactCommon: 6ef348087d250257c44c0204461c03f036650e9b + RNCAsyncStorage: cc6479c4acd84cc7004946946c8afe30b018184d SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8 PODFILE CHECKSUM: d2de0a300a100d6e4a0f6663ec3607c40a5251fd -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3 diff --git a/BrazeProject/package.json b/BrazeProject/package.json index ede9fc6..b21e738 100644 --- a/BrazeProject/package.json +++ b/BrazeProject/package.json @@ -7,14 +7,18 @@ "ios": "npx react-native run-ios", "lint": "eslint .", "start": "npx react-native start", - "test": "jest" + "test": "jest", + "postinstall": "npx patch-package" }, "dependencies": { - "@braze/react-native-sdk": "../", + "@braze/react-native-sdk": "12.0.0", + "@react-native-async-storage/async-storage": "^2.1.0", "react": "18.3.1", "react-native": "^0.75.2", "react-native-codegen": "^0.71.5", - "react-native-radio-buttons-group": "^3.0.1" + "react-native-dotenv": "^3.4.11", + "react-native-radio-buttons-group": "^3.0.1", + "react-native-restart": "^0.0.27" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/BrazeProject/patches/@braze+react-native-sdk+12.0.0.patch b/BrazeProject/patches/@braze+react-native-sdk+12.0.0.patch new file mode 100644 index 0000000..80b0b19 --- /dev/null +++ b/BrazeProject/patches/@braze+react-native-sdk+12.0.0.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/@braze/react-native-sdk/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.h b/node_modules/@braze/react-native-sdk/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.h +index 5cd9088..24798ed 100644 +--- a/node_modules/@braze/react-native-sdk/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.h ++++ b/node_modules/@braze/react-native-sdk/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.h +@@ -16,4 +16,6 @@ + /// - configuration: The customizable configuration from the host app. + + (Braze *)initBraze:(BRZConfiguration *)configuration; + +++(void)deinitBraze; ++ + @end +diff --git a/node_modules/@braze/react-native-sdk/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm b/node_modules/@braze/react-native-sdk/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm +index 793b43b..43dea8a 100644 +--- a/node_modules/@braze/react-native-sdk/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm ++++ b/node_modules/@braze/react-native-sdk/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm +@@ -36,6 +36,10 @@ @implementation BrazeReactBridge { + + #pragma mark - Setup + +++(void)deinitBraze { ++ braze = nil; ++} ++ + + (Braze *)initBraze:(BRZConfiguration *)configuration { + [configuration.api addSDKMetadata:@[BRZSDKMetadata.reactnative]]; + #ifdef RCT_NEW_ARCH_ENABLED diff --git a/BrazeProject/services/DeviceInfoService/DeviceInfoService.ts b/BrazeProject/services/DeviceInfoService/DeviceInfoService.ts new file mode 100644 index 0000000..bf91444 --- /dev/null +++ b/BrazeProject/services/DeviceInfoService/DeviceInfoService.ts @@ -0,0 +1,12 @@ +import RNRestart from 'react-native-restart'; + +export namespace DeviceInfoService { + export const getSelectedCountry = (): 'UK' | 'ROI' => { + // Here should be a real call to system's APIs + return 'UK'; + }; + + export const restartBundle = () => { + RNRestart.restart(); + }; +} diff --git a/BrazeProject/services/DeviceInfoService/index.ts b/BrazeProject/services/DeviceInfoService/index.ts new file mode 100644 index 0000000..9764095 --- /dev/null +++ b/BrazeProject/services/DeviceInfoService/index.ts @@ -0,0 +1 @@ +export * from './DeviceInfoService'; diff --git a/BrazeProject/services/NotificationsService/Config.ts b/BrazeProject/services/NotificationsService/Config.ts new file mode 100644 index 0000000..35a8e08 --- /dev/null +++ b/BrazeProject/services/NotificationsService/Config.ts @@ -0,0 +1,60 @@ +import { Platform } from 'react-native'; +import { + BRAZE_FIREBASE_CLOUD_MESSAGING_SENDER_ID_KEY_ANDROID, + BRAZE_API_KEY_ANDROID_ROI, + BRAZE_API_KEY_ANDROID_UK, + BRAZE_API_KEY_IOS_ROI, + BRAZE_API_KEY_IOS_UK, + BRAZE_ENDPOINT_ROI, + BRAZE_ENDPOINT_UK, +} from 'react-native-dotenv'; + +export namespace Config { + interface ConfigData { + apiKey: string; + endpoint: string; + logLevel: number; + } + + interface ConfigDataAndroid extends ConfigData { + firebaseCloudMessagingSenderIdKey: string; + type: 'android'; + } + + interface ConfigDataIOS extends ConfigData { + // "type" attribute here is just to make TS happy + type: 'iOS'; + } + + export const UK = Platform.select({ + android: { + apiKey: BRAZE_API_KEY_ANDROID_UK, + endpoint: BRAZE_ENDPOINT_UK, + firebaseCloudMessagingSenderIdKey: BRAZE_FIREBASE_CLOUD_MESSAGING_SENDER_ID_KEY_ANDROID, + logLevel: 2, + type: 'android', + }, + ios: { + logLevel: 0, + endpoint: BRAZE_ENDPOINT_UK, + apiKey: BRAZE_API_KEY_IOS_UK, + type: 'iOS', + }, + })!; + + export const ROI = Platform.select({ + android: { + apiKey: BRAZE_API_KEY_ANDROID_ROI, + endpoint: BRAZE_ENDPOINT_ROI, + firebaseCloudMessagingSenderIdKey: BRAZE_FIREBASE_CLOUD_MESSAGING_SENDER_ID_KEY_ANDROID, + logLevel: 2, + type: 'android', + }, + ios: { + logLevel: 0, + endpoint: BRAZE_ENDPOINT_ROI, + apiKey: BRAZE_API_KEY_IOS_ROI, + type: 'iOS', + }, + })!; +} diff --git a/BrazeProject/services/NotificationsService/NotificationsService.ts b/BrazeProject/services/NotificationsService/NotificationsService.ts new file mode 100644 index 0000000..af75bc4 --- /dev/null +++ b/BrazeProject/services/NotificationsService/NotificationsService.ts @@ -0,0 +1,28 @@ +import { NativeModules } from 'react-native'; + +import { Config } from './Config'; + +const { BrazeDynamicConfigurationBridge } = NativeModules; + +export namespace NotificationsService { + interface SaveConfigParams { + country: 'ROI' | 'UK'; + } + + export async function saveConfig(params: SaveConfigParams) { + try { + await BrazeDynamicConfigurationBridge.saveConfig(Config[params.country]); + } catch (error) { + console.error(error); + } + } + + export async function initializeWithSavedConfig() { + try { + await BrazeDynamicConfigurationBridge.initializeWithSavedConfig(); + } catch (error) { + console.error(error); + } + } + +} diff --git a/BrazeProject/services/NotificationsService/index.ts b/BrazeProject/services/NotificationsService/index.ts new file mode 100644 index 0000000..76fe2d9 --- /dev/null +++ b/BrazeProject/services/NotificationsService/index.ts @@ -0,0 +1 @@ +export * from './NotificationsService'; diff --git a/BrazeProject/services/PersistentStorageService/PersistentStorageService.ts b/BrazeProject/services/PersistentStorageService/PersistentStorageService.ts new file mode 100644 index 0000000..14a899b --- /dev/null +++ b/BrazeProject/services/PersistentStorageService/PersistentStorageService.ts @@ -0,0 +1,45 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const parseValue = (value: any) => { + try { + return JSON.parse(value); + } catch { + return value; + } +}; + +const stringifyValue = (value: any): string => { + return typeof value === 'string' ? value : JSON.stringify(value); +}; + +export namespace PersistentStorageService { + export const getSafe = async(key: string, defaultValue: T): Promise => { + if (!key) { + return defaultValue; + } + + try { + const rawValue = await AsyncStorage.getItem(key); + + const parsedValue = parseValue(rawValue || 'null'); + + return parsedValue !== undefined && parsedValue !== null ? parsedValue : defaultValue; + } catch (error) { + console.error(error); + } + + return defaultValue; + }; + + export const setSafe = async (key: string, value: NonNullable) => { + if (!key) { + return; + } + + try { + await AsyncStorage.setItem(key, stringifyValue(value)); + } catch (error) { + console.error(error); + } + }; +} diff --git a/BrazeProject/services/PersistentStorageService/index.ts b/BrazeProject/services/PersistentStorageService/index.ts new file mode 100644 index 0000000..7e70a73 --- /dev/null +++ b/BrazeProject/services/PersistentStorageService/index.ts @@ -0,0 +1 @@ +export * from './PersistentStorageService'; diff --git a/BrazeProject/yarn.lock b/BrazeProject/yarn.lock index ade0b3b..1f22f27 100644 --- a/BrazeProject/yarn.lock +++ b/BrazeProject/yarn.lock @@ -1525,8 +1525,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@braze/react-native-sdk@../": - version "12.2.0" +"@braze/react-native-sdk@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@braze/react-native-sdk/-/react-native-sdk-12.0.0.tgz#38626a46e1d41fbb857fd6ecdda84eae322ff8f0" + integrity sha512-DVPgCwqZKxqDZM+iL0j0eyNUTg3171HTZUvYORkB55VLRcVb5qNfgzgPDpnKYJhuE+88QftOXJ/UrkWINUIh/w== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" @@ -1978,6 +1980,13 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@react-native-async-storage/async-storage@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-2.1.0.tgz#84ca82af320c16d3d8e617508ea523fe786b6781" + integrity sha512-eAGQGPTAuFNEoIQSB5j2Jh1zm5NPyBRTfjRMfCN0W1OakC5WIB5vsDyIQhUweKN9XOE2/V07lqTMGsL0dGXNkA== + dependencies: + merge-options "^3.0.4" + "@react-native-community/cli-clean@14.0.0": version "14.0.0" resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-14.0.0.tgz#37b53762e5f3d02f452a44fc32a7f88a7419ccad" @@ -3587,6 +3596,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dotenv@^16.4.5: + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -4739,6 +4753,11 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -5640,6 +5659,13 @@ memoize-one@^5.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -6533,11 +6559,23 @@ react-native-codegen@^0.71.5: jscodeshift "^0.13.1" nullthrows "^1.1.1" +react-native-dotenv@^3.4.11: + version "3.4.11" + resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz#2e6c4eabd55d5f1bf109b3dd9141dadf9c55cdd4" + integrity sha512-6vnIE+WHABSeHCaYP6l3O1BOEhWxKH6nHAdV7n/wKn/sciZ64zPPp2NUdEUf1m7g4uuzlLbjgr+6uDt89q2DOg== + dependencies: + dotenv "^16.4.5" + react-native-radio-buttons-group@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/react-native-radio-buttons-group/-/react-native-radio-buttons-group-3.0.2.tgz#9016a739819030cbf58a563b2487fac46a711f02" integrity sha512-5Nszk4WJGO4MeL+/4TSQRGoQErZlAaj5zAb4tis5f2J+O1tGzCMRtIARqr0WlIKwsjzH6zmcgyr9Q2Vv5xMiPw== +react-native-restart@^0.0.27: + version "0.0.27" + resolved "https://registry.yarnpkg.com/react-native-restart/-/react-native-restart-0.0.27.tgz#43aa8210312c9dfa5ec7bd4b2f35238ad7972b19" + integrity sha512-8KScVICrXwcTSJ1rjWkqVTHyEKQIttm5AIMGSK1QG1+RS5owYlE4z/1DykOTdWfVl9l16FIk0w9Xzk9ZO6jxlA== + react-native@^0.75.2: version "0.75.2" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.75.2.tgz#12d7e3e63c8ab93dcab7a6d4c4c9f4ad199141d4"