Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

poc: Dynamic config #281

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions BrazeProject/.env
Original file line number Diff line number Diff line change
@@ -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=
32 changes: 32 additions & 0 deletions BrazeProject/BrazeProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
Expand Down Expand Up @@ -1001,6 +1023,16 @@ export const BrazeProject = (): ReactElement => {

{/* Location */}

<Space />

<TouchableHighlight onPress={onSelectROIPress}>
<Text>Select ROI</Text>
</TouchableHighlight>

<TouchableHighlight onPress={onSelectUKPress}>
<Text>Select UK</Text>
</TouchableHighlight>

<Space />
{Platform.OS === 'android' ? (
<TouchableHighlight onPress={requestLocationInitialization}>
Expand Down
2 changes: 2 additions & 0 deletions BrazeProject/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReactPackage> =
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"
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SDK creates the instance regardless of configuration validity - i.e when braze.xml is absent and programmatic configuration is not executed yet, the SDK throws warnings to the console that API_KEY is empty

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()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workaround 1:
During testing, it appears push notifications are still coming for the device with the new braze config (new API key), even though the campaign is configured for another application (API key)


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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()

override fun createNativeModules(
reactContext: ReactApplicationContext
): MutableList<NativeModule> = listOf(
BrazeDynamicConfigurationBridge(reactContext),
).toMutableList()
}
14 changes: 0 additions & 14 deletions BrazeProject/android/app/src/main/res/values/braze.xml

This file was deleted.

10 changes: 10 additions & 0 deletions BrazeProject/babel.config.js
Original file line number Diff line number Diff line change
@@ -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
}],
]
};
9 changes: 9 additions & 0 deletions BrazeProject/global.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading