@binoban/react-native 1.0.0
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/BinobanReactNative.podspec +23 -0
- package/LICENSE +20 -0
- package/README.md +228 -0
- package/android/build.gradle +77 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/io/binoban/sdk/reactNative/BinobanReactNativeModule.kt +248 -0
- package/android/src/main/java/io/binoban/sdk/reactNative/BinobanReactNativePackage.kt +32 -0
- package/android/src/main/java/io/binoban/sdk/reactNative/BinobanSafe.kt +22 -0
- package/android/src/main/java/io/binoban/sdk/reactNative/utils.kt +87 -0
- package/ios/BinobanReactNative.h +7 -0
- package/ios/BinobanReactNative.mm +244 -0
- package/lib/module/NativeBinobanReactNative.js +18 -0
- package/lib/module/NativeBinobanReactNative.js.map +1 -0
- package/lib/module/binobanClient.js +143 -0
- package/lib/module/binobanClient.js.map +1 -0
- package/lib/module/constants.js +31 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/index.js +94 -0
- package/lib/module/index.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/NativeBinobanReactNative.d.ts +101 -0
- package/lib/typescript/src/NativeBinobanReactNative.d.ts.map +1 -0
- package/lib/typescript/src/binobanClient.d.ts +30 -0
- package/lib/typescript/src/binobanClient.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +3 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +53 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +173 -0
- package/react-native.config.js +8 -0
- package/src/NativeBinobanReactNative.ts +168 -0
- package/src/binobanClient.ts +199 -0
- package/src/constants.ts +30 -0
- package/src/index.tsx +105 -0
- package/src/types.ts +78 -0
|
@@ -0,0 +1,23 @@
|
|
|
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 = "BinobanReactNative"
|
|
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://github.com/binoban/binoban-example-react-native.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.exclude_files = "ios/build/**"
|
|
18
|
+
s.private_header_files = "ios/**/*.h"
|
|
19
|
+
|
|
20
|
+
s.dependency "binoban", "~> 1.0.0"
|
|
21
|
+
|
|
22
|
+
install_modules_dependencies(s)
|
|
23
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 farhan
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# @binoban/react-native
|
|
2
|
+
|
|
3
|
+
React Native SDK for Binoban. A TurboModule bridge to the [Binoban KMP SDK](../kotlin/) — Android delegates to `io.binoban.sdk:sdk-android`, iOS delegates to the `binoban` XCFramework via CocoaPods.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @binoban/react-native
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Compatibility
|
|
12
|
+
|
|
13
|
+
| Surface | Supported |
|
|
14
|
+
|---|---|
|
|
15
|
+
| React Native | >= 0.71. **New Architecture (TurboModules) recommended.** On the old architecture the native module resolves to `null`, so the SDK becomes an inert no-op rather than crashing. |
|
|
16
|
+
| React | >= 18 |
|
|
17
|
+
| Android | `minSdkVersion 24`, `compileSdkVersion 36`, `targetSdkVersion 36` |
|
|
18
|
+
| iOS | deployment target = React Native's `min_ios_version_supported` |
|
|
19
|
+
| Node (development) | >= 20 |
|
|
20
|
+
|
|
21
|
+
### Safety contract
|
|
22
|
+
|
|
23
|
+
Every bridge call is wrapped so it can never crash or throw into the host app —
|
|
24
|
+
on any device or OS version. Failures are swallowed and logged only when
|
|
25
|
+
`debug: true`. To observe failures, pass an `onError` callback:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const client = createClient({
|
|
29
|
+
apiHost: 'stage-api.binoban.io',
|
|
30
|
+
credentials: { android: { /* ... */ }, iOS: { /* ... */ } },
|
|
31
|
+
onError: (e) => console.warn('Binoban:', e.method, e.message),
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`onError` receives both JS-side failures and errors the native SDK reports (for
|
|
36
|
+
those, `e.method` is `"native"` or `"native:<ThrowableClass>"`) — e.g. a disabled
|
|
37
|
+
instance from invalid config.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { createClient, BinobanProvider, useBinoban } from '@binoban/react-native';
|
|
43
|
+
|
|
44
|
+
const client = createClient({
|
|
45
|
+
credentials: {
|
|
46
|
+
android: { apiKey: 'YOUR_ANDROID_KEY', sourceIdentifier: 'YOUR_ANDROID_SOURCE' },
|
|
47
|
+
iOS: { apiKey: 'YOUR_IOS_KEY', sourceIdentifier: 'YOUR_IOS_SOURCE' },
|
|
48
|
+
},
|
|
49
|
+
apiHost: 'your-api-host.example.com',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Wrap your app
|
|
53
|
+
<BinobanProvider client={client}>
|
|
54
|
+
<App />
|
|
55
|
+
</BinobanProvider>
|
|
56
|
+
|
|
57
|
+
// Inside a component
|
|
58
|
+
const { track, identify, flush, reset, anonymousId, userId, deviceId, notify, setDeviceToken } = useBinoban();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> **Not yet supported:** `screen()`, `group()`, and `alias()` are not exposed
|
|
62
|
+
> through this bridge yet. `track()`, `identify()`, `flush()`, `reset()`,
|
|
63
|
+
> `anonymousId()`, `userId()`, `deviceId()`, `notify()`, and `setDeviceToken()`
|
|
64
|
+
> are all available, plus the notification-interaction API below.
|
|
65
|
+
|
|
66
|
+
### Notification interactions
|
|
67
|
+
|
|
68
|
+
Subscribe to taps (body and per-action-button), dismissals, and delivery. Each
|
|
69
|
+
interaction carries `{ notificationUuid, type, actionId, uri, customData, reason }`,
|
|
70
|
+
where `type` is one of `DELIVERED | CLICKED | CLOSED | FAILED | OPENED`. Backend
|
|
71
|
+
tracking is preserved automatically — the bridge extends the SDK's default handler.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const { onNotificationInteraction, getInitialNotificationInteraction } = useBinoban();
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
// Cold start: a tap that launched the app before JS subscribed. Call once.
|
|
78
|
+
getInitialNotificationInteraction().then((i) => {
|
|
79
|
+
if (i?.uri) navigateTo(i.uri);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Live interactions while the app runs.
|
|
83
|
+
const sub = onNotificationInteraction((i) => {
|
|
84
|
+
if (i.type === 'CLICKED' && i.uri) navigateTo(i.uri);
|
|
85
|
+
// i.customData is the developer-supplied string→string map from the push.
|
|
86
|
+
});
|
|
87
|
+
return () => sub.remove();
|
|
88
|
+
}, []);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Note:** the SDK does not open URLs itself on iOS — read `interaction.uri` and route
|
|
92
|
+
it in your handler.
|
|
93
|
+
|
|
94
|
+
**Platform wiring:**
|
|
95
|
+
|
|
96
|
+
- **Android** — nothing extra. The SDK's notification trampoline delivers interactions,
|
|
97
|
+
so they reach JS automatically once you subscribe.
|
|
98
|
+
- **iOS** — the SDK never registers its own `UNUserNotificationCenterDelegate`, so the host
|
|
99
|
+
must forward its notification callbacks. Either:
|
|
100
|
+
- **From JS** (e.g. when a JS push library gives you the tap): call
|
|
101
|
+
`didReceiveNotificationResponse(userInfo, actionId, dismissed)`,
|
|
102
|
+
`willPresentNotification(userInfo)`, or `didReceiveRemoteNotification(userInfo)` on the
|
|
103
|
+
client. These are no-ops on Android.
|
|
104
|
+
- **From the native AppDelegate** — forward straight to the SDK's KMP functions
|
|
105
|
+
(`onDidReceiveForwarded`, `onWillPresentForwarded`,
|
|
106
|
+
`onApplicationDidReceiveRemoteNotification`, `onNewToken`); the bridge's installed
|
|
107
|
+
handler still emits the interaction to JS.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
### Prerequisites
|
|
114
|
+
|
|
115
|
+
- Node >= 20 (`nvm use` to switch automatically)
|
|
116
|
+
- Yarn 4 (`corepack enable` if needed)
|
|
117
|
+
- **iOS only:** Ruby via `rbenv` or `rvm`, CocoaPods managed through Bundler
|
|
118
|
+
|
|
119
|
+
### First-time setup
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
# 1. Install JS dependencies
|
|
123
|
+
yarn
|
|
124
|
+
|
|
125
|
+
# 2. iOS only — install the Bundler-locked version of CocoaPods and run pod install
|
|
126
|
+
cd example/ios
|
|
127
|
+
bundle install
|
|
128
|
+
bundle exec pod install
|
|
129
|
+
cd ../..
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
> Always use `bundle exec pod install` (not bare `pod install`) to use the exact CocoaPods version from the Gemfile.lock.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Running the example app
|
|
137
|
+
|
|
138
|
+
### Android
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
yarn example android
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### iOS
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
yarn example ios
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`automaticPodsInstallation` is enabled in `example/react-native.config.js`, so `yarn example ios` will trigger `pod install` automatically when Pods are out of date. If it doesn't pick up your changes, run manually:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
cd example/ios && bundle exec pod install && cd ../..
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
To start Metro separately (useful for attach/reload without rebuilding):
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
yarn example start
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## What to do after common changes
|
|
165
|
+
|
|
166
|
+
### Changing `src/NativeBinobanReactNative.ts` (the TurboModule spec)
|
|
167
|
+
|
|
168
|
+
The spec drives ObjC++ codegen on iOS and Kotlin codegen on Android.
|
|
169
|
+
|
|
170
|
+
**iOS** — regenerate codegen by running pod install:
|
|
171
|
+
```sh
|
|
172
|
+
cd example/ios && bundle exec pod install && cd ../..
|
|
173
|
+
```
|
|
174
|
+
The generated files land in `example/ios/build/generated/ios/ReactCodegen/BinobanReactNativeSpec/`. Then rebuild the app normally with `yarn example ios`.
|
|
175
|
+
|
|
176
|
+
**Android** — no manual step. The Android Gradle plugin regenerates the spec automatically during the next build (`yarn example android`).
|
|
177
|
+
|
|
178
|
+
### Changing native code (`ios/*.mm` / `android/src/**/*.kt`)
|
|
179
|
+
|
|
180
|
+
A full native rebuild is required. Just run:
|
|
181
|
+
```sh
|
|
182
|
+
yarn example ios # or android
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Metro hot-reload does **not** pick up native changes.
|
|
186
|
+
|
|
187
|
+
### Changing JS/TS source only (`src/*.ts`, `src/*.tsx`)
|
|
188
|
+
|
|
189
|
+
Metro picks these up automatically — no rebuild needed. If the example app is already running, save the file and Metro will hot-reload.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Troubleshooting
|
|
194
|
+
|
|
195
|
+
### iOS: `fmt` fails to compile — "call to consteval function … is not a constant expression"
|
|
196
|
+
|
|
197
|
+
React Native 0.83 pulls in `fmt` 11.0.2, which does not compile under Xcode 16.3+ / 26 because of stricter `consteval` handling in the newer Clang. This is a React Native toolchain issue, not a binoban one — it affects any RN 0.83 project on these Xcode versions.
|
|
198
|
+
|
|
199
|
+
The fix is already wired into `example/ios/Podfile`: its `post_install` hook patches `Pods/fmt/include/fmt/base.h` to disable `fmt`'s `consteval` path (compile-time format-string checking off; runtime formatting unchanged). It re-applies automatically on every `pod install`. If you still hit the error, re-run `pod install` and confirm you see `Patched fmt base.h …` in the output, then clean the build folder (Xcode → Product → Clean Build Folder) and rebuild.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Opening native code in an IDE
|
|
204
|
+
|
|
205
|
+
**Xcode (iOS):**
|
|
206
|
+
Open `example/ios/BinobanReactNativeExample.xcworkspace` (the `.xcworkspace`, not `.xcodeproj`). The bridge source files are under `Pods > Development Pods > binoban-react-native`.
|
|
207
|
+
|
|
208
|
+
**Android Studio:**
|
|
209
|
+
Open `example/android`. The bridge source files appear under `binoban-react-native` in the project tree.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Other commands
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
yarn typecheck # TypeScript type check
|
|
217
|
+
yarn lint # ESLint
|
|
218
|
+
yarn lint --fix # Auto-fix lint issues
|
|
219
|
+
yarn test # Jest unit tests
|
|
220
|
+
yarn prepare # Build the library (outputs to lib/)
|
|
221
|
+
yarn release # Publish to npm (bumps version, tags, creates GitHub release)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.BinobanReactNative = [
|
|
3
|
+
kotlinVersion: "2.1.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
targetSdkVersion: 36
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
ext.getExtOrDefault = { prop ->
|
|
10
|
+
if (rootProject.ext.has(prop)) {
|
|
11
|
+
return rootProject.ext.get(prop)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return BinobanReactNative[prop]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
repositories {
|
|
18
|
+
mavenLocal()
|
|
19
|
+
google()
|
|
20
|
+
mavenCentral()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
dependencies {
|
|
24
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
25
|
+
// noinspection DifferentKotlinGradleVersion
|
|
26
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
apply plugin: "com.android.library"
|
|
32
|
+
apply plugin: "kotlin-android"
|
|
33
|
+
|
|
34
|
+
apply plugin: "com.facebook.react"
|
|
35
|
+
|
|
36
|
+
repositories {
|
|
37
|
+
mavenLocal()
|
|
38
|
+
google()
|
|
39
|
+
mavenCentral()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
android {
|
|
43
|
+
namespace "io.binoban.sdk.reactNative"
|
|
44
|
+
|
|
45
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
46
|
+
|
|
47
|
+
defaultConfig {
|
|
48
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
49
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
buildFeatures {
|
|
53
|
+
buildConfig true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
buildTypes {
|
|
57
|
+
release {
|
|
58
|
+
minifyEnabled false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
lint {
|
|
63
|
+
disable "GradleCompatible"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
compileOptions {
|
|
67
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
68
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
dependencies {
|
|
73
|
+
implementation "com.facebook.react:react-android"
|
|
74
|
+
implementation "io.binoban.sdk:sdk-android:1.0.1"
|
|
75
|
+
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1"
|
|
76
|
+
testImplementation "junit:junit:4.13.2"
|
|
77
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
package io.binoban.sdk.reactNative
|
|
2
|
+
|
|
3
|
+
import android.content.pm.PackageManager
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import com.facebook.react.bridge.Arguments
|
|
6
|
+
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.bridge.ReadableMap
|
|
9
|
+
import com.facebook.react.bridge.ReadableMapKeySetIterator
|
|
10
|
+
import com.facebook.react.bridge.ReadableType
|
|
11
|
+
import com.facebook.react.bridge.WritableMap
|
|
12
|
+
import io.binoban.sdk.core.Binoban
|
|
13
|
+
import io.binoban.sdk.core.Notification
|
|
14
|
+
import io.binoban.sdk.core.platform.notifier.notification.DefaultNotificationInteractionHandler
|
|
15
|
+
import io.binoban.sdk.core.platform.notifier.notification.NotificationInteraction
|
|
16
|
+
import io.binoban.sdk.core.platform.notifier.notification.NotificationInteractionManager
|
|
17
|
+
import io.binoban.sdk.core.platform.notifier.notification.NotificationInteractionType
|
|
18
|
+
import io.binoban.sdk.core.platform.notifier.notification.configuration.NotificationPlatformConfiguration
|
|
19
|
+
import io.binoban.sdk.core.platform.plugins.setDeviceToken
|
|
20
|
+
import io.binoban.sdk.core.platform.policies.CountBasedFlushPolicy
|
|
21
|
+
import io.binoban.sdk.core.platform.policies.FrequencyFlushPolicy
|
|
22
|
+
import kotlinx.serialization.json.JsonObject
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BinobanReactNativeModule(reactContext: ReactApplicationContext) :
|
|
26
|
+
NativeBinobanReactNativeSpec(reactContext) {
|
|
27
|
+
|
|
28
|
+
lateinit var binoban: Binoban
|
|
29
|
+
var initializing = false
|
|
30
|
+
var initialized = false
|
|
31
|
+
|
|
32
|
+
// Holds a cold-start interaction (a tap that arrived before JS subscribed) until
|
|
33
|
+
// getInitialNotificationInteraction() consumes it. Only one is retained.
|
|
34
|
+
private var initialInteraction: NotificationInteraction? = null
|
|
35
|
+
|
|
36
|
+
override fun setup(config: ReadableMap): Boolean {
|
|
37
|
+
if (initialized || initializing) return false
|
|
38
|
+
initializing = true
|
|
39
|
+
val ok = BinobanSafe.safe(false) {
|
|
40
|
+
BinobanSafe.debug = config.getBoolean("debug") == true
|
|
41
|
+
val module = this@BinobanReactNativeModule
|
|
42
|
+
binoban = Binoban(
|
|
43
|
+
config.getString("apiKey").toString(),
|
|
44
|
+
config.getString("sourceIdentifier").toString()
|
|
45
|
+
) {
|
|
46
|
+
// Route the KMP SDK's internal errors to JS (createClient's onError).
|
|
47
|
+
this.errorHandler = { t -> module.emitNativeError(t) }
|
|
48
|
+
this.collectDeviceId = config.getBoolean("collectDeviceId") == true
|
|
49
|
+
this.trackApplicationLifecycleEvents = config.getBoolean("trackAppLifecycleEvents") == true
|
|
50
|
+
this.useLifecycleObserver =
|
|
51
|
+
if (config.hasKey("useLifecycleObserver")) config.getBoolean("useLifecycleObserver") else true
|
|
52
|
+
this.trackDeepLinks = config.getBoolean("trackDeepLinks") == true
|
|
53
|
+
this.apiHost = config.getString("apiHost").toString()
|
|
54
|
+
this.autoAddSegmentDestination =
|
|
55
|
+
if (config.hasKey("autoAddSegmentDestination")) config.getBoolean("autoAddSegmentDestination") else true
|
|
56
|
+
// Telemetry (var since sdk 1.0.0 — now at parity with iOS)
|
|
57
|
+
this.disableTelemetry = config.getBoolean("disableTelemetry") == true
|
|
58
|
+
config.getString("telemetryHost")?.takeIf { it.isNotEmpty() }?.let { this.telemetryHost = it }
|
|
59
|
+
// Upload resilience tunables (sdk 1.0.0). Fall back to KMP defaults when a key is absent.
|
|
60
|
+
if (config.hasKey("uploadMaxRetries")) this.uploadMaxRetries = config.getInt("uploadMaxRetries")
|
|
61
|
+
if (config.hasKey("backoffBaseMillis")) this.backoffBaseMillis = config.getDouble("backoffBaseMillis").toLong()
|
|
62
|
+
if (config.hasKey("backoffFactor")) this.backoffFactor = config.getDouble("backoffFactor")
|
|
63
|
+
if (config.hasKey("backoffMaxMillis")) this.backoffMaxMillis = config.getDouble("backoffMaxMillis").toLong()
|
|
64
|
+
if (config.hasKey("maxFilesPerFlush")) this.maxFilesPerFlush = config.getInt("maxFilesPerFlush")
|
|
65
|
+
this.flushPolicies = listOf(
|
|
66
|
+
CountBasedFlushPolicy(config.getInt("flushAt")),
|
|
67
|
+
FrequencyFlushPolicy(config.getInt("flushInterval").toLong() * 1000L),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
Binoban.debugLogsEnabled = BinobanSafe.debug
|
|
71
|
+
installInteractionHandler()
|
|
72
|
+
true
|
|
73
|
+
}
|
|
74
|
+
initialized = ok
|
|
75
|
+
initializing = false
|
|
76
|
+
return ok
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Install a handler that preserves the SDK's default backend tracking (via super)
|
|
80
|
+
// and additionally forwards each interaction to JS. The trampoline delivers taps
|
|
81
|
+
// on its own, so no host wiring is needed on Android.
|
|
82
|
+
private fun installInteractionHandler() {
|
|
83
|
+
NotificationInteractionManager.setHandler(object : DefaultNotificationInteractionHandler() {
|
|
84
|
+
override fun onNotificationInteraction(interaction: NotificationInteraction) {
|
|
85
|
+
super.onNotificationInteraction(interaction)
|
|
86
|
+
BinobanSafe.safe(Unit) {
|
|
87
|
+
initialInteraction = interaction
|
|
88
|
+
emitOnNotificationInteraction(interaction.toWritableMap())
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
// Recover a cold-start tap the notification trampoline delivered during process
|
|
93
|
+
// startup, before this handler was installed (process start -> trampoline -> setup).
|
|
94
|
+
// Tracking already happened via the default handler; surface it to JS only through
|
|
95
|
+
// getInitialNotificationInteraction(). Keep the most recent actual tap.
|
|
96
|
+
BinobanSafe.safe(Unit) {
|
|
97
|
+
NotificationInteractionManager.drainPendingInteractions()
|
|
98
|
+
.lastOrNull {
|
|
99
|
+
it.type == NotificationInteractionType.CLICKED ||
|
|
100
|
+
it.type == NotificationInteractionType.OPENED
|
|
101
|
+
}
|
|
102
|
+
?.let { initialInteraction = it }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun emitNativeError(t: Throwable) {
|
|
107
|
+
BinobanSafe.safe(Unit) {
|
|
108
|
+
val map = Arguments.createMap()
|
|
109
|
+
map.putString("message", t.message ?: t.toString())
|
|
110
|
+
map.putString("name", t::class.simpleName)
|
|
111
|
+
emitOnNativeError(map)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private fun NotificationInteraction.toWritableMap(): WritableMap {
|
|
116
|
+
val map = Arguments.createMap()
|
|
117
|
+
map.putString("notificationUuid", notificationUuid)
|
|
118
|
+
map.putString("type", type.name)
|
|
119
|
+
map.putString("actionId", actionId)
|
|
120
|
+
map.putString("uri", uri)
|
|
121
|
+
map.putString("reason", reason)
|
|
122
|
+
val cd = customData
|
|
123
|
+
if (cd != null) {
|
|
124
|
+
val cdMap = Arguments.createMap()
|
|
125
|
+
cd.forEach { (k, v) -> cdMap.putString(k, v) }
|
|
126
|
+
map.putMap("customData", cdMap)
|
|
127
|
+
} else {
|
|
128
|
+
map.putNull("customData")
|
|
129
|
+
}
|
|
130
|
+
return map
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
override fun track(eventName: String?, properties: ReadableMap?) {
|
|
134
|
+
BinobanSafe.safe(Unit) {
|
|
135
|
+
if (!initialized || eventName == null) return@safe
|
|
136
|
+
binoban.track(eventName, properties?.toJsonObject() ?: JsonObject(emptyMap()))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
override fun identify(userId: String?, userTraits: ReadableMap?) {
|
|
141
|
+
BinobanSafe.safe(Unit) {
|
|
142
|
+
if (!initialized || userId == null) return@safe
|
|
143
|
+
binoban.identify(userId, userTraits?.toJsonObject() ?: JsonObject(emptyMap()))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override fun flush() {
|
|
148
|
+
BinobanSafe.safe(Unit) { if (initialized) binoban.flush() }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override fun reset() {
|
|
152
|
+
BinobanSafe.safe(Unit) { if (initialized) binoban.reset() }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override fun anonymousId(): String =
|
|
156
|
+
BinobanSafe.safe("") { if (initialized) binoban.anonymousId() else "" }
|
|
157
|
+
|
|
158
|
+
override fun userId(): String? =
|
|
159
|
+
BinobanSafe.safe<String?>(null) { if (initialized) binoban.userId() else null }
|
|
160
|
+
|
|
161
|
+
override fun deviceId(): String? =
|
|
162
|
+
BinobanSafe.safe<String?>(null) { if (initialized) binoban.deviceId() else null }
|
|
163
|
+
|
|
164
|
+
override fun setDeviceToken(token: String) {
|
|
165
|
+
BinobanSafe.safe(Unit) { if (initialized) binoban.setDeviceToken(token) }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override fun notify(payloadData: ReadableMap) {
|
|
169
|
+
BinobanSafe.safe(Unit) {
|
|
170
|
+
if (payloadData.getString("source") != "binoban") return@safe
|
|
171
|
+
|
|
172
|
+
val remoteMessage: MutableMap<String, String> = HashMap()
|
|
173
|
+
val iterator: ReadableMapKeySetIterator = payloadData.keySetIterator()
|
|
174
|
+
while (iterator.hasNextKey()) {
|
|
175
|
+
val key = iterator.nextKey()
|
|
176
|
+
val type: ReadableType = payloadData.getType(key)
|
|
177
|
+
if (type == ReadableType.String) {
|
|
178
|
+
payloadData.getString(key)?.let { remoteMessage.put(key, it) }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
var icon: Int = reactApplicationContext.applicationInfo.icon
|
|
183
|
+
var channelId = "DEFAULT_NOTIFICATION_CHANNEL_ID"
|
|
184
|
+
var channelName = "General"
|
|
185
|
+
var channelDescription = ""
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
val app = reactApplicationContext.packageManager
|
|
189
|
+
.getApplicationInfo(reactApplicationContext.packageName, PackageManager.GET_META_DATA)
|
|
190
|
+
val bundle = app.metaData
|
|
191
|
+
icon = bundle.getInt(BINOBAN_PUSH_DEFAULT_ICON, reactApplicationContext.applicationInfo.icon)
|
|
192
|
+
channelId = bundle.getString(BINOBAN_PUSH_CHANNEL_ID, channelId)
|
|
193
|
+
channelName = bundle.getString(BINOBAN_PUSH_CHANNEL_NAME, channelName)
|
|
194
|
+
channelDescription = bundle.getString(BINOBAN_PUSH_CHANNEL_DESCRIPTION, channelDescription)
|
|
195
|
+
} catch (e: PackageManager.NameNotFoundException) {
|
|
196
|
+
if (BinobanSafe.debug) Log.w("Binoban", "notification meta-data lookup failed", e)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
Notification.initialize(
|
|
201
|
+
NotificationPlatformConfiguration.Android(
|
|
202
|
+
notificationIconResId = icon,
|
|
203
|
+
notificationChannelData = NotificationPlatformConfiguration.Android.NotificationChannelData(
|
|
204
|
+
channelId,
|
|
205
|
+
channelName,
|
|
206
|
+
channelDescription
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
Notification.notify(remoteMessage)
|
|
211
|
+
} catch (_: Exception) {}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
override fun getInitialNotificationInteraction(promise: Promise) {
|
|
216
|
+
BinobanSafe.safe(Unit) {
|
|
217
|
+
val interaction = initialInteraction
|
|
218
|
+
initialInteraction = null
|
|
219
|
+
promise.resolve(interaction?.toWritableMap())
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// iOS-only host forwarding. On Android the trampoline delivers interactions
|
|
224
|
+
// directly, so these are no-ops kept for a single cross-platform JS surface.
|
|
225
|
+
override fun didReceiveNotificationResponse(
|
|
226
|
+
userInfo: ReadableMap,
|
|
227
|
+
actionId: String?,
|
|
228
|
+
dismissed: Boolean?
|
|
229
|
+
) {
|
|
230
|
+
// no-op on Android
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
override fun willPresentNotification(userInfo: ReadableMap) {
|
|
234
|
+
// no-op on Android
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
override fun didReceiveRemoteNotification(userInfo: ReadableMap) {
|
|
238
|
+
// no-op on Android
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
override fun getName(): String {
|
|
242
|
+
return NAME
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
companion object {
|
|
246
|
+
const val NAME = "BinobanReactNative"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package io.binoban.sdk.reactNative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.TurboReactPackage
|
|
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 BinobanReactNativePackage : TurboReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == BinobanReactNativeModule.NAME) {
|
|
13
|
+
BinobanReactNativeModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
|
20
|
+
mapOf(
|
|
21
|
+
BinobanReactNativeModule.NAME to ReactModuleInfo(
|
|
22
|
+
BinobanReactNativeModule.NAME,
|
|
23
|
+
BinobanReactNativeModule::class.java.name,
|
|
24
|
+
false,
|
|
25
|
+
false,
|
|
26
|
+
true,
|
|
27
|
+
false,
|
|
28
|
+
true
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package io.binoban.sdk.reactNative
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Central safety wrapper for every bridge call. Never lets a Throwable escape
|
|
7
|
+
* to React Native / the host app; logs only when [debug] is enabled.
|
|
8
|
+
*/
|
|
9
|
+
object BinobanSafe {
|
|
10
|
+
private const val TAG = "Binoban"
|
|
11
|
+
|
|
12
|
+
@Volatile
|
|
13
|
+
var debug: Boolean = false
|
|
14
|
+
|
|
15
|
+
fun <T> safe(fallback: T, block: () -> T): T =
|
|
16
|
+
try {
|
|
17
|
+
block()
|
|
18
|
+
} catch (e: Throwable) {
|
|
19
|
+
if (debug) Log.w(TAG, "binoban bridge call failed", e)
|
|
20
|
+
fallback
|
|
21
|
+
}
|
|
22
|
+
}
|