@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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/android/build.gradle +50 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/ai/mobana/sdk/MobanaModule.kt +67 -0
- package/android/src/main/java/ai/mobana/sdk/MobanaPackage.kt +19 -0
- package/app.plugin.js +274 -0
- package/ios/Mobana.h +11 -0
- package/ios/Mobana.m +20 -0
- package/lib/commonjs/Mobana.js +676 -0
- package/lib/commonjs/Mobana.js.map +1 -0
- package/lib/commonjs/NativeMobana.js +53 -0
- package/lib/commonjs/NativeMobana.js.map +1 -0
- package/lib/commonjs/api.js +201 -0
- package/lib/commonjs/api.js.map +1 -0
- package/lib/commonjs/bridge/index.js +19 -0
- package/lib/commonjs/bridge/index.js.map +1 -0
- package/lib/commonjs/bridge/injectBridge.js +528 -0
- package/lib/commonjs/bridge/injectBridge.js.map +1 -0
- package/lib/commonjs/components/FlowWebView.js +676 -0
- package/lib/commonjs/components/FlowWebView.js.map +1 -0
- package/lib/commonjs/components/MobanaProvider.js +275 -0
- package/lib/commonjs/components/MobanaProvider.js.map +1 -0
- package/lib/commonjs/components/index.js +20 -0
- package/lib/commonjs/components/index.js.map +1 -0
- package/lib/commonjs/device.js +49 -0
- package/lib/commonjs/device.js.map +1 -0
- package/lib/commonjs/index.js +20 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/storage.js +277 -0
- package/lib/commonjs/storage.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/Mobana.js +673 -0
- package/lib/module/Mobana.js.map +1 -0
- package/lib/module/NativeMobana.js +49 -0
- package/lib/module/NativeMobana.js.map +1 -0
- package/lib/module/api.js +194 -0
- package/lib/module/api.js.map +1 -0
- package/lib/module/bridge/index.js +4 -0
- package/lib/module/bridge/index.js.map +1 -0
- package/lib/module/bridge/injectBridge.js +523 -0
- package/lib/module/bridge/injectBridge.js.map +1 -0
- package/lib/module/components/FlowWebView.js +672 -0
- package/lib/module/components/FlowWebView.js.map +1 -0
- package/lib/module/components/MobanaProvider.js +270 -0
- package/lib/module/components/MobanaProvider.js.map +1 -0
- package/lib/module/components/index.js +5 -0
- package/lib/module/components/index.js.map +1 -0
- package/lib/module/device.js +45 -0
- package/lib/module/device.js.map +1 -0
- package/lib/module/index.js +53 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/storage.js +257 -0
- package/lib/module/storage.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/Mobana.d.ts +209 -0
- package/lib/typescript/Mobana.d.ts.map +1 -0
- package/lib/typescript/NativeMobana.d.ts +11 -0
- package/lib/typescript/NativeMobana.d.ts.map +1 -0
- package/lib/typescript/api.d.ts +34 -0
- package/lib/typescript/api.d.ts.map +1 -0
- package/lib/typescript/bridge/index.d.ts +3 -0
- package/lib/typescript/bridge/index.d.ts.map +1 -0
- package/lib/typescript/bridge/injectBridge.d.ts +23 -0
- package/lib/typescript/bridge/injectBridge.d.ts.map +1 -0
- package/lib/typescript/components/FlowWebView.d.ts +38 -0
- package/lib/typescript/components/FlowWebView.d.ts.map +1 -0
- package/lib/typescript/components/MobanaProvider.d.ts +65 -0
- package/lib/typescript/components/MobanaProvider.d.ts.map +1 -0
- package/lib/typescript/components/index.d.ts +5 -0
- package/lib/typescript/components/index.d.ts.map +1 -0
- package/lib/typescript/device.d.ts +6 -0
- package/lib/typescript/device.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +46 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/storage.d.ts +68 -0
- package/lib/typescript/storage.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +298 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/mobana.podspec +19 -0
- package/package.json +131 -0
- package/src/Mobana.ts +742 -0
- package/src/NativeMobana.ts +61 -0
- package/src/api.ts +259 -0
- package/src/bridge/index.ts +2 -0
- package/src/bridge/injectBridge.ts +542 -0
- package/src/components/FlowWebView.tsx +826 -0
- package/src/components/MobanaProvider.tsx +393 -0
- package/src/components/index.ts +4 -0
- package/src/device.ts +42 -0
- package/src/index.ts +66 -0
- package/src/storage.ts +262 -0
- 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
|