Skip to content

Commit d11041b

Browse files
authored
feat: inline in-app messages (#453)
2 parents 7a3d68d + 7cb453c commit d11041b

48 files changed

Lines changed: 4659 additions & 320 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

android/build.gradle

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,32 @@ def getExtOrIntegerDefault(name) {
2424
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['customerio.reactnative.' + name]).toInteger()
2525
}
2626

27+
def isNewArchitectureEnabled() {
28+
// Allow customers to override new architecture setting specifically for Customer.io SDK.
29+
// This is useful as a workaround for apps where the new architecture has issues.
30+
// Customers can add 'customerioNewArchEnabled=false' in their gradle.properties
31+
// to force the SDK to use old architecture even when their app has newArchEnabled=true.
32+
// This can be helpful for customers who are using the new architecture in their app, but are on older
33+
// versions of React Native than the one used by the SDK to generate the codegen files.
34+
if (project.hasProperty("customerioNewArchEnabled")) {
35+
return project.customerioNewArchEnabled == "true"
36+
}
37+
// Otherwise, use react-native's newArchEnabled property.
38+
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
39+
}
40+
41+
if (isNewArchitectureEnabled()) {
42+
apply plugin: "com.facebook.react"
43+
}
44+
2745
android {
2846
namespace = 'io.customer.reactnative.sdk'
2947
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
3048
defaultConfig {
49+
def isNewArchEnabled = isNewArchitectureEnabled()
3150
minSdkVersion getExtOrIntegerDefault('minSdkVersion')
3251
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
52+
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchEnabled.toString())
3353
}
3454

3555
buildTypes {
@@ -44,6 +64,16 @@ android {
4464
sourceCompatibility JavaVersion.VERSION_17
4565
targetCompatibility JavaVersion.VERSION_17
4666
}
67+
sourceSets {
68+
main {
69+
if (isNewArchitectureEnabled()) {
70+
// Include both new architecture source directories and codegen generated files
71+
java.srcDirs += ['src/newarch']
72+
} else {
73+
java.srcDirs += ['src/oldarch']
74+
}
75+
}
76+
}
4777
}
4878

