@amplitude/plugin-engagement-react-native 0.1.1-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/PluginEngagementReactNative.podspec +22 -0
- package/README.md +95 -0
- package/android/build.gradle +89 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/amplitude/pluginengagementreactnative/PluginEngagementReactNativeModule.kt +183 -0
- package/android/src/main/java/com/amplitude/pluginengagementreactnative/PluginEngagementReactNativePackage.kt +33 -0
- package/ios/AmplitudeEngagementAdapter.swift +121 -0
- package/ios/PluginEngagementReactNative.h +5 -0
- package/ios/PluginEngagementReactNative.mm +87 -0
- package/lib/module/AmplitudeEngagement.js +42 -0
- package/lib/module/AmplitudeEngagement.js.map +1 -0
- package/lib/module/AmplitudeEngagementPlugin.js +29 -0
- package/lib/module/AmplitudeEngagementPlugin.js.map +1 -0
- package/lib/module/NativePluginEngagementReactNative.js +5 -0
- package/lib/module/NativePluginEngagementReactNative.js.map +1 -0
- package/lib/module/index.js +64 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/logger.js +53 -0
- package/lib/module/logger.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AmplitudeEngagement.d.ts +17 -0
- package/lib/typescript/src/AmplitudeEngagement.d.ts.map +1 -0
- package/lib/typescript/src/AmplitudeEngagementPlugin.d.ts +10 -0
- package/lib/typescript/src/AmplitudeEngagementPlugin.d.ts.map +1 -0
- package/lib/typescript/src/NativePluginEngagementReactNative.d.ts +18 -0
- package/lib/typescript/src/NativePluginEngagementReactNative.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +13 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/logger.d.ts +28 -0
- package/lib/typescript/src/logger.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +7 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +168 -0
- package/src/AmplitudeEngagement.ts +62 -0
- package/src/AmplitudeEngagementPlugin.ts +47 -0
- package/src/NativePluginEngagementReactNative.ts +28 -0
- package/src/index.tsx +78 -0
- package/src/logger.ts +72 -0
- package/src/types.ts +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Amplitude, Inc.
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "PluginEngagementReactNative"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://amplitude.com.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,cpp,swift}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
#s.dependency "AmplitudeEngagementSwift"
|
|
19
|
+
s.dependency "AmplitudeEngagementSwift"
|
|
20
|
+
|
|
21
|
+
install_modules_dependencies(s)
|
|
22
|
+
end
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @amplitude/plugin-engagement-react-native
|
|
2
|
+
|
|
3
|
+
Amplitude Engagement plugin for React Native
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To install:
|
|
8
|
+
- Add `@amplitude/analytics-react-native` and `@amplitude/plugin-engagement-react-native`
|
|
9
|
+
- Also, add `@react-native-async-storage/async-storage`
|
|
10
|
+
- Why? This is used by the `engagement` native module, but the CLI only auto links native modules which you directly depend on in your app’s `package.json`
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @amplitude/analytics-react-native
|
|
15
|
+
npm install @amplitude/plugin-engagement-react-native
|
|
16
|
+
npm install @react-native-async-storage/async-storage
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Add the source for the `AmplitudeEngagementSwift` pod to your Podfile:
|
|
20
|
+
```rb
|
|
21
|
+
target '<your appname>' do
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
pod "AmplitudeEngagementSwift", :git => "https://github.com/amplitude/Amplitude-Engagement-Swift"
|
|
25
|
+
|
|
26
|
+
...
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Re-run `pod install` in the `ios` directory:
|
|
31
|
+
```sh
|
|
32
|
+
$ cd ios
|
|
33
|
+
$ bundle exec pod install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Setup code
|
|
39
|
+
|
|
40
|
+
Setup by adding the plugin and calling “init” as usual:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
// index.js
|
|
44
|
+
|
|
45
|
+
import {Linking} from 'react-native';
|
|
46
|
+
|
|
47
|
+
import { init, add } from '@amplitude/analytics-react-native';
|
|
48
|
+
import { getPlugin, handleURL } from '@amplitude/plugin-engagement-react-native';
|
|
49
|
+
|
|
50
|
+
init('<<< YOUR API KEY HERE >>>');
|
|
51
|
+
add(getPlugin());
|
|
52
|
+
|
|
53
|
+
Linking.getInitialURL().then(async (url) => {
|
|
54
|
+
if (url) {
|
|
55
|
+
const didHandleURL = await handleURL(url);
|
|
56
|
+
if (didHandleURL) { return; }
|
|
57
|
+
|
|
58
|
+
// Handle a non-Amplitude SDK URL
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
Linking.addEventListener('url', async ({ url }) => {
|
|
63
|
+
const didHandleURL = await handleURL(url);
|
|
64
|
+
if (didHandleURL) { return; }
|
|
65
|
+
|
|
66
|
+
// Handle a non-Amplitude SDK URL
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Linking setup
|
|
71
|
+
If you don't already have it set up, please follow this guide to enable deep-linking support in your React Native app in addition to adding the above Linking code:
|
|
72
|
+
https://reactnative.dev/docs/linking#enabling-deep-links
|
|
73
|
+
|
|
74
|
+
Then, following the Alpha Onboarding guide to setup the "scheme" for the deep links in your Android and iOS projects.
|
|
75
|
+
|
|
76
|
+
### Boot
|
|
77
|
+
|
|
78
|
+
Finally, “boot” with the user’s ID:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
import {
|
|
82
|
+
boot
|
|
83
|
+
} from '@amplitude/plugin-engagement-react-native';
|
|
84
|
+
|
|
85
|
+
export default function App() {
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
boot('rn-test-user-1', 'test-device-1');
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = {name ->
|
|
3
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['PluginEngagementReactNative_' + name]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
13
|
+
// noinspection DifferentKotlinGradleVersion
|
|
14
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
apply plugin: "com.android.library"
|
|
20
|
+
apply plugin: "kotlin-android"
|
|
21
|
+
|
|
22
|
+
apply plugin: "com.facebook.react"
|
|
23
|
+
|
|
24
|
+
def getExtOrIntegerDefault(name) {
|
|
25
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["PluginEngagementReactNative_" + name]).toInteger()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
android {
|
|
29
|
+
namespace "com.amplitude.pluginengagementreactnative"
|
|
30
|
+
|
|
31
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
32
|
+
|
|
33
|
+
defaultConfig {
|
|
34
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
35
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
buildFeatures {
|
|
39
|
+
buildConfig true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
buildTypes {
|
|
43
|
+
release {
|
|
44
|
+
minifyEnabled false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lintOptions {
|
|
49
|
+
disable "GradleCompatible"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
compileOptions {
|
|
53
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
54
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sourceSets {
|
|
58
|
+
main {
|
|
59
|
+
java.srcDirs += [
|
|
60
|
+
"generated/java",
|
|
61
|
+
"generated/jni"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
repositories {
|
|
68
|
+
mavenCentral()
|
|
69
|
+
google()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
73
|
+
|
|
74
|
+
dependencies {
|
|
75
|
+
implementation "com.facebook.react:react-android"
|
|
76
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
77
|
+
|
|
78
|
+
// Amplitude Engagement SDK
|
|
79
|
+
implementation "com.amplitude:amplitude-engagement-android:1.0.14"
|
|
80
|
+
|
|
81
|
+
// Amplitude Analytics SDK (required dependency)
|
|
82
|
+
implementation "com.amplitude:analytics-android:1.+"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
react {
|
|
86
|
+
jsRootDir = file("../src/")
|
|
87
|
+
libraryName = "PluginEngagementReactNative"
|
|
88
|
+
codegenJavaPackageName = "com.amplitude.pluginengagementreactnative"
|
|
89
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
package com.amplitude.pluginengagementreactnative
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.os.Bundle
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import androidx.core.net.toUri
|
|
9
|
+
import com.amplitude.android.engagement.AmplitudeEngagement
|
|
10
|
+
import com.amplitude.android.engagement.__ReactNative__AESDK
|
|
11
|
+
import com.amplitude.android.engagement.AmplitudeInitOptions
|
|
12
|
+
import com.amplitude.android.engagement.ui.theme.ThemeMode
|
|
13
|
+
import com.amplitude.core.events.BaseEvent
|
|
14
|
+
import com.facebook.react.bridge.ActivityEventListener
|
|
15
|
+
import com.facebook.react.bridge.Arguments
|
|
16
|
+
import com.facebook.react.bridge.Callback
|
|
17
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
18
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
19
|
+
import com.facebook.react.bridge.ReadableMap
|
|
20
|
+
import com.facebook.react.bridge.WritableArray
|
|
21
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
22
|
+
import kotlinx.coroutines.Dispatchers
|
|
23
|
+
import kotlinx.coroutines.runBlocking
|
|
24
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
25
|
+
|
|
26
|
+
@ReactModule(name = PluginEngagementReactNativeModule.NAME)
|
|
27
|
+
class PluginEngagementReactNativeModule(val reactContext: ReactApplicationContext) :
|
|
28
|
+
NativePluginEngagementReactNativeSpec(reactContext) {
|
|
29
|
+
|
|
30
|
+
private data class InstanceInfo(val apiKey: String, val instance: AmplitudeEngagement)
|
|
31
|
+
|
|
32
|
+
private var instances = ConcurrentHashMap<Double, InstanceInfo>()
|
|
33
|
+
private var _id: Double = 0.0
|
|
34
|
+
|
|
35
|
+
override fun getName(): String {
|
|
36
|
+
return NAME
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun newInstance(apiKey: String): Double {
|
|
40
|
+
val existingId = instances.entries.firstOrNull { it.value.apiKey == apiKey }?.key
|
|
41
|
+
if (existingId != null) {
|
|
42
|
+
return existingId
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return synchronized(this) {
|
|
46
|
+
runBlocking(Dispatchers.Main) {
|
|
47
|
+
val amplitudeEngagement = __ReactNative__AESDK(reactContext, apiKey, AmplitudeInitOptions())
|
|
48
|
+
|
|
49
|
+
// The React Native environment seems to have a different activity management lifecycle;
|
|
50
|
+
// so we need to register our own listener to get the current activity.
|
|
51
|
+
amplitudeEngagement.setCurrentActivity(reactContext.currentActivity)
|
|
52
|
+
val lifecycleEventListener: LifecycleEventListener = object : LifecycleEventListener {
|
|
53
|
+
override fun onHostResume() {
|
|
54
|
+
amplitudeEngagement.setCurrentActivity(reactContext.currentActivity)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun onHostPause() {
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun onHostDestroy() {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
reactContext.addLifecycleEventListener(lifecycleEventListener)
|
|
65
|
+
|
|
66
|
+
_id++
|
|
67
|
+
val id = _id
|
|
68
|
+
instances[id] = InstanceInfo(
|
|
69
|
+
apiKey,
|
|
70
|
+
amplitudeEngagement
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
id
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override fun boot(id: Double, userId: String, deviceId: String) {
|
|
79
|
+
val instance = instances[id]?.instance ?: return
|
|
80
|
+
|
|
81
|
+
runBlocking(Dispatchers.Main) {
|
|
82
|
+
Log.d("PluginEngagementReactNativeModule", "boot: $userId, $deviceId")
|
|
83
|
+
instance.boot(userId, deviceId)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun setThemeMode(id: Double, themeMode: String?) {
|
|
88
|
+
val instance = instances[id]?.instance ?: return
|
|
89
|
+
|
|
90
|
+
runBlocking(Dispatchers.Main) {
|
|
91
|
+
if (themeMode != null) {
|
|
92
|
+
instance.setThemeMode(ThemeMode.valueOf(themeMode))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override fun reset(id: Double, key: String, stepIndex: Double) {
|
|
98
|
+
val instance = instances[id]?.instance ?: return
|
|
99
|
+
runBlocking(Dispatchers.Main) {
|
|
100
|
+
instance.reset(key, stepIndex.toInt())
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun list(id: Double): WritableArray {
|
|
105
|
+
val writableArray = Arguments.createArray()
|
|
106
|
+
val instance = instances[id]?.instance ?: return writableArray
|
|
107
|
+
runBlocking(Dispatchers.Main) {
|
|
108
|
+
val guidesAndSurveysList = instance.list()
|
|
109
|
+
guidesAndSurveysList.forEach { guide ->
|
|
110
|
+
Log.d("PluginEngagementReactNativeModule", "list: $guide")
|
|
111
|
+
val map = Arguments.createMap();
|
|
112
|
+
map.putInt("id", guide.id);
|
|
113
|
+
map.putString("title", guide.title);
|
|
114
|
+
map.putString("status", guide.status);
|
|
115
|
+
map.putInt("step", guide.step);
|
|
116
|
+
writableArray.pushMap(map);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return writableArray
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
override fun show(id: Double, key: String, stepIndex: Double) {
|
|
123
|
+
val instance = instances[id]?.instance ?: return
|
|
124
|
+
runBlocking(Dispatchers.Main) {
|
|
125
|
+
instance.show(key, stepIndex.toInt())
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override fun screen(id: Double, screenName: String) {
|
|
130
|
+
val instance = instances[id]?.instance ?: return
|
|
131
|
+
runBlocking(Dispatchers.Main) {
|
|
132
|
+
instance.screen(screenName)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override fun closeAll(id: Double) {
|
|
137
|
+
val instance = instances[id]?.instance ?: return
|
|
138
|
+
runBlocking(Dispatchers.Main) {
|
|
139
|
+
instance.closeAll()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override fun forwardEvent(id: Double, event: ReadableMap) {
|
|
144
|
+
val instance = instances[id]?.instance ?: return
|
|
145
|
+
val baseEvent = BaseEvent()
|
|
146
|
+
baseEvent.eventType = event.getString("event_type") ?: return
|
|
147
|
+
baseEvent.eventId = event.getDouble("event_id").toLong()
|
|
148
|
+
baseEvent.platform = event.getString("platform")
|
|
149
|
+
baseEvent.osName = event.getString("os_name")
|
|
150
|
+
baseEvent.osVersion = event.getString("os_version")
|
|
151
|
+
baseEvent.eventProperties = event.getMap("event_properties")?.toHashMap()
|
|
152
|
+
baseEvent.groupProperties = event.getMap("group_properties")?.toHashMap()
|
|
153
|
+
baseEvent.groups = event.getMap("groups")?.toHashMap()
|
|
154
|
+
baseEvent.userProperties = event.getMap("user_properties")?.toHashMap()
|
|
155
|
+
Log.d("PluginEngagementReactNativeModule", "forwardEvent: $baseEvent")
|
|
156
|
+
runBlocking(Dispatchers.Main) {
|
|
157
|
+
instance.forwardEvent(baseEvent)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
override fun addCallback(id: Double, key: String, func: Callback) {
|
|
162
|
+
val instance = instances[id]?.instance ?: return
|
|
163
|
+
runBlocking(Dispatchers.Main) {
|
|
164
|
+
instance.addCallback(key) { func.invoke() }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override fun handleURL(id: Double, url: String): Boolean {
|
|
169
|
+
val instance = instances[id]?.instance ?: return false
|
|
170
|
+
|
|
171
|
+
val uri = url.toUri()
|
|
172
|
+
val intent = Intent(Intent.ACTION_VIEW, uri)
|
|
173
|
+
|
|
174
|
+
return runBlocking(Dispatchers.Main) {
|
|
175
|
+
instance.handlePreviewLinkIntent(intent)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
companion object {
|
|
181
|
+
const val NAME = "PluginEngagementReactNative"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.amplitude.pluginengagementreactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class PluginEngagementReactNativePackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == PluginEngagementReactNativeModule.NAME) {
|
|
13
|
+
PluginEngagementReactNativeModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
|
+
return ReactModuleInfoProvider {
|
|
21
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
22
|
+
moduleInfos[PluginEngagementReactNativeModule.NAME] = ReactModuleInfo(
|
|
23
|
+
PluginEngagementReactNativeModule.NAME,
|
|
24
|
+
PluginEngagementReactNativeModule.NAME,
|
|
25
|
+
false, // canOverrideExistingModule
|
|
26
|
+
false, // needsEagerInit
|
|
27
|
+
false, // isCxxModule
|
|
28
|
+
true // isTurboModule
|
|
29
|
+
)
|
|
30
|
+
moduleInfos
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//
|
|
2
|
+
// AmplitudeEngagementAdapter.swift
|
|
3
|
+
// PluginEngagementReactNative
|
|
4
|
+
//
|
|
5
|
+
// Created by Jared Luxenberg on 5/19/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import AmplitudeEngagementSwift
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
extension Encodable {
|
|
13
|
+
/// Converting object to postable dictionary
|
|
14
|
+
func toDictionary(_ encoder: JSONEncoder = JSONEncoder()) throws -> [String: Any] {
|
|
15
|
+
let data = try encoder.encode(self)
|
|
16
|
+
let object = try JSONSerialization.jsonObject(with: data)
|
|
17
|
+
if let json = object as? [String: Any] { return json }
|
|
18
|
+
|
|
19
|
+
let context = DecodingError.Context(codingPath: [], debugDescription: "Deserialized object is not a dictionary")
|
|
20
|
+
throw DecodingError.typeMismatch(type(of: object), context)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// id -> (api_key, amplitude engagement instance)
|
|
25
|
+
var instances: [Int: (String, AmplitudeEngagement)] = [:]
|
|
26
|
+
var _id = 0
|
|
27
|
+
|
|
28
|
+
/** `@objc` attribute exposes Swift methods to the Objective-C runtime**/
|
|
29
|
+
@objc public class AmplitudeEngagementAdapter: NSObject {
|
|
30
|
+
@objc public func newInstance(_ apiKey: String) -> Int {
|
|
31
|
+
if let existingId = instances.first(where: { $0.value.0 == apiKey })?.key {
|
|
32
|
+
return existingId
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return DispatchQueue.main.sync {
|
|
36
|
+
_id += 1;
|
|
37
|
+
let id = _id + 1;
|
|
38
|
+
instances[id] = (apiKey, __ReactNative__AESDK(apiKey, AmplitudeInitOptions(logLevel: AmplitudeLogLevel.verbose)))
|
|
39
|
+
return id
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@objc public func boot(_ instanceId: Int, _ user_id: String, device_id: String? = nil) {
|
|
45
|
+
DispatchQueue.main.sync {
|
|
46
|
+
guard let (_, instance) = instances[instanceId] else { return }
|
|
47
|
+
|
|
48
|
+
return instance.boot(user_id, device_id: device_id)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@objc public func setThemeMode(_ instanceId: Int, themeMode: String) {
|
|
53
|
+
DispatchQueue.main.sync {
|
|
54
|
+
guard let (_, instance) = instances[instanceId] else { fatalError("🚨 Runtime exception: no instance found for id “\(instanceId)”") }
|
|
55
|
+
guard let themeModeParsed = ThemeMode(rawValue: themeMode) else { return }
|
|
56
|
+
|
|
57
|
+
return instance.setThemeMode(themeMode: themeModeParsed)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@objc public func reset(_ instanceId: Int, key: String, stepIndex: Int = 0) {
|
|
62
|
+
DispatchQueue.main.sync {
|
|
63
|
+
guard let (_, instance) = instances[instanceId] else { fatalError("🚨 Runtime exception: no instance found for id “\(instanceId)”") }
|
|
64
|
+
|
|
65
|
+
return instance.reset(key: key, stepIndex: stepIndex)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@objc public func list(_ instanceId: Int) -> [[String: Any]] {
|
|
70
|
+
DispatchQueue.main.sync {
|
|
71
|
+
guard let (_, instance) = instances[instanceId] else { fatalError("🚨 Runtime exception: no instance found for id “\(instanceId)”") }
|
|
72
|
+
|
|
73
|
+
return instance.list().compactMap { x in try? x.toDictionary() }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@objc public func handleUrl(_ instanceId: Int, _ url: String) -> Bool {
|
|
78
|
+
DispatchQueue.main.sync {
|
|
79
|
+
guard let (_, instance) = instances[instanceId] else { fatalError("🚨 Runtime exception: no instance found for id “\(instanceId)”") }
|
|
80
|
+
guard let url = URL(string: url) else { return false; }
|
|
81
|
+
|
|
82
|
+
return instance.handleUrl(url)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@objc public func show(_ instanceId: Int, key: String, stepIndex: Int = 0) {
|
|
87
|
+
DispatchQueue.main.sync {
|
|
88
|
+
guard let (_, instance) = instances[instanceId] else { fatalError("🚨 Runtime exception: no instance found for id “\(instanceId)”") }
|
|
89
|
+
return instance.show(key: key, stepIndex: stepIndex)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@objc public func screen(_ instanceId: Int, _ screenName: String) {
|
|
94
|
+
DispatchQueue.main.sync {
|
|
95
|
+
guard let (_, instance) = instances[instanceId] else { fatalError("🚨 Runtime exception: no instance found for id “\(instanceId)”") }
|
|
96
|
+
return instance.screen(screenName)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@objc public func closeAll(_ instanceId: Int) {
|
|
101
|
+
DispatchQueue.main.sync {
|
|
102
|
+
guard let (_, instance) = instances[instanceId] else { fatalError("🚨 Runtime exception: no instance found for id “\(instanceId)”") }
|
|
103
|
+
return instance.closeAll()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@objc public func forwardEvent(_ instanceId: Int, _ event: [String: Any]) {
|
|
108
|
+
DispatchQueue.main.sync {
|
|
109
|
+
guard let (_, instance) = instances[instanceId] else { fatalError("🚨 Runtime exception: no instance found for id “\(instanceId)”") }
|
|
110
|
+
return instance.forwardEvent(event)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@objc(addCallback:::) public func addCallback(_ instanceId: Int, _ key: String, _ function: @escaping () -> Void) {
|
|
115
|
+
DispatchQueue.main.sync {
|
|
116
|
+
guard let (_, instance) = instances[instanceId] else { fatalError("🚨 Runtime exception: no instance found for id “\(instanceId)”") }
|
|
117
|
+
return instance.addCallback(key, function)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#import "PluginEngagementReactNative.h"
|
|
2
|
+
#import "PluginEngagementReactNative-Swift.h"
|
|
3
|
+
|
|
4
|
+
//@interface RCT_EXTERN_MODULE(AmplitudeEngagementAdapter, NSObject)
|
|
5
|
+
|
|
6
|
+
@implementation PluginEngagementReactNative {
|
|
7
|
+
AmplitudeEngagementAdapter *adapter;
|
|
8
|
+
}
|
|
9
|
+
//RCT_EXPORT_MODULE()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
- (id) init {
|
|
13
|
+
if (self = [super init]) {
|
|
14
|
+
adapter = [AmplitudeEngagementAdapter new];
|
|
15
|
+
}
|
|
16
|
+
return self;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// TODO: binding for AmplitudeEngagement here
|
|
20
|
+
|
|
21
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
22
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
23
|
+
{
|
|
24
|
+
return std::make_shared<facebook::react::NativePluginEngagementReactNativeSpecJSI>(params);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
+ (NSString *)moduleName {
|
|
28
|
+
return @"PluginEngagementReactNative";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
- (void)boot:(double)id user_id:(nonnull NSString *)user_id device_id:(nonnull NSString *)device_id {
|
|
32
|
+
[adapter boot:id :user_id device_id:device_id];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
- (nonnull NSArray<NSDictionary *> *)list:(double)id {
|
|
36
|
+
return [adapter list:id];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
- (NSNumber *)newInstance:(nonnull NSString *)apiKey {
|
|
40
|
+
return @([adapter newInstance:apiKey]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
- (NSNumber*)handleURL:(double)id url:(NSString *)url {
|
|
44
|
+
BOOL success = [adapter handleUrl:id :url];
|
|
45
|
+
return @(success);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (void)addCallback:(double)id key:(nonnull NSString *)key func:(nonnull RCTResponseSenderBlock)func {
|
|
49
|
+
RCTResponseSenderBlock jsCallback = func;
|
|
50
|
+
void (^callback)(void) = ^{
|
|
51
|
+
jsCallback(@[]); // or pass whatever args you need
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
[adapter addCallback:id :key :callback];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
- (void)closeAll:(double)id {
|
|
59
|
+
[adapter closeAll:id];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
- (void)forwardEvent:(double)id event:(nonnull NSDictionary *)event {
|
|
64
|
+
[adapter forwardEvent:id :event];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
- (void)reset:(double)id key:(nonnull NSString *)key stepIndex:(double)stepIndex {
|
|
69
|
+
[adapter reset:id key:key stepIndex:stepIndex];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
- (void)screen:(double)id screenName:(nonnull NSString *)screenName {
|
|
74
|
+
[adapter screen:id :screenName];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
- (void)setThemeMode:(double)id themeMode:(nonnull NSString *)themeMode {
|
|
79
|
+
[adapter setThemeMode:id themeMode:themeMode];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
- (void)show:(double)id key:(nonnull NSString *)key stepIndex:(double)stepIndex {
|
|
84
|
+
[adapter show:id key:key stepIndex:stepIndex];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@end
|