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