@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.
Files changed (40) hide show
  1. package/BinobanReactNative.podspec +23 -0
  2. package/LICENSE +20 -0
  3. package/README.md +228 -0
  4. package/android/build.gradle +77 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/io/binoban/sdk/reactNative/BinobanReactNativeModule.kt +248 -0
  7. package/android/src/main/java/io/binoban/sdk/reactNative/BinobanReactNativePackage.kt +32 -0
  8. package/android/src/main/java/io/binoban/sdk/reactNative/BinobanSafe.kt +22 -0
  9. package/android/src/main/java/io/binoban/sdk/reactNative/utils.kt +87 -0
  10. package/ios/BinobanReactNative.h +7 -0
  11. package/ios/BinobanReactNative.mm +244 -0
  12. package/lib/module/NativeBinobanReactNative.js +18 -0
  13. package/lib/module/NativeBinobanReactNative.js.map +1 -0
  14. package/lib/module/binobanClient.js +143 -0
  15. package/lib/module/binobanClient.js.map +1 -0
  16. package/lib/module/constants.js +31 -0
  17. package/lib/module/constants.js.map +1 -0
  18. package/lib/module/index.js +94 -0
  19. package/lib/module/index.js.map +1 -0
  20. package/lib/module/package.json +1 -0
  21. package/lib/module/types.js +2 -0
  22. package/lib/module/types.js.map +1 -0
  23. package/lib/typescript/package.json +1 -0
  24. package/lib/typescript/src/NativeBinobanReactNative.d.ts +101 -0
  25. package/lib/typescript/src/NativeBinobanReactNative.d.ts.map +1 -0
  26. package/lib/typescript/src/binobanClient.d.ts +30 -0
  27. package/lib/typescript/src/binobanClient.d.ts.map +1 -0
  28. package/lib/typescript/src/constants.d.ts +3 -0
  29. package/lib/typescript/src/constants.d.ts.map +1 -0
  30. package/lib/typescript/src/index.d.ts +11 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -0
  32. package/lib/typescript/src/types.d.ts +53 -0
  33. package/lib/typescript/src/types.d.ts.map +1 -0
  34. package/package.json +173 -0
  35. package/react-native.config.js +8 -0
  36. package/src/NativeBinobanReactNative.ts +168 -0
  37. package/src/binobanClient.ts +199 -0
  38. package/src/constants.ts +30 -0
  39. package/src/index.tsx +105 -0
  40. 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,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -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
+ }