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