4979
repositories {

android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ customerio.reactnative.kotlinVersion=1.7.21
22
customerio.reactnative.compileSdkVersion=33
33
customerio.reactnative.targetSdkVersion=33
44
customerio.reactnative.minSdkVersion=21
5-
customerio.reactnative.cioSDKVersionAndroid=4.6.1
5+
customerio.reactnative.cioSDKVersionAndroid=4.6.3

android/src/main/java/io/customer/reactnative/sdk/CustomerIOReactNativeModule.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class CustomerIOReactNativeModule(
2424
private val pushMessagingModule: RNCIOPushMessaging,
2525
private val inAppMessagingModule: RNCIOInAppMessaging,
2626
) : ReactContextBaseJavaModule(reactContext) {
27-
override fun getName(): String = "NativeCustomerIO"
27+
override fun getName(): String = NAME
2828

2929
private val logger: Logger = SDKComponent.logger
3030
private fun customerIO(): CustomerIO? = runCatching {
@@ -146,4 +146,8 @@ class CustomerIOReactNativeModule(
146146
fun showPromptForPushNotifications(pushConfigurationOptions: ReadableMap?, promise: Promise) {
147147
pushMessagingModule.showPromptForPushNotifications(pushConfigurationOptions, promise)
148148
}
149+
150+
companion object {
151+
const val NAME = "NativeCustomerIO"
152+
}
149153
}

android/src/main/java/io/customer/reactnative/sdk/CustomerIOReactNativePackage.kt

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package io.customer.reactnative.sdk
2+
3+
import com.facebook.react.bridge.NativeModule
4+
import com.facebook.react.bridge.ReactApplicationContext
5+
import com.facebook.react.uimanager.ViewManager
6+
import io.customer.reactnative.sdk.logging.RNCIOConsoleLoggerModule
7+
import io.customer.reactnative.sdk.messaginginapp.InlineInAppMessageViewManager
8+
import io.customer.reactnative.sdk.messaginginapp.RNCIOInAppMessaging
9+
import io.customer.reactnative.sdk.messagingpush.RNCIOPushMessaging
10+
11+
/**
12+
* Shared implementation of the [CustomerIOReactNativePackage] for common functionality in
13+
* both the new and old architecture.
14+
*/
15+
object CustomerIOReactNativePackageImpl {
16+
/**
17+
* Creates and initializes all native modules for the Customer.io React Native SDK.
18+
*/
19+
private fun createAndInitializeModules(
20+
reactContext: ReactApplicationContext,
21+
nativeModules: MutableMap<String, NativeModule>? = null
22+
): Map<String, NativeModule> {
23+
val modules = nativeModules ?: mutableMapOf()
24+
25+
val loggerModule = modules.getOrPut(RNCIOConsoleLoggerModule.NAME) {
26+
RNCIOConsoleLoggerModule(reactContext)
27+
}
28+
val pushMessagingModule = modules.getOrPut(RNCIOPushMessaging.NAME) {
29+
RNCIOPushMessaging(reactContext)
30+
} as RNCIOPushMessaging
31+
val inAppMessagingModule = modules.getOrPut(RNCIOInAppMessaging.NAME) {
32+
RNCIOInAppMessaging(reactContext)
33+
} as RNCIOInAppMessaging
34+
val mainModule = modules.getOrPut(CustomerIOReactNativeModule.NAME) {
35+
CustomerIOReactNativeModule(
36+
reactContext = reactContext,
37+
pushMessagingModule = pushMessagingModule,
38+
inAppMessagingModule = inAppMessagingModule,
39+
)
40+
}
41+
42+
return mapOf(
43+
RNCIOConsoleLoggerModule.NAME to loggerModule,
44+
RNCIOPushMessaging.NAME to pushMessagingModule,
45+
RNCIOInAppMessaging.NAME to inAppMessagingModule,
46+
CustomerIOReactNativeModule.NAME to mainModule
47+
)
48+
}
49+
50+
/**
51+
* Creates the list of native modules for the Customer.io React Native SDK.
52+
*/
53+
fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
54+
// Since order does not matter, we can just return the values of the map
55+
return createAndInitializeModules(reactContext).values.toList()
56+
}
57+
58+
/**
59+
* Creates the list of view managers for the Customer.io React Native SDK.
60+
*/
61+
fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
62+
return listOf(InlineInAppMessageViewManager())
63+
}
64+
65+
/**
66+
* Initializes native modules with caching to avoid re-creating them multiple times.
67+
* This should be removed in the future when we switch to TurboModules.
68+
*/
69+
fun initializeNativeModules(
70+
reactContext: ReactApplicationContext,
71+
nativeModules: MutableMap<String, NativeModule>
72+
) {
73+
createAndInitializeModules(reactContext, nativeModules)
74+
}
75+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.customer.reactnative.sdk.extension
2+
3+
import android.view.View
4+
import com.facebook.react.bridge.ReactContext
5+
6+
/**
7+
* Extension property to get [ReactContext] from a View.
8+
* Casts the view's context to ReactContext for React Native integration.
9+
*/
10+
internal val View.reactContext: ReactContext
11+
get() = context as ReactContext

android/src/main/java/io/customer/reactnative/sdk/logging/RNCIOConsoleLoggerModule.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import java.lang.ref.WeakReference
1212
class RNCIOConsoleLoggerModule(
1313
reactContext: ReactApplicationContext,
1414
) : ReactContextBaseJavaModule(reactContext) {
15-
override fun getName(): String = "CioLoggingEmitter"
15+
override fun getName(): String = NAME
1616

1717
// Hold weak reference to ReactContext to avoid memory leaks
1818
// As loggers are long-lived objects, they might hold references to log dispatchers
@@ -54,4 +54,8 @@ class RNCIOConsoleLoggerModule(
5454
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
5555
.emit("CioLogEvent", params)
5656
}
57+
58+
companion object {
59+
const val NAME = "CioLoggingEmitter"
60+
}
5761
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package io.customer.reactnative.sdk.messaginginapp
2+
3+
import com.facebook.react.uimanager.SimpleViewManager
4+
import com.facebook.react.uimanager.ThemedReactContext
5+
import com.facebook.react.uimanager.annotations.ReactProp
6+
import io.customer.messaginginapp.ui.bridge.WrapperPlatformDelegate
7+
8+
/**
9+
* Base view manager for inline in-app message components.
10+
*
11+
* Provides common functionality for both old and new React Native architecture
12+
* implementations, including view creation, event handling, and property management.
13+
*/
14+
abstract class BaseInlineInAppMessageViewManager :
15+
SimpleViewManager<ReactInlineInAppMessageView>() {
16+
override fun getName() = NAME
17+
18+
override fun createViewInstance(context: ThemedReactContext): ReactInlineInAppMessageView {
19+
return ReactInlineInAppMessageView(context)
20+
}
21+
22+
/**
23+
* This method exports custom direct event types for the inline message view.
24+
* It registers two custom events:
25+
* - onSizeChange: Triggered when the size of the inline message changes.
26+
* - onStateChange: Triggered when the state of the inline message changes.
27+
*/
28+
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
29+
val customEvents = super.getExportedCustomDirectEventTypeConstants() ?: mutableMapOf()
30+
val registerCustomEvent = { eventName: String ->
31+
customEvents.put(eventName, mapOf("registrationName" to eventName))
32+
}
33+
registerCustomEvent(WrapperPlatformDelegate.ON_SIZE_CHANGE)
34+
registerCustomEvent(WrapperPlatformDelegate.ON_STATE_CHANGE)
35+
registerCustomEvent(ReactInAppPlatformDelegate.ON_ACTION_CLICK)
36+
return customEvents
37+
}
38+
39+
@ReactProp(name = "elementId")
40+
fun setElementId(view: ReactInlineInAppMessageView, elementId: String?) {
41+
view.elementId = elementId
42+
}
43+
44+
companion object {
45+
const val NAME = "InlineMessageNative"
46+
}
47+
}

android/src/main/java/io/customer/reactnative/sdk/messaginginapp/RNCIOInAppMessaging.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import io.customer.sdk.data.model.Region
2323
class RNCIOInAppMessaging(
2424
private val reactContext: ReactApplicationContext,
2525
) : ReactContextBaseJavaModule(reactContext), InAppEventListener {
26-
override fun getName(): String = "CioRctInAppMessaging"
26+
override fun getName(): String = NAME
2727

2828
private val inAppMessagingModule: ModuleMessagingInApp?
2929
get() = kotlin.runCatching { CustomerIO.instance().inAppMessaging() }.getOrNull()
@@ -122,4 +122,8 @@ class RNCIOInAppMessaging(
122122
eventType = "messageShown",
123123
message = message,
124124
)
125+
126+
companion object {
127+
const val NAME = "CioRctInAppMessaging"
128+
}
125129
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.customer.reactnative.sdk.messaginginapp
2+
3+
import android.view.View
4+
import com.facebook.react.bridge.Arguments
5+
import io.customer.messaginginapp.ui.bridge.WrapperPlatformDelegate
6+
import io.customer.reactnative.sdk.extension.sendUIEventToReactJS
7+
8+
/**
9+
* React Native platform delegate for in-app messaging.
10+
* Bridges native in-app message events to React Native components.
11+
*
12+
* @param view The native Android view hosting the in-app message
13+
*/
14+
class ReactInAppPlatformDelegate(
15+
view: View,
16+
) : WrapperPlatformDelegate(view) {
17+
/**
18+
* Dispatches in-app message events from native Android to React Native components.
19+
* Converts native events to React Native UI events using the provided event name and payload.
20+
*/
21+
override fun dispatchEvent(eventName: String, payload: Map<String, Any?>) {
22+
view.sendUIEventToReactJS(
23+
eventName = eventName,
24+
payload = Arguments.makeNativeMap(payload),
25+
)
26+
}
27+
28+
// Internal helper method for dispatching events within the SDK.
29+
internal fun dispatchEventInternal(eventName: String, payload: Map<String, Any?>) {
30+
dispatchEvent(eventName, payload)
31+
}
32+
33+
companion object {
34+
// Event name constant for in-app message action clicks
35+
const val ON_ACTION_CLICK = "onActionClick"
36+
}
37+
}

0 commit comments

Comments
 (0)