@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,672 @@
1
+ "use strict";
2
+
3
+ import React, { useRef, useCallback, useEffect, useState } from 'react';
4
+ import { View, StyleSheet, ActivityIndicator, Linking, Vibration, Platform, Dimensions, useColorScheme } from 'react-native';
5
+ import { generateBridgeScript, buildFlowHtml } from '../bridge/injectBridge';
6
+ import { setLocalData, getAllLocalData, getLocalData } from '../storage';
7
+ import { trackFlowEvent } from '../api';
8
+
9
+ // Optional peer dependencies - gracefully handle if not installed
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ let HapticFeedback = null;
12
+ let Geolocation = null;
13
+
14
+ // react-native-permissions types
15
+
16
+ let Permissions = null;
17
+ let hapticFeedbackWarningShown = false;
18
+ let geolocationWarningShown = false;
19
+ let permissionsWarningShown = false;
20
+
21
+ // Storage keys for tracking permission request states (for "not_requested" detection on Android)
22
+ const LOCATION_REQUESTED_KEY = '@mobana:location_requested';
23
+ const BACKGROUND_LOCATION_REQUESTED_KEY = '@mobana:bg_location_requested';
24
+
25
+ /**
26
+ * Show warning about missing react-native-permissions and return unavailable response
27
+ */
28
+ function warnPermissionsNotInstalled(feature) {
29
+ if (!permissionsWarningShown) {
30
+ permissionsWarningShown = true;
31
+ console.warn(`[Mobana] react-native-permissions is not installed. ` + `Permission features (${feature}) will not work. To enable permission handling in Flows, install: ` + `npm install react-native-permissions\n` + `See: https://github.com/zoontek/react-native-permissions for setup instructions.`);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Location permission status object returned by getLocationPermissionStatus
37
+ */
38
+
39
+ try {
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ HapticFeedback = require('react-native-haptic-feedback').default;
42
+ } catch {
43
+ // Not installed - will use Vibration fallback
44
+ }
45
+ try {
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ Geolocation = require('react-native-geolocation-service').default;
48
+ } catch {
49
+ // Not installed
50
+ }
51
+ try {
52
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
53
+ Permissions = require('react-native-permissions');
54
+ } catch {
55
+ // Not installed - permission features will show warning when used
56
+ }
57
+
58
+ // Safe area context - try to import for accurate insets
59
+ let SafeAreaContext = null;
60
+ try {
61
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
62
+ SafeAreaContext = require('react-native-safe-area-context');
63
+ } catch {
64
+ // Not installed - will use platform defaults
65
+ }
66
+
67
+ // WebView - required for flows, but loaded dynamically to avoid build failure if not installed
68
+ // MobanaProvider checks for availability before rendering FlowWebView
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ let WebView = null;
71
+ try {
72
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
73
+ WebView = require('react-native-webview').WebView;
74
+ } catch {
75
+ // Not installed - MobanaProvider will handle this case
76
+ }
77
+
78
+ /**
79
+ * Get safe area insets with fallback to platform defaults
80
+ */
81
+ function getSafeAreaInsets() {
82
+ // Try to get from initialWindowMetrics (available at module load time)
83
+ if (SafeAreaContext?.initialWindowMetrics?.insets) {
84
+ return SafeAreaContext.initialWindowMetrics.insets;
85
+ }
86
+
87
+ // Fall back to reasonable platform-specific defaults
88
+ if (Platform.OS === 'ios') {
89
+ // Modern iPhones with notch/dynamic island: ~59pt top (47pt status + 12pt extra for island)
90
+ // Home indicator: ~34pt bottom
91
+ // Older iPhones: ~20pt status bar, 0pt bottom
92
+ const {
93
+ height
94
+ } = Dimensions.get('window');
95
+ const hasNotch = height >= 812; // iPhone X and later
96
+ return {
97
+ top: hasNotch ? 59 : 20,
98
+ bottom: hasNotch ? 34 : 0,
99
+ left: 0,
100
+ right: 0
101
+ };
102
+ } else {
103
+ // Android: typically ~24-32pt for status bar, ~48pt for gesture nav
104
+ return {
105
+ top: 24,
106
+ bottom: 0,
107
+ // Android gesture nav is usually handled by system
108
+ left: 0,
109
+ right: 0
110
+ };
111
+ }
112
+ }
113
+ /**
114
+ * Internal WebView component for rendering flows
115
+ * Handles bridge communication between flow JS and native capabilities
116
+ */
117
+ export function FlowWebView({
118
+ config,
119
+ slug,
120
+ installId,
121
+ endpoint,
122
+ appKey,
123
+ attribution,
124
+ params = {},
125
+ sessionId,
126
+ onComplete,
127
+ onDismiss,
128
+ onEvent,
129
+ onCallback,
130
+ webViewProps,
131
+ debug = false
132
+ }) {
133
+ const webViewRef = useRef(null);
134
+ const [isLoading, setIsLoading] = useState(true);
135
+ const [htmlContent, setHtmlContent] = useState(null);
136
+ const colorScheme = useColorScheme();
137
+ const isDark = colorScheme === 'dark';
138
+ const bgColor = isDark ? '#1c1c1e' : '#FFFFFF';
139
+
140
+ // Build HTML with bridge on mount
141
+ useEffect(() => {
142
+ const buildHtml = async () => {
143
+ const localData = await getAllLocalData();
144
+ const insets = getSafeAreaInsets();
145
+ const {
146
+ width,
147
+ height
148
+ } = Dimensions.get('window');
149
+ const safeArea = {
150
+ ...insets,
151
+ width,
152
+ height
153
+ };
154
+ const bridgeScript = generateBridgeScript({
155
+ attribution,
156
+ params,
157
+ installId,
158
+ platform: Platform.OS === 'ios' ? 'ios' : 'android',
159
+ colorScheme: colorScheme === 'dark' ? 'dark' : 'light',
160
+ localData,
161
+ safeArea
162
+ });
163
+ const fullHtml = buildFlowHtml(config.html, config.css, config.js, bridgeScript, safeArea, colorScheme === 'dark' ? 'dark' : 'light');
164
+ setHtmlContent(fullHtml);
165
+ };
166
+ buildHtml();
167
+ }, [config, attribution, params, installId, colorScheme]);
168
+
169
+ // Send response back to WebView for async requests
170
+ const sendResponse = useCallback((requestId, success, result) => {
171
+ const js = `window.__mobanaBridgeResponse(${requestId}, ${success}, ${JSON.stringify(result)});`;
172
+ webViewRef.current?.injectJavaScript(js);
173
+ }, []);
174
+
175
+ // Track flow event
176
+ const trackEvent = useCallback((event, step, data) => {
177
+ trackFlowEvent(endpoint, appKey, slug, installId, config.versionId, sessionId, event, step, data, debug);
178
+ }, [endpoint, appKey, slug, installId, config.versionId, sessionId, debug]);
179
+
180
+ // Handle haptic feedback
181
+ const triggerHaptic = useCallback(style => {
182
+ if (HapticFeedback) {
183
+ const typeMap = {
184
+ light: 'impactLight',
185
+ medium: 'impactMedium',
186
+ heavy: 'impactHeavy',
187
+ success: 'notificationSuccess',
188
+ warning: 'notificationWarning',
189
+ error: 'notificationError',
190
+ selection: 'selection'
191
+ };
192
+ HapticFeedback.trigger(typeMap[style] || 'impactMedium', {
193
+ enableVibrateFallback: true
194
+ });
195
+ } else {
196
+ // Show warning once about missing optional dependency
197
+ if (!hapticFeedbackWarningShown) {
198
+ hapticFeedbackWarningShown = true;
199
+ console.warn('[Mobana] react-native-haptic-feedback is not installed. ' + 'Falling back to basic Vibration API. For better haptic feedback, install: ' + 'npm install react-native-haptic-feedback');
200
+ }
201
+ // Fallback to basic vibration
202
+ const durationMap = {
203
+ light: 10,
204
+ medium: 20,
205
+ heavy: 30,
206
+ success: 30,
207
+ warning: 40,
208
+ error: 50,
209
+ selection: 5
210
+ };
211
+ Vibration.vibrate(durationMap[style] || 20);
212
+ }
213
+ }, []);
214
+
215
+ // Handle messages from WebView
216
+ const handleMessage = useCallback(async event => {
217
+ try {
218
+ const message = JSON.parse(event.nativeEvent.data);
219
+ const {
220
+ type,
221
+ payload,
222
+ requestId
223
+ } = message;
224
+ if (debug) {
225
+ console.log(`[Mobana] Bridge message: ${type}`, payload);
226
+ }
227
+ switch (type) {
228
+ // Flow control
229
+ case 'complete':
230
+ onComplete(payload?.data);
231
+ break;
232
+ case 'dismiss':
233
+ onDismiss();
234
+ break;
235
+ case 'trackEvent':
236
+ trackEvent(payload?.name);
237
+ onEvent?.(payload?.name);
238
+ break;
239
+
240
+ // Permissions
241
+ case 'requestNotificationPermission':
242
+ if (!Permissions) {
243
+ warnPermissionsNotInstalled('notifications');
244
+ sendResponse(requestId, false, 'react-native-permissions is not installed');
245
+ break;
246
+ }
247
+ try {
248
+ // Use requestNotifications for both platforms - it's cross-platform and handles
249
+ // Android API level differences (POST_NOTIFICATIONS only exists on API 33+)
250
+ const {
251
+ status
252
+ } = await Permissions.requestNotifications(['alert', 'sound', 'badge']);
253
+ sendResponse(requestId, true, status === Permissions.RESULTS.GRANTED);
254
+ } catch (error) {
255
+ if (debug) {
256
+ console.log('[Mobana] Notification permission request error:', error);
257
+ }
258
+ sendResponse(requestId, false, 'Permission request failed');
259
+ }
260
+ break;
261
+ case 'checkNotificationPermission':
262
+ if (!Permissions) {
263
+ warnPermissionsNotInstalled('notifications');
264
+ sendResponse(requestId, true, {
265
+ status: 'unavailable',
266
+ granted: false
267
+ });
268
+ break;
269
+ }
270
+ try {
271
+ // Use checkNotifications for both platforms - it's cross-platform and handles
272
+ // Android API level differences (POST_NOTIFICATIONS only exists on API 33+)
273
+ const {
274
+ status,
275
+ settings
276
+ } = await Permissions.checkNotifications();
277
+ sendResponse(requestId, true, {
278
+ status,
279
+ granted: status === Permissions.RESULTS.GRANTED,
280
+ settings // Detailed settings (alert, badge, sound, etc.)
281
+ });
282
+ } catch (error) {
283
+ if (debug) {
284
+ console.log('[Mobana] Notification permission check error:', error);
285
+ }
286
+ sendResponse(requestId, true, {
287
+ status: 'unavailable',
288
+ granted: false
289
+ });
290
+ }
291
+ break;
292
+ case 'requestATTPermission':
293
+ if (Platform.OS !== 'ios') {
294
+ // ATT is iOS only
295
+ sendResponse(requestId, true, 'authorized');
296
+ break;
297
+ }
298
+ if (!Permissions) {
299
+ warnPermissionsNotInstalled('ATT');
300
+ sendResponse(requestId, true, 'not-determined');
301
+ break;
302
+ }
303
+ try {
304
+ const result = await Permissions.request(Permissions.PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY);
305
+ const statusMap = {
306
+ [Permissions.RESULTS.GRANTED]: 'authorized',
307
+ [Permissions.RESULTS.DENIED]: 'denied',
308
+ [Permissions.RESULTS.BLOCKED]: 'denied',
309
+ [Permissions.RESULTS.UNAVAILABLE]: 'not-determined',
310
+ [Permissions.RESULTS.LIMITED]: 'restricted'
311
+ };
312
+ sendResponse(requestId, true, statusMap[result] || 'not-determined');
313
+ } catch {
314
+ sendResponse(requestId, true, 'not-determined');
315
+ }
316
+ break;
317
+ case 'checkATTPermission':
318
+ if (Platform.OS !== 'ios') {
319
+ // ATT is iOS only - Android doesn't have this restriction
320
+ sendResponse(requestId, true, 'authorized');
321
+ break;
322
+ }
323
+ if (!Permissions) {
324
+ warnPermissionsNotInstalled('ATT');
325
+ sendResponse(requestId, true, 'not-determined');
326
+ break;
327
+ }
328
+ try {
329
+ const result = await Permissions.check(Permissions.PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY);
330
+ const statusMap = {
331
+ [Permissions.RESULTS.GRANTED]: 'authorized',
332
+ [Permissions.RESULTS.DENIED]: 'denied',
333
+ [Permissions.RESULTS.BLOCKED]: 'denied',
334
+ [Permissions.RESULTS.UNAVAILABLE]: 'not-determined',
335
+ [Permissions.RESULTS.LIMITED]: 'restricted'
336
+ };
337
+ sendResponse(requestId, true, statusMap[result] || 'not-determined');
338
+ } catch {
339
+ sendResponse(requestId, true, 'not-determined');
340
+ }
341
+ break;
342
+ case 'requestLocationPermission':
343
+ if (!Permissions) {
344
+ warnPermissionsNotInstalled('location');
345
+ sendResponse(requestId, false, 'react-native-permissions is not installed');
346
+ break;
347
+ }
348
+ try {
349
+ // Mark as requested for "not_requested" detection on Android
350
+ await setLocalData(LOCATION_REQUESTED_KEY, true);
351
+
352
+ // Get precision option: 'precise' (default) or 'coarse'
353
+ const precision = payload?.precision === 'coarse' ? 'coarse' : 'precise';
354
+ const permission = Platform.select({
355
+ ios: Permissions.PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
356
+ // Android: choose between fine and coarse based on precision option
357
+ android: precision === 'coarse' ? Permissions.PERMISSIONS.ANDROID.ACCESS_COARSE_LOCATION : Permissions.PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION
358
+ });
359
+ if (debug) {
360
+ console.log(`[Mobana] Requesting location permission (precision: ${precision}): ${permission}`);
361
+ }
362
+ if (permission) {
363
+ const result = await Permissions.request(permission);
364
+ if (debug) {
365
+ console.log(`[Mobana] Location permission result: ${result}`);
366
+ }
367
+ sendResponse(requestId, true, result);
368
+ } else {
369
+ sendResponse(requestId, true, 'unavailable');
370
+ }
371
+ } catch (error) {
372
+ if (debug) {
373
+ console.log(`[Mobana] Location permission error:`, error);
374
+ }
375
+ sendResponse(requestId, false, 'Permission request failed');
376
+ }
377
+ break;
378
+ case 'requestBackgroundLocationPermission':
379
+ if (!Permissions) {
380
+ warnPermissionsNotInstalled('background location');
381
+ sendResponse(requestId, false, 'react-native-permissions is not installed');
382
+ break;
383
+ }
384
+ try {
385
+ // Mark as requested for "not_requested" detection on Android
386
+ await setLocalData(BACKGROUND_LOCATION_REQUESTED_KEY, true);
387
+ const permission = Platform.select({
388
+ ios: Permissions.PERMISSIONS.IOS.LOCATION_ALWAYS,
389
+ android: Permissions.PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION
390
+ });
391
+ if (debug) {
392
+ console.log(`[Mobana] Requesting background location permission: ${permission}`);
393
+ }
394
+ if (permission) {
395
+ const result = await Permissions.request(permission);
396
+ if (debug) {
397
+ console.log(`[Mobana] Background location permission result: ${result}`);
398
+ }
399
+ sendResponse(requestId, true, result);
400
+ } else {
401
+ sendResponse(requestId, true, 'unavailable');
402
+ }
403
+ } catch (error) {
404
+ if (debug) {
405
+ console.log(`[Mobana] Background location permission error:`, error);
406
+ }
407
+ sendResponse(requestId, false, 'Permission request failed');
408
+ }
409
+ break;
410
+ case 'getLocationPermissionStatus':
411
+ if (!Permissions) {
412
+ warnPermissionsNotInstalled('location status');
413
+ sendResponse(requestId, true, {
414
+ foreground: 'denied',
415
+ background: 'not_requested',
416
+ precision: 'unknown'
417
+ });
418
+ break;
419
+ }
420
+ try {
421
+ // Check if we've ever requested these permissions (for "not_requested" on Android)
422
+ const locationRequested = await getLocalData(LOCATION_REQUESTED_KEY);
423
+ const bgLocationRequested = await getLocalData(BACKGROUND_LOCATION_REQUESTED_KEY);
424
+
425
+ // Helper to convert react-native-permissions result to our status
426
+ const mapStatus = (result, wasRequested) => {
427
+ if (result === Permissions.RESULTS.GRANTED || result === Permissions.RESULTS.LIMITED) return 'granted';
428
+ if (result === Permissions.RESULTS.BLOCKED) return 'blocked';
429
+ // On iOS, RESULTS.DENIED means "not determined" (can ask)
430
+ // On Android, we need to track if we've asked before
431
+ if (result === Permissions.RESULTS.DENIED) {
432
+ if (Platform.OS === 'ios') {
433
+ return 'not_requested'; // iOS "denied" means "not yet asked"
434
+ }
435
+ // Android: check our tracking flag
436
+ return wasRequested ? 'denied' : 'not_requested';
437
+ }
438
+ if (result === Permissions.RESULTS.UNAVAILABLE) return 'denied';
439
+ return 'denied';
440
+ };
441
+
442
+ // Check foreground location
443
+ let foregroundStatus = 'not_requested';
444
+ let locationPrecision = 'unknown';
445
+ if (Platform.OS === 'ios') {
446
+ const iosResult = await Permissions.check(Permissions.PERMISSIONS.IOS.LOCATION_WHEN_IN_USE);
447
+ foregroundStatus = mapStatus(iosResult, !!locationRequested);
448
+ // iOS precision is user-controlled, we can't easily detect it without getting location
449
+ // Mark as unknown since we can't determine without actually getting a location
450
+ if (foregroundStatus === 'granted') {
451
+ locationPrecision = 'unknown'; // iOS user may have chosen precise or approximate
452
+ }
453
+ } else {
454
+ // Android: check both fine and coarse to determine precision
455
+ const fineResult = await Permissions.check(Permissions.PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);
456
+ const coarseResult = await Permissions.check(Permissions.PERMISSIONS.ANDROID.ACCESS_COARSE_LOCATION);
457
+ if (fineResult === Permissions.RESULTS.GRANTED) {
458
+ foregroundStatus = 'granted';
459
+ locationPrecision = 'precise';
460
+ } else if (coarseResult === Permissions.RESULTS.GRANTED) {
461
+ foregroundStatus = 'granted';
462
+ locationPrecision = 'coarse';
463
+ } else if (fineResult === Permissions.RESULTS.BLOCKED || coarseResult === Permissions.RESULTS.BLOCKED) {
464
+ foregroundStatus = 'blocked';
465
+ } else {
466
+ foregroundStatus = mapStatus(fineResult, !!locationRequested);
467
+ }
468
+ }
469
+
470
+ // Check background location
471
+ let backgroundStatus = 'not_requested';
472
+ const bgPermission = Platform.select({
473
+ ios: Permissions.PERMISSIONS.IOS.LOCATION_ALWAYS,
474
+ android: Permissions.PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION
475
+ });
476
+ if (bgPermission) {
477
+ const bgResult = await Permissions.check(bgPermission);
478
+ backgroundStatus = mapStatus(bgResult, !!bgLocationRequested);
479
+ }
480
+ const status = {
481
+ foreground: foregroundStatus,
482
+ background: backgroundStatus,
483
+ precision: locationPrecision
484
+ };
485
+ if (debug) {
486
+ console.log(`[Mobana] Location permission status:`, status);
487
+ }
488
+ sendResponse(requestId, true, status);
489
+ } catch (error) {
490
+ if (debug) {
491
+ console.log(`[Mobana] Location permission status error:`, error);
492
+ }
493
+ sendResponse(requestId, true, {
494
+ foreground: 'denied',
495
+ background: 'not_requested',
496
+ precision: 'unknown'
497
+ });
498
+ }
499
+ break;
500
+ case 'getCurrentLocation':
501
+ if (Geolocation) {
502
+ Geolocation.getCurrentPosition(position => {
503
+ sendResponse(requestId, true, {
504
+ latitude: position.coords.latitude,
505
+ longitude: position.coords.longitude,
506
+ accuracy: position.coords.accuracy,
507
+ altitude: position.coords.altitude,
508
+ altitudeAccuracy: position.coords.altitudeAccuracy,
509
+ heading: position.coords.heading,
510
+ speed: position.coords.speed,
511
+ timestamp: position.timestamp
512
+ });
513
+ }, error => {
514
+ sendResponse(requestId, false, error.message);
515
+ }, {
516
+ enableHighAccuracy: true,
517
+ timeout: 15000,
518
+ maximumAge: 10000
519
+ });
520
+ } else {
521
+ if (!geolocationWarningShown) {
522
+ geolocationWarningShown = true;
523
+ console.warn('[Mobana] react-native-geolocation-service is not installed. ' + 'getCurrentLocation will not work. To enable location features, install: ' + 'npm install react-native-geolocation-service');
524
+ }
525
+ sendResponse(requestId, false, 'Geolocation not available - react-native-geolocation-service is not installed');
526
+ }
527
+ break;
528
+
529
+ // Native utilities
530
+ case 'requestAppReview':
531
+ // App review can't be shown while Modal is visible (StoreKit limitation)
532
+ // Complete the flow with action, and the provider will show review after modal closes
533
+ onComplete({
534
+ action: 'request-app-review'
535
+ });
536
+ break;
537
+ case 'haptic':
538
+ triggerHaptic(payload?.style || 'medium');
539
+ break;
540
+ case 'openURL':
541
+ if (payload?.url) {
542
+ Linking.openURL(payload.url).catch(() => {
543
+ if (debug) {
544
+ console.log(`[Mobana] Failed to open URL: ${payload.url}`);
545
+ }
546
+ });
547
+ }
548
+ break;
549
+ case 'openSettings':
550
+ if (!Permissions) {
551
+ warnPermissionsNotInstalled('openSettings');
552
+ // Try to open settings via Linking as fallback
553
+ Linking.openSettings().catch(() => {
554
+ if (debug) {
555
+ console.log('[Mobana] Failed to open settings via Linking fallback');
556
+ }
557
+ });
558
+ break;
559
+ }
560
+ Permissions.openSettings().catch(() => {
561
+ if (debug) {
562
+ console.log('[Mobana] Failed to open settings');
563
+ }
564
+ });
565
+ break;
566
+
567
+ // Local data
568
+ case 'setLocalData':
569
+ if (payload?.key !== undefined) {
570
+ await setLocalData(payload.key, payload.value);
571
+ }
572
+ break;
573
+
574
+ // App callback
575
+ case 'requestCallback':
576
+ if (!onCallback) {
577
+ sendResponse(requestId, false, 'No onCallback handler provided to startFlow()');
578
+ break;
579
+ }
580
+ try {
581
+ const callbackResult = await onCallback(payload?.data || {});
582
+ sendResponse(requestId, true, callbackResult);
583
+ } catch (error) {
584
+ if (debug) {
585
+ console.log('[Mobana] onCallback error:', error);
586
+ }
587
+ sendResponse(requestId, false, error instanceof Error ? error.message : 'onCallback handler failed');
588
+ }
589
+ break;
590
+ default:
591
+ if (debug) {
592
+ console.log(`[Mobana] Unknown bridge message type: ${type}`);
593
+ }
594
+ }
595
+ } catch (error) {
596
+ if (debug) {
597
+ console.log('[Mobana] Failed to parse bridge message:', error);
598
+ }
599
+ }
600
+ }, [debug, onComplete, onDismiss, onEvent, onCallback, sendResponse, trackEvent, triggerHaptic]);
601
+ if (!htmlContent || !WebView) {
602
+ return /*#__PURE__*/_jsx(View, {
603
+ style: [styles.container, {
604
+ backgroundColor: bgColor
605
+ }],
606
+ children: /*#__PURE__*/_jsx(ActivityIndicator, {
607
+ size: "large",
608
+ color: isDark ? '#0A84FF' : '#007AFF'
609
+ })
610
+ });
611
+ }
612
+ return /*#__PURE__*/_jsxs(View, {
613
+ style: [styles.container, {
614
+ backgroundColor: bgColor
615
+ }],
616
+ children: [/*#__PURE__*/_jsx(WebView, {
617
+ ref: webViewRef,
618
+ source: {
619
+ html: htmlContent
620
+ },
621
+ style: styles.webview,
622
+ onMessage: handleMessage,
623
+ onLoadStart: () => setIsLoading(true),
624
+ onLoadEnd: () => setIsLoading(false),
625
+ originWhitelist: ['*'],
626
+ javaScriptEnabled: true,
627
+ domStorageEnabled: true,
628
+ allowsInlineMediaPlayback: true,
629
+ mediaPlaybackRequiresUserAction: false,
630
+ scrollEnabled: true,
631
+ bounces: false,
632
+ showsHorizontalScrollIndicator: false,
633
+ showsVerticalScrollIndicator: false
634
+ // Security: don't allow navigation away from the flow
635
+ ,
636
+ onShouldStartLoadWithRequest: request => {
637
+ // Allow initial load and javascript: URLs
638
+ if (request.url === 'about:blank' || request.url.startsWith('data:')) {
639
+ return true;
640
+ }
641
+ // Block external navigation - use openURL bridge instead
642
+ if (request.url.startsWith('http://') || request.url.startsWith('https://')) {
643
+ return false;
644
+ }
645
+ return true;
646
+ },
647
+ ...webViewProps
648
+ }), isLoading && /*#__PURE__*/_jsx(View, {
649
+ style: [styles.loadingOverlay, {
650
+ backgroundColor: bgColor
651
+ }],
652
+ children: /*#__PURE__*/_jsx(ActivityIndicator, {
653
+ size: "large",
654
+ color: isDark ? '#0A84FF' : '#007AFF'
655
+ })
656
+ })]
657
+ });
658
+ }
659
+ const styles = StyleSheet.create({
660
+ container: {
661
+ flex: 1
662
+ },
663
+ webview: {
664
+ flex: 1
665
+ },
666
+ loadingOverlay: {
667
+ ...StyleSheet.absoluteFillObject,
668
+ justifyContent: 'center',
669
+ alignItems: 'center'
670
+ }
671
+ });
672
+ //# sourceMappingURL=FlowWebView.js.map