@oobit/react-native-sdk 1.0.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,9 +18,9 @@ yarn add @oobit/react-native-sdk
18
18
  Make sure you have these installed:
19
19
 
20
20
  ```bash
21
- npm install react-native-webview expo-linking
21
+ npm install react-native-webview expo-linking expo-clipboard expo-intent-launcher expo-local-authentication
22
22
  # or
23
- yarn add react-native-webview expo-linking
23
+ yarn add react-native-webview expo-linking expo-clipboard expo-intent-launcher expo-local-authentication
24
24
  ```
25
25
 
26
26
  ## Quick Start
@@ -30,28 +30,23 @@ import { WidgetSDK } from '@oobit/react-native-sdk';
30
30
  import { View, Alert } from 'react-native';
31
31
 
32
32
  function MyApp() {
33
- const widgetUrl = 'https://your-widget.com?token=xyz'; // Your authenticated widget URL
34
-
35
33
  return (
36
34
  <View style={{ flex: 1 }}>
37
35
  <WidgetSDK
38
- apiKey="your-api-key"
36
+ accessToken="your-jwt-token-from-backend"
37
+ userWalletAddress="0x1234...abcd"
39
38
  environment="production"
40
- widgetUrl={widgetUrl}
41
39
  onReady={() => {
42
40
  console.log('Widget loaded successfully');
43
41
  }}
44
- onCardCreated={(cardId, cardType, last4) => {
45
- Alert.alert('Success', `Card ${last4} created!`);
46
- // Save card info, navigate to card details, etc.
47
- }}
48
42
  onError={(code, message) => {
49
43
  Alert.alert('Error', message);
50
- // Log to error tracking, show user-friendly message, etc.
51
44
  }}
52
45
  onClose={() => {
53
46
  console.log('User closed the widget');
54
- // Navigate back, clean up state, etc.
47
+ }}
48
+ onTransactionRequested={(token, amount, address, tag) => {
49
+ console.log(`Send ${amount} ${token.symbol} to ${address}`);
55
50
  }}
56
51
  />
57
52
  </View>
@@ -63,105 +58,72 @@ function MyApp() {
63
58
 
64
59
  ### Required Props
65
60
 
66
- #### `apiKey: string`
67
- Your API key for authentication.
61
+ #### `accessToken: string`
62
+ JWT access token from your backend for authentication.
68
63
 
69
64
  ```typescript
70
- <WidgetSDK apiKey="your-api-key" />
65
+ <WidgetSDK accessToken="eyJhbGciOiJIUzI1NiIs..." />
71
66
  ```
72
67
 
73
- #### `environment: 'sandbox' | 'production'`
74
- The environment to use.
68
+ #### `userWalletAddress: string`
69
+ The user's external wallet address for crypto deposits.
75
70
 
76
71
  ```typescript
77
- <WidgetSDK environment="production" />
72
+ <WidgetSDK userWalletAddress="0x1234567890abcdef..." />
78
73
  ```
79
74
 
80
- #### `widgetUrl: string`
81
- The URL of your widget (typically includes a JWT token for authentication).
75
+ ### Optional Props
76
+
77
+ #### `environment?: 'development' | 'production'`
78
+ The environment to use. Defaults to `'production'`.
82
79
 
83
80
  ```typescript
84
- <WidgetSDK widgetUrl="https://widget.yourapp.com?token=jwt_token_here" />
81
+ <WidgetSDK environment="development" />
85
82
  ```
86
83
 
87
- ### Optional Callback Props
84
+ #### `debug?: boolean`
85
+ Enable debug logging to console. Defaults to `false`.
88
86
 
89
- #### `onReady?: () => void`
90
- Called when the widget has finished loading and is ready for interaction.
87
+ ```typescript
88
+ <WidgetSDK debug={true} />
89
+ ```
91
90
 
92
- **Use this to:**
93
- - Hide loading indicators
94
- - Track analytics events
95
- - Initialize app state
91
+ #### `loadingIndicatorColor?: string`
92
+ Custom color for the loading indicator. Defaults to `'#007AFF'`.
96
93
 
97
94
  ```typescript
98
- <WidgetSDK
99
- onReady={() => {
100
- console.log('Widget is ready!');
101
- analytics.track('widget_loaded');
102
- }}
103
- />
95
+ <WidgetSDK loadingIndicatorColor="#6200EE" />
104
96
  ```
105
97
 
106
- #### `onCardCreated?: (cardId: string, cardType: string, last4: string) => void`
107
- Called when a card is successfully created in the widget.
98
+ ### Callback Props
108
99
 
109
- **Parameters:**
110
- - `cardId` - Unique identifier for the created card
111
- - `cardType` - Type of card: `'virtual'` or `'physical'`
112
- - `last4` - Last 4 digits of the card number
113
-
114
- **Use this to:**
115
- - Save card info to your app's state
116
- - Navigate to card details screen
117
- - Show success message to user
118
- - Trigger analytics events
119
- - Close the widget
100
+ #### `onReady?: () => void`
101
+ Called when the widget has finished loading and is ready for interaction.
120
102
 
121
103
  ```typescript
122
104
  <WidgetSDK
123
- onCardCreated={(cardId, cardType, last4) => {
124
- console.log(`Card created: ${cardId}, type: ${cardType}, ending in ${last4}`);
125
-
126
- // Save to state
127
- setUserCard({ id: cardId, type: cardType, last4 });
128
-
129
- // Navigate to card details
130
- navigation.navigate('CardDetails', { cardId });
131
-
132
- // Track analytics
133
- analytics.track('card_created', { cardType });
105
+ onReady={() => {
106
+ console.log('Widget is ready!');
107
+ setIsLoading(false);
134
108
  }}
135
109
  />
136
110
  ```
137
111
 
138
- #### `onError?: (code: string, message: string) => void`
112
+ #### `onError?: (code: SDKErrorCode | string, message: string) => void`
139
113
  Called when an error occurs in the widget.
140
114
 
141
- **Parameters:**
142
- - `code` - Error code (e.g., `'INVALID_CARD'`, `'NETWORK_ERROR'`)
143
- - `message` - Human-readable error message
144
-
145
- **Use this to:**
146
- - Show error alerts to user
147
- - Log errors to monitoring service
148
- - Handle specific error cases
149
- - Retry operations
115
+ **Error Codes:**
116
+ - `TOKEN_EXPIRED` - The access token has expired
117
+ - `PARSE_ERROR` - Failed to parse a message from the widget
118
+ - `WEBVIEW_ERROR` - The WebView failed to load
150
119
 
151
120
  ```typescript
152
121
  <WidgetSDK
153
122
  onError={(code, message) => {
154
123
  console.error(`Widget error [${code}]: ${message}`);
155
124
 
156
- // Show user-friendly error
157
- Alert.alert('Error', message);
158
-
159
- // Log to error tracking
160
- Sentry.captureException(new Error(`Widget error: ${code} - ${message}`));
161
-
162
- // Handle specific errors
163
- if (code === 'SESSION_EXPIRED') {
164
- // Refresh authentication and reload widget
125
+ if (code === 'TOKEN_EXPIRED') {
126
+ // Refresh token and reload
165
127
  refreshAuth();
166
128
  }
167
129
  }}
@@ -169,135 +131,92 @@ Called when an error occurs in the widget.
169
131
  ```
170
132
 
171
133
  #### `onClose?: () => void`
172
- Called when the user requests to close the widget (via a close button in the widget).
173
-
174
- **Use this to:**
175
- - Navigate back to previous screen
176
- - Clean up state
177
- - Track analytics events
178
- - Hide the widget
134
+ Called when the user requests to close the widget.
179
135
 
180
136
  ```typescript
181
137
  <WidgetSDK
182
138
  onClose={() => {
183
- console.log('User closed widget');
184
-
185
- // Navigate back
186
139
  navigation.goBack();
140
+ }}
141
+ />
142
+ ```
187
143
 
188
- // Or hide widget
189
- setShowWidget(false);
144
+ #### `onTransactionRequested?: (token, cryptoAmount, depositAddress, depositAddressTag) => void`
145
+ Called when a crypto transaction is requested.
190
146
 
191
- // Track analytics
192
- analytics.track('widget_closed');
147
+ ```typescript
148
+ <WidgetSDK
149
+ onTransactionRequested={(token, cryptoAmount, depositAddress, depositAddressTag) => {
150
+ console.log(`Send ${cryptoAmount} ${token.symbol} to ${depositAddress}`);
151
+ // Navigate to your send crypto screen
152
+ navigation.navigate('SendCrypto', {
153
+ token,
154
+ amount: cryptoAmount,
155
+ address: depositAddress,
156
+ tag: depositAddressTag,
157
+ });
193
158
  }}
194
159
  />
195
160
  ```
196
161
 
197
- ## Complete Example
162
+ #### `onLoadingChange?: (isLoading: boolean) => void`
163
+ Called when the loading state changes.
198
164
 
199
165
  ```typescript
200
- import React, { useState } from 'react';
201
- import { View, Button, StyleSheet, Alert, ActivityIndicator } from 'react-native';
202
- import { WidgetSDK } from '@oobit/react-native-sdk';
166
+ <WidgetSDK
167
+ onLoadingChange={(isLoading) => {
168
+ setShowCustomLoader(isLoading);
169
+ }}
170
+ />
171
+ ```
203
172
 
204
- export default function CardIssuanceScreen() {
205
- const [showWidget, setShowWidget] = useState(false);
206
- const [widgetUrl, setWidgetUrl] = useState('');
207
- const [isLoading, setIsLoading] = useState(false);
208
-
209
- const launchWidget = async () => {
210
- try {
211
- setIsLoading(true);
212
-
213
- // Get authenticated widget URL from your backend
214
- const response = await fetch('https://your-api.com/widget/token', {
215
- method: 'POST',
216
- headers: { 'Authorization': 'Bearer your-token' }
217
- });
218
- const { url } = await response.json();
219
-
220
- setWidgetUrl(url);
221
- setShowWidget(true);
222
- } catch (error) {
223
- Alert.alert('Error', 'Failed to load widget');
224
- } finally {
225
- setIsLoading(false);
226
- }
173
+ ## Ref Methods
174
+
175
+ You can control the widget programmatically using a ref:
176
+
177
+ ```typescript
178
+ import { useRef } from 'react';
179
+ import { WidgetSDK, WidgetSDKRef } from '@oobit/react-native-sdk';
180
+
181
+ function MyScreen() {
182
+ const widgetRef = useRef<WidgetSDKRef>(null);
183
+
184
+ const handleBackPress = () => {
185
+ widgetRef.current?.navigateBack();
227
186
  };
228
187
 
229
- if (showWidget && widgetUrl) {
230
- return (
231
- <View style={styles.container}>
232
- <WidgetSDK
233
- apiKey="your-api-key"
234
- environment="production"
235
- widgetUrl={widgetUrl}
236
- onReady={() => {
237
- console.log('Widget ready');
238
- }}
239
- onCardCreated={(cardId, cardType, last4) => {
240
- Alert.alert(
241
- 'Card Created!',
242
- `Your ${cardType} card ending in ${last4} is ready.`,
243
- [
244
- {
245
- text: 'View Card',
246
- onPress: () => {
247
- setShowWidget(false);
248
- // Navigate to card details
249
- }
250
- }
251
- ]
252
- );
253
- }}
254
- onError={(code, message) => {
255
- Alert.alert('Error', message);
256
- }}
257
- onClose={() => {
258
- setShowWidget(false);
259
- }}
260
- />
261
- </View>
262
- );
263
- }
188
+ const handleRetry = () => {
189
+ widgetRef.current?.reload();
190
+ };
264
191
 
265
192
  return (
266
- <View style={styles.container}>
267
- <Button
268
- title={isLoading ? 'Loading...' : 'Create Card'}
269
- onPress={launchWidget}
270
- disabled={isLoading}
271
- />
272
- {isLoading && <ActivityIndicator style={{ marginTop: 20 }} />}
273
- </View>
193
+ <WidgetSDK
194
+ ref={widgetRef}
195
+ accessToken={token}
196
+ userWalletAddress={address}
197
+ />
274
198
  );
275
199
  }
276
-
277
- const styles = StyleSheet.create({
278
- container: {
279
- flex: 1,
280
- justifyContent: 'center',
281
- padding: 20,
282
- },
283
- });
284
200
  ```
285
201
 
202
+ ### Available Methods
203
+
204
+ | Method | Description |
205
+ |--------|-------------|
206
+ | `navigateBack()` | Navigate back within the widget |
207
+ | `reload()` | Reload the widget |
208
+
286
209
  ## TypeScript Support
287
210
 
288
211
  The SDK is fully typed. Import types as needed:
289
212
 
290
213
  ```typescript
291
- import type { WidgetSDKConfig } from '@oobit/react-native-sdk';
292
-
293
- const config: WidgetSDKConfig = {
294
- apiKey: 'your-key',
295
- environment: 'production',
296
- widgetUrl: 'https://...',
297
- onCardCreated: (cardId, cardType, last4) => {
298
- // Fully typed parameters
299
- },
300
- };
214
+ import type {
215
+ WidgetSDKConfig,
216
+ WidgetSDKRef,
217
+ SDKErrorCode,
218
+ DepositToken,
219
+ } from '@oobit/react-native-sdk';
301
220
  ```
302
221
 
303
222
  ### Message Type Constants
@@ -308,49 +227,14 @@ For advanced use cases where you need to handle messages directly:
308
227
  import { MessageTypes } from '@oobit/react-native-sdk';
309
228
 
310
229
  // Use constants instead of strings
311
- if (message.type === MessageTypes.CARD_CREATED) {
312
- // Handle card creation
230
+ if (message.type === MessageTypes.ERROR) {
231
+ // Handle error
313
232
  }
314
233
  ```
315
234
 
316
- Available constants:
317
-
318
- **Widget → Native:**
319
- - `MessageTypes.READY` - Widget is ready
320
- - `MessageTypes.CARD_CREATED` - Card was created
321
- - `MessageTypes.ERROR` - An error occurred
322
- - `MessageTypes.CLOSE` - User closed widget
323
- - `MessageTypes.OPEN_WALLET` - User wants to add card to wallet
324
-
325
- **Native → Widget:**
326
- - `MessageTypes.PLATFORM_INFO` - Platform info sent to widget
327
- - `MessageTypes.WALLET_OPENED` - Wallet open result
328
- - `MessageTypes.BACK_PRESSED` - User pressed back button/gesture
329
- - `MessageTypes.NAVIGATE_BACK` - Programmatic back navigation
330
-
331
- ## Platform Support
332
-
333
- | Platform | Supported | Notes |
334
- |----------|-----------|-------|
335
- | iOS | ✅ | Includes Apple Wallet integration |
336
- | Android | ✅ | Includes Google Pay integration |
337
- | Web | ❌ | React Native only |
338
-
339
235
  ## Native Wallet Integration
340
236
 
341
- The SDK automatically handles adding cards to Apple Wallet (iOS) and Google Pay (Android) when requested by your widget.
342
-
343
- **Your widget can trigger wallet addition:**
344
- ```javascript
345
- // In your web widget
346
- window.ReactNativeWebView.postMessage(JSON.stringify({
347
- type: 'widget:open-wallet',
348
- timestamp: Date.now(),
349
- payload: { platform: 'ios' } // or 'android'
350
- }));
351
- ```
352
-
353
- **The SDK handles it automatically** - no callback needed from your app!
237
+ The SDK automatically handles adding cards to Apple Wallet (iOS) and Google Pay (Android).
354
238
 
355
239
  You can also manually open the wallet from your React Native app:
356
240
 
@@ -366,84 +250,81 @@ if (available) {
366
250
  }
367
251
  ```
368
252
 
369
- ## Back Navigation
370
-
371
- The SDK handles back navigation gestures and forwards them to your web widget, allowing the widget to control its own internal navigation.
372
-
373
- ### How It Works
374
-
375
- 1. **Android**: Hardware back button press is intercepted and forwarded to the widget
376
- 2. **iOS**: Swipe-back gestures are enabled via `allowsBackForwardNavigationGestures`
377
-
378
- The widget receives a `native:back-pressed` message and decides whether to:
379
- - Navigate back internally (if there's navigation history)
380
- - Close the widget (if on the first screen)
253
+ ## Platform Support
381
254
 
382
- ### Programmatic Back Navigation
255
+ | Platform | Supported | Notes |
256
+ |----------|-----------|-------|
257
+ | iOS | ✅ | Includes Apple Wallet integration |
258
+ | Android | ✅ | Includes Google Pay integration |
259
+ | Web | ❌ | React Native only |
383
260
 
384
- You can programmatically trigger back navigation using a ref:
261
+ ## Complete Example
385
262
 
386
263
  ```typescript
387
- import { useRef } from 'react';
264
+ import React, { useState, useRef } from 'react';
265
+ import { View, Button, StyleSheet, Alert } from 'react-native';
388
266
  import { WidgetSDK, WidgetSDKRef } from '@oobit/react-native-sdk';
389
267
 
390
- function MyScreen() {
268
+ export default function PaymentScreen() {
269
+ const [showWidget, setShowWidget] = useState(false);
391
270
  const widgetRef = useRef<WidgetSDKRef>(null);
392
271
 
393
- const handleTimeout = () => {
394
- // Programmatically navigate widget back
395
- widgetRef.current?.navigateBack();
396
- };
272
+ if (showWidget) {
273
+ return (
274
+ <View style={styles.container}>
275
+ <WidgetSDK
276
+ ref={widgetRef}
277
+ accessToken="your-jwt-token"
278
+ userWalletAddress="0x1234..."
279
+ environment="production"
280
+ debug={__DEV__}
281
+ onReady={() => {
282
+ console.log('Widget ready');
283
+ }}
284
+ onError={(code, message) => {
285
+ Alert.alert('Error', `${code}: ${message}`);
286
+ if (code === 'WEBVIEW_ERROR') {
287
+ // Offer retry option
288
+ Alert.alert('Error', 'Failed to load. Retry?', [
289
+ { text: 'Cancel', onPress: () => setShowWidget(false) },
290
+ { text: 'Retry', onPress: () => widgetRef.current?.reload() },
291
+ ]);
292
+ }
293
+ }}
294
+ onClose={() => {
295
+ setShowWidget(false);
296
+ }}
297
+ onTransactionRequested={(token, amount, address, tag) => {
298
+ Alert.alert(
299
+ 'Transaction Requested',
300
+ `Send ${amount} ${token.symbol} to ${address}`,
301
+ );
302
+ }}
303
+ onLoadingChange={(isLoading) => {
304
+ console.log('Loading:', isLoading);
305
+ }}
306
+ />
307
+ </View>
308
+ );
309
+ }
397
310
 
398
311
  return (
399
- <WidgetSDK
400
- ref={widgetRef}
401
- widgetUrl={widgetUrl}
402
- accessToken={token}
403
- onClose={() => setShowWidget(false)}
404
- />
312
+ <View style={styles.container}>
313
+ <Button
314
+ title="Open Payment Widget"
315
+ onPress={() => setShowWidget(true)}
316
+ />
317
+ </View>
405
318
  );
406
319
  }
407
- ```
408
320
 
409
- ### Web Widget Implementation
410
-
411
- Your web widget must handle these messages from the native app:
412
-
413
- ```javascript
414
- // In your web widget
415
- window.addEventListener('message', (event) => {
416
- try {
417
- const message = JSON.parse(event.data);
418
-
419
- if (message.type === 'native:back-pressed' || message.type === 'native:navigate-back') {
420
- // Check if on first screen (login, etc.)
421
- if (isOnFirstScreen()) {
422
- // Close widget - return to native app
423
- window.ReactNativeWebView.postMessage(JSON.stringify({
424
- type: 'widget:close',
425
- timestamp: Date.now()
426
- }));
427
- } else {
428
- // Navigate back internally
429
- window.history.back();
430
- // OR: router.back() if using React Router
431
- }
432
- }
433
- } catch (e) {
434
- // Ignore non-JSON messages
435
- }
321
+ const styles = StyleSheet.create({
322
+ container: {
323
+ flex: 1,
324
+ },
436
325
  });
437
326
  ```
438
327
 
439
- ### Message Types
440
-
441
- | Message | Direction | Description |
442
- |---------|-----------|-------------|
443
- | `native:back-pressed` | Native → Widget | User pressed back button/gesture |
444
- | `native:navigate-back` | Native → Widget | Programmatic back navigation request |
445
- | `widget:close` | Widget → Native | Widget requests to close |
446
-
447
328
  ## Dependencies
448
329
 
449
330
  ### Required Peer Dependencies
@@ -453,116 +334,35 @@ window.addEventListener('message', (event) => {
453
334
  "react": ">=18.0.0",
454
335
  "react-native": ">=0.70.0",
455
336
  "react-native-webview": ">=13.0.0",
456
- "expo-linking": ">=6.0.0"
337
+ "expo-linking": ">=6.0.0",
338
+ "expo-clipboard": ">=5.0.0",
339
+ "expo-intent-launcher": ">=6.0.0",
340
+ "expo-local-authentication": ">=14.0.0"
457
341
  }
458
342
  ```
459
343
 
460
- ### Optional Dependencies
461
-
462
- For full wallet integration on Android:
463
- - `expo-intent-launcher` - Android wallet integration
464
-
465
- Install with:
466
- ```bash
467
- expo install expo-intent-launcher
468
- ```
469
-
470
- ## Common Use Cases
471
-
472
- ### Show Widget After Authentication
473
-
474
- ```typescript
475
- const [isAuthenticated, setIsAuthenticated] = useState(false);
476
- const [widgetUrl, setWidgetUrl] = useState('');
477
-
478
- useEffect(() => {
479
- if (isAuthenticated) {
480
- fetchWidgetUrl().then(setWidgetUrl);
481
- }
482
- }, [isAuthenticated]);
483
-
484
- if (!isAuthenticated) {
485
- return <LoginScreen onLogin={() => setIsAuthenticated(true)} />;
486
- }
487
-
488
- return <WidgetSDK widgetUrl={widgetUrl} {...props} />;
489
- ```
490
-
491
- ### Close Widget and Navigate
492
-
493
- ```typescript
494
- <WidgetSDK
495
- onCardCreated={(cardId) => {
496
- // Navigate to card details immediately
497
- navigation.replace('CardDetails', { cardId });
498
- }}
499
- onClose={() => {
500
- // Go back to previous screen
501
- navigation.goBack();
502
- }}
503
- />
504
- ```
505
-
506
- ### Track Analytics
507
-
508
- ```typescript
509
- <WidgetSDK
510
- onReady={() => {
511
- analytics.track('widget_viewed');
512
- }}
513
- onCardCreated={(cardId, cardType) => {
514
- analytics.track('card_created', {
515
- cardId,
516
- cardType,
517
- source: 'mobile_app'
518
- });
519
- }}
520
- onError={(code, message) => {
521
- analytics.track('widget_error', { code, message });
522
- }}
523
- />
524
- ```
525
-
526
- ### Show Loading State
527
-
528
- ```typescript
529
- const [isWidgetReady, setIsWidgetReady] = useState(false);
530
-
531
- <View style={{ flex: 1 }}>
532
- {!isWidgetReady && (
533
- <View style={styles.loadingOverlay}>
534
- <ActivityIndicator size="large" />
535
- <Text>Loading widget...</Text>
536
- </View>
537
- )}
538
- <WidgetSDK
539
- {...props}
540
- onReady={() => setIsWidgetReady(true)}
541
- />
542
- </View>
543
- ```
544
-
545
344
  ## Troubleshooting
546
345
 
547
346
  ### Widget Not Loading
548
347
 
549
- **Check these:**
550
- 1. Is `widgetUrl` accessible?
551
- 2. Is the JWT token valid?
552
- 3. Check React Native debugger for errors
553
- 4. Verify `react-native-webview` is installed
348
+ 1. Verify `accessToken` is valid and not expired
349
+ 2. Check that `userWalletAddress` is a valid address
350
+ 3. Enable `debug={true}` to see console logs
351
+ 4. Verify all peer dependencies are installed
554
352
 
555
353
  ### Callbacks Not Firing
556
354
 
557
- **Make sure:**
558
- 1. Your widget is sending proper postMessage events
559
- 2. Message format matches expected structure (see web widget integration docs)
560
- 3. Check console for `[WidgetSDK]` logs
355
+ 1. Enable debug mode to see message logs
356
+ 2. Check that your widget is sending proper postMessage events
357
+ 3. Verify message format matches expected structure
358
+
359
+ ### Error: TOKEN_EXPIRED
360
+
361
+ The access token has expired. Fetch a new token from your backend and re-render the widget with the new token.
561
362
 
562
- ### App Crashes on Wallet Integration
363
+ ### Error: WEBVIEW_ERROR
563
364
 
564
- **iOS:** Ensure Apple Wallet app is installed (may require real device)
565
- **Android:** Ensure Google Play Services is configured on the device/emulator
365
+ The WebView failed to load. This could be a network issue. Use `widgetRef.current?.reload()` to retry.
566
366
 
567
367
  ## License
568
368