Skip to content

Commit

Permalink
In-app content blocks support added
Browse files Browse the repository at this point in the history
  • Loading branch information
adam1929 authored Nov 28, 2023
1 parent 0841ffb commit 134b755
Show file tree
Hide file tree
Showing 43 changed files with 5,089 additions and 4,250 deletions.
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'@react-native',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ android {
* [Fetching](./documentation/FETCHING.md)
* [Push notifications](./documentation/PUSH.md)
* [Anonymize customer](./documentation/ANONYMIZE.md)
* [In-app messages](./documentation/IN_APP_MESSAGES.md)
* In-App Personalization
* [In-app messages](./documentation/IN_APP_MESSAGES.md)
* [In-app content blocks](./documentation/IN_APP_CONTENT_BLOCKS.md)
* [App Inbox](./documentation/APP_INBOX.md)
* [Example/Package development documentation](./documentation/DEVELOPMENT.md) - Learn how to build example application or the package itself
* [SDK version update guide](./documentation/VERSION_UPDATE.md)
Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,12 @@ repositories {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
if (REACT_NATIVE_VERSION >= 71) {
implementation "com.facebook.react:react-android:"
implementation "com.facebook.react:react-android:0.72.6"
} else {
// noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+'
}
implementation 'com.exponea.sdk:sdk:3.6.1'
implementation 'com.exponea.sdk:sdk:3.10.0'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.danilopianini:gson-extras:0.2.2'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
Expand Down
7 changes: 7 additions & 0 deletions android/src/main/java/com/exponea/ConfigurationParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ internal class ConfigurationParser(private val readableMap: ReadableMap) {
)
parseAndroidConfig(androidConfig, context)
}
"inAppContentBlockPlaceholdersAutoLoad" -> {
val placeholderIds = map.getNullSafelyArray(
"inAppContentBlockPlaceholdersAutoLoad",
emptyList<String>()
) ?: emptyList()
configuration.inAppContentBlockPlaceholdersAutoLoad = placeholderIds
}
}
}
return configuration
Expand Down
4 changes: 3 additions & 1 deletion android/src/main/java/com/exponea/ExponeaPackage.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.exponea

