@mobana/react-native-sdk 0.2.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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/android/build.gradle +50 -0
  4. package/android/src/main/AndroidManifest.xml +6 -0
  5. package/android/src/main/java/ai/mobana/sdk/MobanaModule.kt +67 -0
  6. package/android/src/main/java/ai/mobana/sdk/MobanaPackage.kt +19 -0
  7. package/app.plugin.js +274 -0
  8. package/ios/Mobana.h +11 -0
  9. package/ios/Mobana.m +20 -0
  10. package/lib/commonjs/Mobana.js +676 -0
  11. package/lib/commonjs/Mobana.js.map +1 -0
  12. package/lib/commonjs/NativeMobana.js +53 -0
  13. package/lib/commonjs/NativeMobana.js.map +1 -0
  14. package/lib/commonjs/api.js +201 -0
  15. package/lib/commonjs/api.js.map +1 -0
  16. package/lib/commonjs/bridge/index.js +19 -0
  17. package/lib/commonjs/bridge/index.js.map +1 -0
  18. package/lib/commonjs/bridge/injectBridge.js +528 -0
  19. package/lib/commonjs/bridge/injectBridge.js.map +1 -0
  20. package/lib/commonjs/components/FlowWebView.js +676 -0
  21. package/lib/commonjs/components/FlowWebView.js.map +1 -0
  22. package/lib/commonjs/components/MobanaProvider.js +275 -0
  23. package/lib/commonjs/components/MobanaProvider.js.map +1 -0
  24. package/lib/commonjs/components/index.js +20 -0
  25. package/lib/commonjs/components/index.js.map +1 -0
  26. package/lib/commonjs/device.js +49 -0
  27. package/lib/commonjs/device.js.map +1 -0
  28. package/lib/commonjs/index.js +20 -0
  29. package/lib/commonjs/index.js.map +1 -0
  30. package/lib/commonjs/package.json +1 -0
  31. package/lib/commonjs/storage.js +277 -0
  32. package/lib/commonjs/storage.js.map +1 -0
  33. package/lib/commonjs/types.js +2 -0
  34. package/lib/commonjs/types.js.map +1 -0
  35. package/lib/module/Mobana.js +673 -0
  36. package/lib/module/Mobana.js.map +1 -0
  37. package/lib/module/NativeMobana.js +49 -0
  38. package/lib/module/NativeMobana.js.map +1 -0
  39. package/lib/module/api.js +194 -0
  40. package/lib/module/api.js.map +1 -0
  41. package/lib/module/bridge/index.js +4 -0
  42. package/lib/module/bridge/index.js.map +1 -0
  43. package/lib/module/bridge/injectBridge.js +523 -0
  44. package/lib/module/bridge/injectBridge.js.map +1 -0
  45. package/lib/module/components/FlowWebView.js +672 -0
  46. package/lib/module/components/FlowWebView.js.map +1 -0
  47. package/lib/module/components/MobanaProvider.js +270 -0
  48. package/lib/module/components/MobanaProvider.js.map +1 -0
  49. package/lib/module/components/index.js +5 -0
  50. package/lib/module/components/index.js.map +1 -0
  51. package/lib/module/device.js +45 -0
  52. package/lib/module/device.js.map +1 -0
  53. package/lib/module/index.js +53 -0
  54. package/lib/module/index.js.map +1 -0
  55. package/lib/module/storage.js +257 -0
  56. package/lib/module/storage.js.map +1 -0
  57. package/lib/module/types.js +2 -0
  58. package/lib/module/types.js.map +1 -0
  59. package/lib/typescript/Mobana.d.ts +209 -0
  60. package/lib/typescript/Mobana.d.ts.map +1 -0
  61. package/lib/typescript/NativeMobana.d.ts +11 -0
  62. package/lib/typescript/NativeMobana.d.ts.map +1 -0
  63. package/lib/typescript/api.d.ts +34 -0
  64. package/lib/typescript/api.d.ts.map +1 -0
  65. package/lib/typescript/bridge/index.d.ts +3 -0
  66. package/lib/typescript/bridge/index.d.ts.map +1 -0
  67. package/lib/typescript/bridge/injectBridge.d.ts +23 -0
  68. package/lib/typescript/bridge/injectBridge.d.ts.map +1 -0
  69. package/lib/typescript/components/FlowWebView.d.ts +38 -0
  70. package/lib/typescript/components/FlowWebView.d.ts.map +1 -0
  71. package/lib/typescript/components/MobanaProvider.d.ts +65 -0
  72. package/lib/typescript/components/MobanaProvider.d.ts.map +1 -0
  73. package/lib/typescript/components/index.d.ts +5 -0
  74. package/lib/typescript/components/index.d.ts.map +1 -0
  75. package/lib/typescript/device.d.ts +6 -0
  76. package/lib/typescript/device.d.ts.map +1 -0
  77. package/lib/typescript/index.d.ts +46 -0
  78. package/lib/typescript/index.d.ts.map +1 -0
  79. package/lib/typescript/storage.d.ts +68 -0
  80. package/lib/typescript/storage.d.ts.map +1 -0
  81. package/lib/typescript/types.d.ts +298 -0
  82. package/lib/typescript/types.d.ts.map +1 -0
  83. package/mobana.podspec +19 -0
  84. package/package.json +131 -0
  85. package/src/Mobana.ts +742 -0
  86. package/src/NativeMobana.ts +61 -0
  87. package/src/api.ts +259 -0
  88. package/src/bridge/index.ts +2 -0
  89. package/src/bridge/injectBridge.ts +542 -0
  90. package/src/components/FlowWebView.tsx +826 -0
  91. package/src/components/MobanaProvider.tsx +393 -0
  92. package/src/components/index.ts +4 -0
  93. package/src/device.ts +42 -0
  94. package/src/index.ts +66 -0
  95. package/src/storage.ts +262 -0
  96. package/src/types.ts +362 -0
