@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,199 @@
1
+ import BinobanReactNative, {
2
+ type InternalConfig,
3
+ type NotificationInteractionEvent,
4
+ type NativeErrorEvent,
5
+ } from './NativeBinobanReactNative';
6
+ import type {
7
+ UserTraits,
8
+ BinobanError,
9
+ JsonMap,
10
+ NotificationInteraction,
11
+ NotificationInteractionType,
12
+ NotificationInteractionSubscription,
13
+ } from './types';
14
+
15
+ type OnError = (error: BinobanError) => void;
16
+
17
+ const noopSubscription: NotificationInteractionSubscription = {
18
+ remove: () => {},
19
+ };
20
+
21
+ // Native event → public shape. customData arrives as an untyped map (UnsafeObject);
22
+ // normalize it to a string→string record and leave nullables as-is.
23
+ const toInteraction = (
24
+ e: NotificationInteractionEvent
25
+ ): NotificationInteraction => ({
26
+ notificationUuid: e.notificationUuid,
27
+ type: e.type as NotificationInteractionType,
28
+ actionId: e.actionId ?? null,
29
+ uri: e.uri ?? null,
30
+ customData: (e.customData as Record<string, string> | null) ?? null,
31
+ reason: e.reason ?? null,
32
+ });
33
+
34
+ export class BinobanClient {
35
+ private config: InternalConfig;
36
+ private onError?: OnError;
37
+
38
+ constructor({
39
+ config,
40
+ onError,
41
+ }: {
42
+ config: InternalConfig;
43
+ onError?: OnError;
44
+ }) {
45
+ this.config = config;
46
+ this.onError = onError;
47
+ }
48
+
49
+ private safe<T>(method: string, fn: () => T, fallback?: T): T | undefined {
50
+ try {
51
+ return fn();
52
+ } catch (e) {
53
+ this.onError?.({
54
+ method,
55
+ message: e instanceof Error ? e.message : String(e),
56
+ cause: e,
57
+ });
58
+ if (this.config.debug) {
59
+ console.warn(`[binoban] ${method}() failed`, e);
60
+ }
61
+ return fallback;
62
+ }
63
+ }
64
+
65
+ async init() {
66
+ return this.safe(
67
+ 'init',
68
+ () => {
69
+ if (!BinobanReactNative) return true;
70
+ // Subscribe before setup() so an error the native SDK reports during
71
+ // initialization (e.g. a disabled instance from invalid config) is caught.
72
+ this.subscribeToNativeErrors();
73
+ return BinobanReactNative.setup(this.config);
74
+ },
75
+ false
76
+ );
77
+ }
78
+
79
+ // Bridge the KMP Configuration.errorHandler to the onError callback. Only wired
80
+ // when an onError was provided — otherwise there is nothing to route errors to.
81
+ private subscribeToNativeErrors() {
82
+ if (!BinobanReactNative || !this.onError) return;
83
+ this.safe('subscribeToNativeErrors', () => {
84
+ BinobanReactNative?.onNativeError((e: NativeErrorEvent) => {
85
+ this.onError?.({
86
+ method: e.name ? `native:${e.name}` : 'native',
87
+ message: e.message,
88
+ });
89
+ });
90
+ });
91
+ }
92
+
93
+ async track(eventName: string, properties?: Object) {
94
+ this.safe('track', () => {
95
+ if (BinobanReactNative) BinobanReactNative.track(eventName, properties);
96
+ });
97
+ }
98
+
99
+ async identify(userId: string, userTraits?: UserTraits) {
100
+ this.safe('identify', () => {
101
+ if (BinobanReactNative) BinobanReactNative.identify(userId, userTraits);
102
+ });
103
+ }
104
+
105
+ async flush() {
106
+ this.safe('flush', () => {
107
+ if (BinobanReactNative) BinobanReactNative.flush();
108
+ });
109
+ }
110
+
111
+ async reset() {
112
+ this.safe('reset', () => {
113
+ if (BinobanReactNative) BinobanReactNative.reset();
114
+ });
115
+ }
116
+
117
+ anonymousId() {
118
+ return this.safe('anonymousId', () => BinobanReactNative?.anonymousId());
119
+ }
120
+
121
+ userId() {
122
+ return this.safe('userId', () => BinobanReactNative?.userId());
123
+ }
124
+
125
+ deviceId() {
126
+ return this.safe('deviceId', () => BinobanReactNative?.deviceId());
127
+ }
128
+
129
+ notify(payloadData: Object) {
130
+ this.safe('notify', () => {
131
+ if (BinobanReactNative) BinobanReactNative.notify(payloadData);
132
+ });
133
+ }
134
+
135
+ setDeviceToken(token: string) {
136
+ this.safe('setDeviceToken', () => {
137
+ if (BinobanReactNative) BinobanReactNative.setDeviceToken(token);
138
+ });
139
+ }
140
+
141
+ // Subscribe to notification interactions (body/action-button taps, dismissals,
142
+ // delivery). Returns a subscription; call remove() to stop listening.
143
+ onNotificationInteraction(
144
+ listener: (interaction: NotificationInteraction) => void
145
+ ): NotificationInteractionSubscription {
146
+ return (
147
+ this.safe('onNotificationInteraction', () => {
148
+ if (!BinobanReactNative) return noopSubscription;
149
+ return BinobanReactNative.onNotificationInteraction(
150
+ (e: NotificationInteractionEvent) => {
151
+ this.safe('onNotificationInteraction:listener', () =>
152
+ listener(toInteraction(e))
153
+ );
154
+ }
155
+ );
156
+ }) ?? noopSubscription
157
+ );
158
+ }
159
+
160
+ // The interaction that cold-started the app (a tap delivered before JS could
161
+ // subscribe), consumed once. Resolves null when there is none.
162
+ async getInitialNotificationInteraction(): Promise<NotificationInteraction | null> {
163
+ return (
164
+ this.safe('getInitialNotificationInteraction', async () => {
165
+ const e = await BinobanReactNative?.getInitialNotificationInteraction();
166
+ return e ? toInteraction(e) : null;
167
+ }) ?? Promise.resolve(null)
168
+ );
169
+ }
170
+
171
+ // iOS only — forward UNUserNotificationCenter callbacks from the host (a native
172
+ // AppDelegate or a JS push library). No-ops on Android, where the SDK's
173
+ // notification trampoline delivers interactions on its own.
174
+ didReceiveNotificationResponse(
175
+ userInfo: JsonMap,
176
+ actionId?: string | null,
177
+ dismissed?: boolean
178
+ ) {
179
+ this.safe('didReceiveNotificationResponse', () => {
180
+ BinobanReactNative?.didReceiveNotificationResponse(
181
+ userInfo,
182
+ actionId ?? null,
183
+ dismissed ?? false
184
+ );
185
+ });
186
+ }
187
+
188
+ willPresentNotification(userInfo: JsonMap) {
189
+ this.safe('willPresentNotification', () => {
190
+ BinobanReactNative?.willPresentNotification(userInfo);
191
+ });
192
+ }
193
+
194
+ didReceiveRemoteNotification(userInfo: JsonMap) {
195
+ this.safe('didReceiveRemoteNotification', () => {
196
+ BinobanReactNative?.didReceiveRemoteNotification(userInfo);
197
+ });
198
+ }
199
+ }
@@ -0,0 +1,30 @@
1
+ import type { Config } from './NativeBinobanReactNative';
2
+
3
+ export const defaultConfig: Config = {
4
+ credentials: {
5
+ android: {
6
+ apiKey: '',
7
+ sourceIdentifier: '',
8
+ },
9
+ iOS: {
10
+ apiKey: '',
11
+ sourceIdentifier: '',
12
+ },
13
+ },
14
+ apiHost: '',
15
+ debug: false,
16
+ collectDeviceId: true,
17
+ trackAppLifecycleEvents: true,
18
+ useLifecycleObserver: true,
19
+ trackDeepLinks: true,
20
+ flushAt: 20,
21
+ flushInterval: 30,
22
+ autoAddSegmentDestination: true,
23
+ disableTelemetry: false,
24
+ telemetryHost: '',
25
+ uploadMaxRetries: 3,
26
+ backoffBaseMillis: 1000,
27
+ backoffFactor: 2.0,
28
+ backoffMaxMillis: 60000,
29
+ maxFilesPerFlush: 20,
30
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,105 @@
1
+ import React, { createContext, useContext } from 'react';
2
+ import type { Config, InternalConfig } from './NativeBinobanReactNative';
3
+ import type { ClientMethods } from './types';
4
+ import { defaultConfig } from './constants';
5
+ import { BinobanClient } from './binobanClient';
6
+ import { Platform } from 'react-native';
7
+
8
+ export const createClient = (config: Config) => {
9
+ const clientConfig = { ...defaultConfig, ...config };
10
+ const onError = config.onError;
11
+
12
+ const platformCreds =
13
+ Platform.OS === 'android'
14
+ ? clientConfig.credentials?.android
15
+ : clientConfig.credentials?.iOS;
16
+
17
+ const internalConfig: InternalConfig = {
18
+ apiHost: clientConfig.apiHost,
19
+ debug: clientConfig.debug,
20
+ collectDeviceId: clientConfig.collectDeviceId,
21
+ trackAppLifecycleEvents: clientConfig.trackAppLifecycleEvents,
22
+ useLifecycleObserver: clientConfig.useLifecycleObserver,
23
+ trackDeepLinks: clientConfig.trackDeepLinks,
24
+ flushAt: clientConfig.flushAt,
25
+ flushInterval: clientConfig.flushInterval,
26
+ autoAddSegmentDestination: clientConfig.autoAddSegmentDestination,
27
+ disableTelemetry: clientConfig.disableTelemetry,
28
+ telemetryHost: clientConfig.telemetryHost,
29
+ uploadMaxRetries: clientConfig.uploadMaxRetries,
30
+ backoffBaseMillis: clientConfig.backoffBaseMillis,
31
+ backoffFactor: clientConfig.backoffFactor,
32
+ backoffMaxMillis: clientConfig.backoffMaxMillis,
33
+ maxFilesPerFlush: clientConfig.maxFilesPerFlush,
34
+ apiKey: platformCreds?.apiKey ?? '',
35
+ sourceIdentifier: platformCreds?.sourceIdentifier ?? '',
36
+ };
37
+
38
+ const client = new BinobanClient({ config: internalConfig, onError });
39
+
40
+ const credsValid =
41
+ !!platformCreds?.apiKey && !!platformCreds?.sourceIdentifier;
42
+
43
+ if (credsValid) {
44
+ // Fire-and-forget; init() is internally guarded and never throws.
45
+ void client.init();
46
+ } else {
47
+ const message = `Missing Binoban credentials for platform "${Platform.OS}". SDK will be inert.`;
48
+ onError?.({ method: 'createClient', message });
49
+ if (clientConfig.debug) {
50
+ console.warn(`[binoban] ${message}`);
51
+ }
52
+ }
53
+
54
+ return client;
55
+ };
56
+
57
+ const Context = createContext<BinobanClient | null>(null);
58
+
59
+ export const BinobanProvider = ({
60
+ client,
61
+ children,
62
+ }: {
63
+ client?: BinobanClient;
64
+ children?: React.ReactNode;
65
+ }) => {
66
+ if (!client) {
67
+ return null;
68
+ }
69
+
70
+ return <Context.Provider value={client}>{children}</Context.Provider>;
71
+ };
72
+
73
+ export const useBinoban = (): ClientMethods => {
74
+ const client = useContext(Context);
75
+ return React.useMemo(() => {
76
+ if (!client) {
77
+ console.error(
78
+ 'Binoban client not configured!',
79
+ 'To use the useBinoban() hook, pass an initialized Binoban client into the BinobanProvider'
80
+ );
81
+ }
82
+
83
+ return {
84
+ track: async (...args) => client?.track(...args),
85
+ identify: async (...args) => client?.identify(...args),
86
+ flush: async () => client?.flush(),
87
+ reset: async () => client?.reset(),
88
+ anonymousId: () => client?.anonymousId(),
89
+ userId: () => client?.userId(),
90
+ deviceId: () => client?.deviceId(),
91
+ notify: (...args) => client?.notify(...args),
92
+ setDeviceToken: (token) => client?.setDeviceToken(token),
93
+ onNotificationInteraction: (listener) =>
94
+ client?.onNotificationInteraction(listener) ?? { remove: () => {} },
95
+ getInitialNotificationInteraction: () =>
96
+ client?.getInitialNotificationInteraction() ?? Promise.resolve(null),
97
+ didReceiveNotificationResponse: (...args) =>
98
+ client?.didReceiveNotificationResponse(...args),
99
+ willPresentNotification: (...args) =>
100
+ client?.willPresentNotification(...args),
101
+ didReceiveRemoteNotification: (...args) =>
102
+ client?.didReceiveRemoteNotification(...args),
103
+ };
104
+ }, [client]);
105
+ };
package/src/types.ts ADDED
@@ -0,0 +1,78 @@
1
+ export type JsonValue =
2
+ | boolean
3
+ | number
4
+ | string
5
+ | null
6
+ | JsonList
7
+ | JsonMap
8
+ | undefined;
9
+ export interface JsonMap {
10
+ [key: string]: JsonValue;
11
+ [index: number]: JsonValue;
12
+ }
13
+ export type JsonList = Array<JsonValue>;
14
+
15
+ export type UserTraits = JsonMap & {
16
+ firstName?: string;
17
+ lastName?: string;
18
+ phone?: string;
19
+ birthday?: string;
20
+ email?: string;
21
+ gender?: string;
22
+ id?: string;
23
+ name?: string;
24
+ title?: string;
25
+ username?: string;
26
+ website?: string;
27
+ };
28
+
29
+ export type NotificationInteractionType =
30
+ | 'DELIVERED'
31
+ | 'CLICKED'
32
+ | 'CLOSED'
33
+ | 'FAILED'
34
+ | 'OPENED';
35
+
36
+ // Public shape of a notification interaction delivered to JS. Mirrors the KMP
37
+ // NotificationInteraction, with customData narrowed to a string→string map.
38
+ export type NotificationInteraction = {
39
+ notificationUuid: string;
40
+ type: NotificationInteractionType;
41
+ actionId?: string | null;
42
+ uri?: string | null;
43
+ customData?: Record<string, string> | null;
44
+ reason?: string | null;
45
+ };
46
+
47
+ export type NotificationInteractionSubscription = { remove: () => void };
48
+
49
+ export type ClientMethods = {
50
+ track: (event: string, properties?: JsonMap) => void;
51
+ identify: (userId: string, userTraits?: UserTraits) => void;
52
+ flush: () => void;
53
+ reset: () => void;
54
+ anonymousId: () => string | undefined;
55
+ userId: () => string | undefined;
56
+ deviceId: () => string | undefined;
57
+ notify: (payloadData: JsonMap) => void;
58
+ setDeviceToken: (token: string) => void;
59
+ // Notification interactions
60
+ onNotificationInteraction: (
61
+ listener: (interaction: NotificationInteraction) => void
62
+ ) => NotificationInteractionSubscription;
63
+ getInitialNotificationInteraction: () => Promise<NotificationInteraction | null>;
64
+ // iOS-only host forwarding (no-ops on Android)
65
+ didReceiveNotificationResponse: (
66
+ userInfo: JsonMap,
67
+ actionId?: string | null,
68
+ dismissed?: boolean
69
+ ) => void;
70
+ willPresentNotification: (userInfo: JsonMap) => void;
71
+ didReceiveRemoteNotification: (userInfo: JsonMap) => void;
72
+ };
73
+
74
+ export type BinobanError = {
75
+ method: string;
76
+ message: string;
77
+ cause?: unknown;
78
+ };