import com.exponea.widget.AppInboxButtonManager
import com.exponea.widget.InAppContentBlocksPlaceholderManager
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
Expand All @@ -17,7 +18,8 @@ class ExponeaPackage : ReactPackage {
reactContext: ReactApplicationContext
): MutableList<ViewManager<*, *>> {
return arrayListOf(
AppInboxButtonManager()
AppInboxButtonManager(),
InAppContentBlocksPlaceholderManager()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.exponea.widget

import android.content.Context
import android.widget.LinearLayout
import com.exponea.sdk.Exponea
import com.exponea.sdk.models.InAppContentBlockPlaceholderConfiguration
import com.exponea.sdk.util.Logger
import com.exponea.sdk.view.InAppContentBlockPlaceholderView
import com.facebook.react.bridge.Arguments
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.events.RCTEventEmitter

class InAppContentBlocksPlaceholderManager : SimpleViewManager<InAppContentBlocksPlaceholder>() {

override fun getName() = "RNInAppContentBlocksPlaceholder"

override fun createViewInstance(reactContext: ThemedReactContext): InAppContentBlocksPlaceholder {
val container = InAppContentBlocksPlaceholder(reactContext)
container.setOnDimensChangedListener { width, height ->
Logger.i(this, "InAppCB: Size changed with w${width}px h${height}px")
val widthInDp = PixelUtil.toDIPFromPixel(width.toFloat())
val heightInDp = PixelUtil.toDIPFromPixel(height.toFloat())
notifyDimensChanged(reactContext, container.id, widthInDp, heightInDp)
}
return container
}

@ReactProp(name = "placeholderId")
fun setPlaceholderId(placeholderContainer: InAppContentBlocksPlaceholder, newPlaceholderId: String?) {
placeholderContainer.setPlaceholderId(newPlaceholderId)
}

override fun getExportedCustomBubblingEventTypeConstants(): MutableMap<String, Any>? {
val map = super.getExportedCustomBubblingEventTypeConstants() ?: MapBuilder.builder<String, Any>().build()
map.put("dimensChanged", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onDimensChanged")))
return map
}

private fun notifyDimensChanged(context: ThemedReactContext, viewId: Int, width: Float, height: Float) {
val event = Arguments.createMap().apply {
putDouble("width", width.toDouble())
putDouble("height", height.toDouble())
}
val eventEmitter = context.getJSModule(RCTEventEmitter::class.java)
eventEmitter.receiveEvent(viewId, "dimensChanged", event)
}
}

class InAppContentBlocksPlaceholder(context: Context?) : LinearLayout(context) {

private var currentPlaceholderId: String? = null
private var currentPlaceholderInstance: InAppContentBlockPlaceholderView? = null

private var sizeChangedListener: ((Int, Int) -> Unit)? = null

init {
orientation = VERTICAL
}

fun setPlaceholderId(newPlaceholderId: String?) {
if (currentPlaceholderId == newPlaceholderId && currentPlaceholderInstance != null) {
// placeholderContainer holds same currentPlaceholderInstance
currentPlaceholderInstance?.refreshContent()
return
}
currentPlaceholderId = newPlaceholderId
if (newPlaceholderId == null) {
currentPlaceholderInstance = null
} else {
currentPlaceholderInstance = Exponea.getInAppContentBlocksPlaceholder(
newPlaceholderId, context, InAppContentBlockPlaceholderConfiguration(true)
)
currentPlaceholderInstance?.let { registerContentLoadedListeners(it) }
}
this.removeAllViews()
currentPlaceholderInstance?.let {
val layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT
)
this.addView(it, layoutParams)
}
}

private fun registerContentLoadedListeners(target: InAppContentBlockPlaceholderView) {
target.setOnContentReadyListener { _ ->
sizeChangedListener?.invoke(target.width, target.height)
}
}

private val measureAndLayout = Runnable {
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED)
)
Logger.i(this, "InAppCB: Measuring with $left $top $right $bottom")
layout(left, top, right, bottom)
}

override fun requestLayout() {
super.requestLayout()
post(measureAndLayout)
}

fun setOnDimensChangedListener(listener: (Int, Int) -> Unit) {
sizeChangedListener = listener
}
}
4 changes: 3 additions & 1 deletion android/src/test/java/com/exponea/ExponeaModuleTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ internal class ExponeaModuleTest {

@Test
fun `should set default properties`() {
Exponea.init(ApplicationProvider.getApplicationContext(), ExponeaConfiguration())
Exponea.init(ApplicationProvider.getApplicationContext(), ExponeaConfiguration(
projectToken = "mockToken"
))
module.setDefaultProperties(
JavaOnlyMap.of(),
MockResolvingPromise { assertEquals(hashMapOf<String, Any>(), Exponea.defaultProperties) }
Expand Down
3 changes: 3 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
};
2 changes: 2 additions & 0 deletions documentation/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ You can see the Typescript definition for Configuration object at [src/Configura

* **advancedAuthEnabled** If true, Customer Token authentication is used for communication with BE for API listed in [Authorization](./AUTHORIZATION.md) document.

* **inAppContentBlockPlaceholdersAutoLoad** Automatically load content of In-app content blocks assigned to these Placeholder IDs

* **android** Specific configuration for Android

* **ios** Specific configuration for iOS
Expand Down
45 changes: 45 additions & 0 deletions documentation/IN_APP_CONTENT_BLOCKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## In-app content blocks
Exponea SDK allows you to display native In-app content blocks based on definitions set up on the Exponea web application. You can find information on creating your messages in [Exponea documentation](https://documentation.bloomreach.com/engagement/docs/in-app-content-blocks)

In-app content block will be shown exactly where you'll place a placeholder UI view. You can register a placeholder view into your layout:

```typescript jsx
<InAppContentBlocksPlaceholder
style={{
width: '100%',
}}
placeholderId={'placeholder_1'}
/>
```

No more developer work is required; they work automatically after the SDK is initialized.
In-app content blocks are shown within placeholder view by its ID automatically based on conditions setup on the Exponea backend. Once a message passes those filters, the SDK will try to present the message.

### If displaying In-app content blocks has delay

Message is able to be shown only if it is fully loaded and also its images are loaded too. In case that message is not yet fully loaded (including its images) then you may experience delayed showing.

If you need to show In-app content block as soon as possible (ideally instantly) you may set a auto-prefetch of placeholders. In-app content blocks for these placeholders are loaded immediately after SDK initialization.

```typescript jsx
import Exponea from 'react-native-exponea-sdk'

Exponea.configure({
// ... your configuration
inAppContentBlockPlaceholdersAutoLoad: ['placeholder_1'],
}).catch(error => console.log(error))
```

### In-app content block images caching
To reduce the number of API calls, SDK is caching the images displayed in messages. Therefore, once the SDK downloads the image, an image with the same URL may not be downloaded again, and will not change, since it was already cached. For this reason, we recommend always using different URLs for different images.

### In-app content blocks tracking

In-app content blocks are tracked automatically by SDK. You may see these `action` values in customers tracked events:

- 'show' - event is tracked if message has been shown to user
- 'action' - event is tracked if user clicked on action button inside message. Event contains 'text' and 'link' properties that you might be interested in
- 'close' - event is tracked if user clicked on close button inside message
- 'error' - event is tracked if showing of message has failed. Event contains 'error' property with meaningfull description

> The behaviour of In-app content block tracking may be affected by the tracking consent feature, which in enabled mode considers the requirement of explicit consent for tracking. Read more in [tracking consent documentation](./TRACKING_CONSENT.md).
2 changes: 1 addition & 1 deletion example/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'@react-native-community',
'@react-native',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
Expand Down
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ dependencies {
gmsImplementation 'com.google.firebase:firebase-messaging:23.0.0'
hmsImplementation 'com.huawei.agconnect:agconnect-core:1.5.2.300'
hmsImplementation 'com.huawei.hms:push:6.3.0.302'
implementation 'com.exponea.sdk:sdk:3.6.1'
implementation 'com.exponea.sdk:sdk:3.10.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.google.code.gson:gson:2.8.9'

Expand Down
15 changes: 8 additions & 7 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ require Pod::Executable.execute_command('node', ['-p',
{paths: [process.argv[1]]},
)', __dir__]).strip

platform :ios, '12.4'
platform :ios, '13.4'
install! 'cocoapods', :deterministic_uuids => false

target 'example' do
Expand Down Expand Up @@ -42,25 +42,26 @@ target 'example' do
:mac_catalyst_enabled => false
)

__apply_Xcode_12_5_M1_post_install_workaround(pi)

# Flipper requires a crude patch to bump up iOS deployment target, or "error: thread-local storage is not supported for the current target"
# I'm not aware of any other way to fix this one other than bumping iOS deployment target to match react-native (iOS 11 now)

# We need to make one crude patch to RCT-Folly - set `__IPHONE_10_0` to our iOS target + 1
# https://github.com/facebook/flipper/issues/834 - 84 comments and still going...
`sed -i -e $'s/__IPHONE_10_0/__IPHONE_14_0/' Pods/RCT-Folly/folly/portability/Time.h`
`sed -i -e $'s/__IPHONE_10_0/__IPHONE_14_0/' Pods/Flipper-Folly/Time.h`
deployment_target = Gem::Version.new('12.4')
deployment_target = Gem::Version.new('13.4')
pi.pods_project.targets.each do |target|
# Explicitly set pods deployment target for each build config to app deployment target
target.build_configurations.each do |config|
target.build_configurations.each do |config|
# Explicitly set pods deployment target for each build config to app deployment target
current_pod_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
pod_ios_deployment_target = Gem::Version.new(current_pod_target)
if pod_ios_deployment_target <= deployment_target
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
end
end
end
end

end
end

Expand Down
14 changes: 7 additions & 7 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ PODS:
- boost (1.76.0)
- CocoaAsyncSocket (7.6.5)
- DoubleConversion (1.1.6)
- ExponeaSDK (2.16.4):
- ExponeaSDK (2.20.0):
- SwiftSoup (= 2.6.1)
- ExponeaSDK-Notifications (2.16.3)
- ExponeaSDK-Notifications (2.20.0)
- FBLazyVector (0.72.6)
- FBReactNativeSpec (0.72.6):
- RCT-Folly (= 2021.07.22.00)
Expand Down Expand Up @@ -349,7 +349,7 @@ PODS:
- glog
- react-native-exponea-sdk (1.5.2):
- AnyCodable-FlightSchool (= 0.4.0)
- ExponeaSDK (= 2.16.4)
- ExponeaSDK (= 2.20.0)
- React-Core
- react-native-safe-area-context (3.4.1):
- React-Core
Expand Down Expand Up @@ -689,8 +689,8 @@ SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
ExponeaSDK: 68f0ae7c0cb8cc6a1452b9ca53d0feaba581c93c
ExponeaSDK-Notifications: 298fe6dccc163de718d176782ae9df25f9dc7104
ExponeaSDK: 5f1e5cd77dafded29a7f654bc38db40bed100eaf
ExponeaSDK-Notifications: e1526805724729a97a0b774b834fda037cce1e17
FBLazyVector: 748c0ef74f2bf4b36cfcccf37916806940a64c32
FBReactNativeSpec: 966f29e4e697de53a3b366355e8f57375c856ad9
Flipper: 6edb735e6c3e332975d1b17956bcc584eccf5818
Expand Down Expand Up @@ -720,7 +720,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: faca9c368233f59ed24601aca0185870466a96e9
React-jsinspector: 194e32c6aab382d88713ad3dd0025c5f5c4ee072
React-logger: cebf22b6cf43434e471dc561e5911b40ac01d289
react-native-exponea-sdk: 8589512f1ed302fa70c15830abedb29f9cdfc4a4
react-native-exponea-sdk: 937b5c275b816f5a1a01f521d149b13759c5e872
react-native-safe-area-context: 9e40fb181dac02619414ba1294d6c2a807056ab9
React-NativeModulesApple: 63505fb94b71e2469cab35bdaf36cca813cb5bfd
React-perflogger: e3596db7e753f51766bceadc061936ef1472edc3
Expand Down Expand Up @@ -749,6 +749,6 @@ SPEC CHECKSUMS:
Yoga: b76f1acfda8212aa16b7e26bcce3983230c82603
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

PODFILE CHECKSUM: d68ac50c9e165e62133828ba6fec99d6e5cb0a45
PODFILE CHECKSUM: 054bda3c53e0577793996547a5f53c2a50ed0b1b

COCOAPODS: 1.13.0
Loading

0 comments on commit 134b755

Please sign in to comment.