@@ -0,0 +1,393 @@
1
+ import React, {
2
+ createContext,
3
+ useState,
4
+ useCallback,
5
+ useEffect,
6
+ useRef,
7
+ ReactNode,
8
+ } from 'react';
9
+ import {
10
+ Modal,
11
+ StyleSheet,
12
+ View,
13
+ StatusBar,
14
+ Platform,
15
+ ActivityIndicator,
16
+ useColorScheme,
17
+ } from 'react-native';
18
+ import type { WebViewProps } from 'react-native-webview';
19
+ import type { ModalProps } from 'react-native';
20
+ import type { FlowConfig, FlowResult, FlowOptions, Attribution } from '../types';
21
+ import type { FlowWebViewProps } from './FlowWebView';
22
+ import { trackFlowEvent } from '../api';
23
+ import { generateUUID } from '../storage';
24
+
25
+ // Optional: react-native-webview (required for Flows; checked at provider mount)
26
+ let WebViewAvailable = false;
27
+ try {
28
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
29
+ require('react-native-webview');
30
+ WebViewAvailable = true;
31
+ } catch {
32
+ // Not installed - provider will throw in __DEV__ if used
33
+ }
34
+
35
+ let FlowWebViewComponent: React.ComponentType<FlowWebViewProps> | null = null;
36
+ if (WebViewAvailable) {
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
38
+ FlowWebViewComponent = require('./FlowWebView').FlowWebView;
39
+ }
40
+
41
+ // Optional peer dependency for app review
42
+ let InAppReview: { RequestInAppReview: () => Promise<boolean> } | null = null;
43
+ let inAppReviewWarningShown = false;
44
+
45
+ try {
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ InAppReview = require('react-native-in-app-review').default;
48
+ } catch {
49
+ // Not installed
50
+ }
51
+
52
+ /**
53
+ * Internal flow request from SDK
54
+ */
55
+ export interface FlowRequest {
56
+ slug: string;
57
+ config: FlowConfig;
58
+ installId: string;
59
+ endpoint: string;
60
+ appKey: string;
61
+ attribution: Attribution | null;
62
+ options?: FlowOptions;
63
+ resolve: (result: FlowResult) => void;
64
+ debug?: boolean;
65
+ }
66
+
67
+ /**
68
+ * Context for flow presentation
69
+ */
70
+ interface FlowContextValue {
71
+ presentFlow: (request: FlowRequest) => void;
72
+ isProviderMounted: boolean;
73
+ }
74
+
75
+ const FlowContext = createContext<FlowContextValue | null>(null);
76
+
77
+ /**
78
+ * Check if provider is mounted (used by SDK)
79
+ */
80
+ let globalFlowContext: FlowContextValue | null = null;
81
+
82
+ export function getGlobalFlowContext(): FlowContextValue | null {
83
+ return globalFlowContext;
84
+ }
85
+
86
+ /**
87
+ * Props for MobanaProvider
88
+ */
89
+ export interface MobanaProviderProps {
90
+ children: ReactNode;
91
+ /**
92
+ * Custom props for the Modal component
93
+ */
94
+ modalProps?: Partial<ModalProps>;
95
+ /**
96
+ * Custom props for the WebView component
97
+ */
98
+ webViewProps?: Partial<WebViewProps>;
99
+ /**
100
+ * Custom loading component to show while flow is loading
101
+ */
102
+ loadingComponent?: ReactNode;
103
+ }
104
+
105
+ /**
106
+ * Provider component for Mobana flows
107
+ *
108
+ * Wrap your app with this component to enable flow presentation:
109
+ *
110
+ * @example
111
+ * ```tsx
112
+ * import { MobanaProvider } from '@mobana/react-native-sdk';
113
+ *
114
+ * export default function App() {
115
+ * return (
116
+ * <MobanaProvider>
117
+ * <YourApp />
118
+ * </MobanaProvider>
119
+ * );
120
+ * }
121
+ * ```
122
+ */
123
+ export function MobanaProvider({
124
+ children,
125
+ modalProps,
126
+ webViewProps,
127
+ loadingComponent,
128
+ }: MobanaProviderProps) {
129
+ const colorScheme = useColorScheme();
130
+ const isDark = colorScheme === 'dark';
131
+ const bgColor = isDark ? '#1c1c1e' : '#FFFFFF';
132
+
133
+ if (__DEV__ && !WebViewAvailable) {
134
+ throw new Error(
135
+ '[Mobana] react-native-webview is required for MobanaProvider.\n\n' +
136
+ 'Install it with: npm install react-native-webview\n\n' +
137
+ 'If you only need attribution/conversion tracking, you can skip the provider ' +
138
+ 'and use Mobana.init, getAttribution, trackConversion directly.'
139
+ );
140
+ }
141
+
142
+ const [currentRequest, setCurrentRequest] = useState<FlowRequest | null>(null);
143
+ const [isLoading, setIsLoading] = useState(false);
144
+ const hasTrackedStartRef = useRef(false);
145
+ // Session ID for the current flow presentation (groups all events together)
146
+ const sessionIdRef = useRef<string>('');
147
+
148
+ // Present a flow
149
+ const presentFlow = useCallback((request: FlowRequest) => {
150
+ setIsLoading(true);
151
+ hasTrackedStartRef.current = false;
152
+ // Generate a new session ID for this flow presentation
153
+ sessionIdRef.current = generateUUID();
154
+ setCurrentRequest(request);
155
+ }, []);
156
+
157
+ // Context value
158
+ const contextValue: FlowContextValue = {
159
+ presentFlow,
160
+ isProviderMounted: true,
161
+ };
162
+
163
+ // Register global context for SDK access
164
+ useEffect(() => {
165
+ globalFlowContext = contextValue;
166
+ return () => {
167
+ globalFlowContext = null;
168
+ };
169
+ }, [contextValue]);
170
+
171
+ // Track "__started__" event when flow is shown (system event, not user-callable)
172
+ useEffect(() => {
173
+ if (currentRequest && !hasTrackedStartRef.current) {
174
+ hasTrackedStartRef.current = true;
175
+ trackFlowEvent(
176
+ currentRequest.endpoint,
177
+ currentRequest.appKey,
178
+ currentRequest.slug,
179
+ currentRequest.installId,
180
+ currentRequest.config.versionId,
181
+ sessionIdRef.current,
182
+ '__started__',
183
+ undefined,
184
+ undefined,
185
+ currentRequest.debug
186
+ );
187
+ setIsLoading(false);
188
+ }
189
+ }, [currentRequest]);
190
+
191
+ // Handle flow completion
192
+ const handleComplete = useCallback(
193
+ (data?: Record<string, unknown>) => {
194
+ if (!currentRequest) return;
195
+
196
+ // Capture context for the trackEvent closure (must be done before clearing currentRequest)
197
+ const capturedEndpoint = currentRequest.endpoint;
198
+ const capturedAppKey = currentRequest.appKey;
199
+ const capturedSlug = currentRequest.slug;
200
+ const capturedInstallId = currentRequest.installId;
201
+ const capturedVersionId = currentRequest.config.versionId;
202
+ const capturedSessionId = sessionIdRef.current;
203
+ const capturedDebug = currentRequest.debug;
204
+
205
+ // Track "__completed__" event (system event, not user-callable)
206
+ trackFlowEvent(
207
+ capturedEndpoint,
208
+ capturedAppKey,
209
+ capturedSlug,
210
+ capturedInstallId,
211
+ capturedVersionId,
212
+ capturedSessionId,
213
+ '__completed__',
214
+ undefined,
215
+ data,
216
+ capturedDebug
217
+ );
218
+
219
+ // Create trackEvent closure for post-flow event tracking
220
+ const trackEvent = async (event: string, eventData?: Record<string, unknown>): Promise<boolean> => {
221
+ return trackFlowEvent(
222
+ capturedEndpoint,
223
+ capturedAppKey,
224
+ capturedSlug,
225
+ capturedInstallId,
226
+ capturedVersionId,
227
+ capturedSessionId,
228
+ event,
229
+ undefined,
230
+ eventData,
231
+ capturedDebug
232
+ );
233
+ };
234
+
235
+ // Resolve the promise with sessionId and trackEvent for post-flow tracking
236
+ currentRequest.resolve({
237
+ completed: true,
238
+ dismissed: false,
239
+ data,
240
+ sessionId: capturedSessionId,
241
+ trackEvent,
242
+ });
243
+
244
+ // Check for special actions that need to run after modal closes
245
+ const action = data?.action;
246
+
247
+ setCurrentRequest(null);
248
+
249
+ // Handle post-modal actions after a delay to ensure modal is fully closed
250
+ if (action === 'request-app-review') {
251
+ setTimeout(async () => {
252
+ if (InAppReview) {
253
+ try {
254
+ await InAppReview.RequestInAppReview();
255
+ } catch {
256
+ // Silently fail - review request is best-effort
257
+ }
258
+ } else {
259
+ if (!inAppReviewWarningShown) {
260
+ inAppReviewWarningShown = true;
261
+ console.warn(
262
+ '[Mobana] react-native-in-app-review is not installed. ' +
263
+ 'App store review prompts will not work. To enable this feature, install: ' +
264
+ 'npm install react-native-in-app-review'
265
+ );
266
+ }
267
+ }
268
+ }, 300); // Small delay to ensure modal animation completes
269
+ }
270
+ },
271
+ [currentRequest]
272
+ );
273
+
274
+ // Handle flow dismissal
275
+ const handleDismiss = useCallback(() => {
276
+ if (!currentRequest) return;
277
+
278
+ // Capture context for the trackEvent closure (must be done before clearing currentRequest)
279
+ const capturedEndpoint = currentRequest.endpoint;
280
+ const capturedAppKey = currentRequest.appKey;
281
+ const capturedSlug = currentRequest.slug;
282
+ const capturedInstallId = currentRequest.installId;
283
+ const capturedVersionId = currentRequest.config.versionId;
284
+ const capturedSessionId = sessionIdRef.current;
285
+ const capturedDebug = currentRequest.debug;
286
+
287
+ // Track "__dismissed__" event (system event, not user-callable)
288
+ trackFlowEvent(
289
+ capturedEndpoint,
290
+ capturedAppKey,
291
+ capturedSlug,
292
+ capturedInstallId,
293
+ capturedVersionId,
294
+ capturedSessionId,
295
+ '__dismissed__',
296
+ undefined,
297
+ undefined,
298
+ capturedDebug
299
+ );
300
+
301
+ // Create trackEvent closure for post-flow event tracking
302
+ const trackEvent = async (event: string, eventData?: Record<string, unknown>): Promise<boolean> => {
303
+ return trackFlowEvent(
304
+ capturedEndpoint,
305
+ capturedAppKey,
306
+ capturedSlug,
307
+ capturedInstallId,
308
+ capturedVersionId,
309
+ capturedSessionId,
310
+ event,
311
+ undefined,
312
+ eventData,
313
+ capturedDebug
314
+ );
315
+ };
316
+
317
+ // Resolve the promise with sessionId and trackEvent for post-flow tracking
318
+ currentRequest.resolve({
319
+ completed: false,
320
+ dismissed: true,
321
+ sessionId: capturedSessionId,
322
+ trackEvent,
323
+ });
324
+
325
+ setCurrentRequest(null);
326
+ }, [currentRequest]);
327
+
328
+ // Handle custom events from flow
329
+ const handleEvent = useCallback(
330
+ (name: string) => {
331
+ currentRequest?.options?.onEvent?.(name);
332
+ },
333
+ [currentRequest]
334
+ );
335
+
336
+ return (
337
+ <FlowContext.Provider value={contextValue}>
338
+ {children}
339
+ <Modal
340
+ visible={currentRequest !== null}
341
+ animationType="slide"
342
+ presentationStyle="fullScreen"
343
+ statusBarTranslucent={Platform.OS === 'android'}
344
+ onRequestClose={undefined} // Disable Android back button dismiss
345
+ // navigationBarTranslucent is supported in RN 0.72+ but types may be outdated
346
+ {...(Platform.OS === 'android' ? { navigationBarTranslucent: true } : {})}
347
+ {...modalProps}
348
+ >
349
+ <View style={[styles.modalContainer, { backgroundColor: bgColor }]}>
350
+ {Platform.OS === 'ios' && (
351
+ <StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} />
352
+ )}
353
+ {currentRequest && !isLoading && FlowWebViewComponent && (
354
+ <FlowWebViewComponent
355
+ config={currentRequest.config}
356
+ slug={currentRequest.slug}
357
+ installId={currentRequest.installId}
358
+ endpoint={currentRequest.endpoint}
359
+ appKey={currentRequest.appKey}
360
+ attribution={currentRequest.attribution}
361
+ params={currentRequest.options?.params}
362
+ sessionId={sessionIdRef.current}
363
+ onComplete={handleComplete}
364
+ onDismiss={handleDismiss}
365
+ onEvent={handleEvent}
366
+ onCallback={currentRequest.options?.onCallback}
367
+ webViewProps={webViewProps}
368
+ debug={currentRequest.debug}
369
+ />
370
+ )}
371
+ {isLoading && (
372
+ <View style={[styles.loadingContainer, { backgroundColor: bgColor }]}>
373
+ {loadingComponent || (
374
+ <ActivityIndicator size="large" color={isDark ? '#0A84FF' : '#007AFF'} />
375
+ )}
376
+ </View>
377
+ )}
378
+ </View>
379
+ </Modal>
380
+ </FlowContext.Provider>
381
+ );
382
+ }
383
+
384
+ const styles = StyleSheet.create({
385
+ modalContainer: {
386
+ flex: 1,
387
+ },
388
+ loadingContainer: {
389
+ flex: 1,
390
+ justifyContent: 'center',
391
+ alignItems: 'center',
392
+ },
393
+ });
@@ -0,0 +1,4 @@
1
+ export { MobanaProvider } from './MobanaProvider';
2
+ export type { MobanaProviderProps } from './MobanaProvider';
3
+ export { FlowWebView } from './FlowWebView';
4
+ export type { FlowWebViewProps } from './FlowWebView';
package/src/device.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { Platform, Dimensions } from 'react-native';
2
+ import type { DeviceInfo } from './types';
3
+
4
+ /**
5
+ * Collect device information for attribution matching
6
+ */
7
+ export function getDeviceInfo(): DeviceInfo {
8
+ const { width, height } = Dimensions.get('screen');
9
+
10
+ return {
11
+ platform: Platform.OS === 'ios' ? 'ios' : 'android',
12
+ timezone: getTimezone(),
13
+ screenWidth: Math.round(width),
14
+ screenHeight: Math.round(height),
15
+ language: getLanguage(),
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Get device timezone
21
+ */
22
+ function getTimezone(): string | undefined {
23
+ try {
24
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get device language in BCP 47 format
32
+ */
33
+ function getLanguage(): string | undefined {
34
+ try {
35
+ // In React Native, we can access the locale through various means
36
+ // The most reliable is through the Intl API
37
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
38
+ return locale;
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @mobana/react-native-sdk
3
+ *
4
+ * Simple, privacy-focused mobile app attribution, conversions, and remote flows.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { Mobana, MobanaProvider } from '@mobana/react-native-sdk';
9
+ *
10
+ * // 1. Wrap your app with the provider (in App.tsx)
11
+ * function App() {
12
+ * return (
13
+ * <MobanaProvider>
14
+ * <YourApp />
15
+ * </MobanaProvider>
16
+ * );
17
+ * }
18
+ *
19
+ * // 2. Initialize the SDK
20
+ * await Mobana.init({ appId: 'a1b2c3d4' });
21
+ *
22
+ * // 3. Get attribution
23
+ * const attribution = await Mobana.getAttribution();
24
+ *
25
+ * // 4. Track conversions
26
+ * Mobana.trackConversion('signup');
27
+ * Mobana.trackConversion('purchase', 49.99);
28
+ *
29
+ * // 5. Show flows and track post-flow events
30
+ * const result = await Mobana.startFlow('onboarding');
31
+ * if (result.completed) {
32
+ * // Track events after flow closes using result.trackEvent()
33
+ * await result.trackEvent('feature_used');
34
+ * // Link conversions to flow session using result.sessionId
35
+ * await Mobana.trackConversion('purchase', 9.99, result.sessionId);
36
+ * }
37
+ * ```
38
+ *
39
+ * @packageDocumentation
40
+ */
41
+
42
+ // Main SDK
43
+ export { Mobana } from './Mobana';
44
+
45
+ // Components
46
+ export { MobanaProvider } from './components/MobanaProvider';
47
+ export type { MobanaProviderProps } from './components/MobanaProvider';
48
+
49
+ // Types - Attribution
50
+ export type {
51
+ MobanaConfig,
52
+ GetAttributionOptions,
53
+ Attribution,
54
+ } from './types';
55
+
56
+ // Types - Flows
57
+ export type {
58
+ FlowConfig,
59
+ FlowResult,
60
+ FlowOptions,
61
+ FlowError,
62
+ HapticStyle,
63
+ LocationPermissionStatus,
64
+ ATTStatus,
65
+ LocationCoordinates,
66
+ } from './types';