@oobit/react-native-sdk 1.0.6 → 1.1.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/src/WidgetSDK.tsx CHANGED
@@ -20,19 +20,16 @@ import {
20
20
  View,
21
21
  } from "react-native";
22
22
  import { WebView, WebViewMessageEvent } from "react-native-webview";
23
- import { authenticateWithBiometrics } from "./biometricUtils";
24
23
  import { getWidgetUrl } from "./config";
25
- import { generateSessionCredentials } from "./cryptoUtils";
26
- import {
27
- NativeBiometricFailedMessage,
28
- NativeCardDetailsSessionMessage,
29
- WidgetMessage,
30
- WidgetSDKConfig,
31
- } from "./types";
24
+ import { CopyToClipboardMessage, WidgetMessage, WidgetSDKConfig } from "./types";
32
25
  import { isWalletAvailable, openNativeWallet } from "./walletUtils";
26
+ import * as Clipboard from "expo-clipboard";
33
27
 
34
28
  export interface WidgetSDKRef {
29
+ /** Navigate back within the widget */
35
30
  navigateBack: () => void;
31
+ /** Reload the widget */
32
+ reload: () => void;
36
33
  }
37
34
 
38
35
  export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
@@ -42,13 +39,25 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
42
39
  userWalletAddress,
43
40
  environment,
44
41
  onReady,
45
- onCardCreated,
46
42
  onError,
47
43
  onClose,
48
44
  onTransactionRequested,
45
+ onLoadingChange,
46
+ debug = false,
47
+ loadingIndicatorColor = "#007AFF",
49
48
  },
50
49
  ref
51
50
  ) => {
51
+ // Debug logger - only logs when debug is enabled
52
+ const log = debug
53
+ ? (...args: unknown[]) => console.log("[WidgetSDK]", ...args)
54
+ : () => {};
55
+ const logError = debug
56
+ ? (...args: unknown[]) => console.error("[WidgetSDK]", ...args)
57
+ : () => {};
58
+ const logWarn = debug
59
+ ? (...args: unknown[]) => console.warn("[WidgetSDK]", ...args)
60
+ : () => {};
52
61
  const webViewRef = useRef<WebView>(null);
53
62
  const [walletAvailable, setWalletAvailable] = useState(false);
54
63
  const [isLoading, setIsLoading] = useState(true);
@@ -77,7 +86,7 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
77
86
  return () => backHandler.remove();
78
87
  }, []);
79
88
 
80
- // Expose navigateBack method via ref
89
+ // Expose methods via ref
81
90
  useImperativeHandle(ref, () => ({
82
91
  navigateBack: () => {
83
92
  sendMessageToWidget({
@@ -85,8 +94,19 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
85
94
  timestamp: Date.now(),
86
95
  });
87
96
  },
97
+ reload: () => {
98
+ log("Reloading widget");
99
+ setIsLoading(true);
100
+ hasLoadedOnce.current = false;
101
+ webViewRef.current?.reload();
102
+ },
88
103
  }));
89
104
 
105
+ // Notify parent of loading state changes
106
+ useEffect(() => {
107
+ onLoadingChange?.(isLoading);
108
+ }, [isLoading, onLoadingChange]);
109
+
90
110
  const checkWalletAvailability = async () => {
91
111
  const available = await isWalletAvailable();
92
112
  setWalletAvailable(available);
@@ -99,7 +119,7 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
99
119
  try {
100
120
  const message: WidgetMessage = JSON.parse(event.nativeEvent.data);
101
121
 
102
- console.log("[WidgetSDK] Received message:", message.type);
122
+ log("Received message:", message.type);
103
123
 
104
124
  switch (message.type) {
105
125
  case "widget:ready":
@@ -110,10 +130,6 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
110
130
  handleOpenWallet();
111
131
  break;
112
132
 
113
- case "widget:card-created":
114
- handleCardCreated(message);
115
- break;
116
-
117
133
  case "widget:error":
118
134
  handleError(message);
119
135
  break;
@@ -127,25 +143,25 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
127
143
  break;
128
144
 
129
145
  case "widget:token-expired":
130
- console.error("[WidgetSDK] Access token expired");
146
+ logError("Access token expired");
131
147
  onError?.("TOKEN_EXPIRED", "Access token has expired");
132
148
  break;
133
149
 
134
- case "widget:request-card-details-session":
135
- handleCardDetailsRequest(message);
150
+ case "widget:copy-to-clipboard":
151
+ handleCopyToClipboard(message as CopyToClipboardMessage);
136
152
  break;
137
153
 
138
154
  default:
139
- console.warn("[WidgetSDK] Unknown message type:", message);
155
+ logWarn("Unknown message type:", message);
140
156
  }
141
157
  } catch (error) {
142
- console.error("[WidgetSDK] Failed to parse message:", error);
158
+ logError("Failed to parse message:", error);
143
159
  onError?.("PARSE_ERROR", "Failed to parse widget message");
144
160
  }
145
161
  };
146
162
 
147
163
  const handleReady = () => {
148
- console.log("[WidgetSDK] Widget ready");
164
+ log("Widget ready");
149
165
  onReady?.();
150
166
 
151
167
  // Send platform info to widget
@@ -159,7 +175,7 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
159
175
  };
160
176
 
161
177
  const handleOpenWallet = async () => {
162
- console.log("[WidgetSDK] Opening native wallet...");
178
+ log("Opening native wallet...");
163
179
 
164
180
  const success = await openNativeWallet();
165
181
 
@@ -170,26 +186,17 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
170
186
  });
171
187
  };
172
188
 
173
- const handleCardCreated = (message: WidgetMessage) => {
174
- if (message.type !== "widget:card-created") return;
175
-
176
- const { cardId, cardType, last4 } = message.payload;
177
- console.log("[WidgetSDK] Card created:", cardId);
178
-
179
- onCardCreated?.(cardId, cardType, last4);
180
- };
181
-
182
189
  const handleError = (message: WidgetMessage) => {
183
190
  if (message.type !== "widget:error") return;
184
191
 
185
192
  const { code, message: errorMessage } = message.payload;
186
- console.error("[WidgetSDK] Widget error:", code, errorMessage);
193
+ logError("Widget error:", code, errorMessage);
187
194
 
188
195
  onError?.(code, errorMessage);
189
196
  };
190
197
 
191
198
  const handleClose = () => {
192
- console.log("[WidgetSDK] Widget closed");
199
+ log("Widget closed");
193
200
  onClose?.();
194
201
  };
195
202
 
@@ -198,11 +205,10 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
198
205
 
199
206
  const { token, cryptoAmount, depositAddress, depositAddressTag } =
200
207
  message.payload;
201
- console.log("[WidgetSDK] Transaction requested:", {
202
- token,
208
+ log("Transaction requested:", {
209
+ token: token.symbol,
203
210
  cryptoAmount,
204
211
  depositAddress,
205
- depositAddressTag,
206
212
  });
207
213
 
208
214
  onTransactionRequested?.(
@@ -214,79 +220,30 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
214
220
  };
215
221
 
216
222
  /**
217
- * Handle card details session request from widget
218
- * 1. Show biometric prompt for user authentication
219
- * 2. Generate cryptographically linked sessionId + secretKey pair
220
- * 3. Send credentials back to widget for API call and decryption
223
+ * Handle clipboard copy request from widget
224
+ * Copies text to device clipboard and sends result back to widget
221
225
  */
222
- const handleCardDetailsRequest = async (message: WidgetMessage) => {
223
- if (message.type !== "widget:request-card-details-session") return;
224
-
225
- const { publicKey } = message.payload;
226
- console.log(
227
- "[WidgetSDK] Card details requested, initiating biometric auth..."
228
- );
226
+ const handleCopyToClipboard = async (message: CopyToClipboardMessage) => {
227
+ const { text, label } = message.payload;
228
+ // Log only the label, never the actual text content (security)
229
+ log("Copy to clipboard requested:", label || "unlabeled");
229
230
 
230
- // Step 1: Authenticate with biometrics
231
- const biometricResult = await authenticateWithBiometrics(
232
- "Authenticate to view card details"
233
- );
234
-
235
- if (!biometricResult.success) {
236
- console.log(
237
- "[WidgetSDK] Biometric auth failed:",
238
- biometricResult.error
239
- );
240
-
241
- const failedMessage: NativeBiometricFailedMessage = {
242
- type: "native:biometric-failed",
243
- payload: {
244
- reason: biometricResult.error?.reason || "failed",
245
- message: biometricResult.error?.message,
246
- },
247
- };
248
-
249
- sendMessageToWidget(failedMessage);
250
- return;
251
- }
252
-
253
- // Step 2: Generate session credentials
254
231
  try {
255
- console.log(
256
- "[WidgetSDK] Biometric auth successful, generating session credentials..."
257
- );
258
-
259
- const { secretKeyHex, sessionId } = await generateSessionCredentials(
260
- publicKey
261
- );
262
-
263
- console.log("[WidgetSDK] Session credentials generated successfully");
264
-
265
- // Step 3: Send credentials to widget
266
- const sessionMessage: NativeCardDetailsSessionMessage = {
267
- type: "native:card-details-session",
268
- payload: {
269
- sessionId,
270
- secretKey: secretKeyHex,
271
- },
272
- };
273
-
274
- sendMessageToWidget(sessionMessage);
232
+ await Clipboard.setStringAsync(text);
233
+ sendMessageToWidget({
234
+ type: "native:clipboard-result",
235
+ payload: { success: true, label },
236
+ });
275
237
  } catch (error) {
276
- console.error(
277
- "[WidgetSDK] Failed to generate session credentials:",
278
- error
279
- );
280
-
281
- const failedMessage: NativeBiometricFailedMessage = {
282
- type: "native:biometric-failed",
238
+ logError("Clipboard copy failed:", error);
239
+ sendMessageToWidget({
240
+ type: "native:clipboard-result",
283
241
  payload: {
284
- reason: "failed",
285
- message: "Failed to generate secure session",
242
+ success: false,
243
+ label,
244
+ error: "Clipboard operation failed",
286
245
  },
287
- };
288
-
289
- sendMessageToWidget(failedMessage);
246
+ });
290
247
  }
291
248
  };
292
249
 
@@ -314,7 +271,7 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
314
271
  });
315
272
 
316
273
  const url = `${baseUrl}?${params.toString()}`;
317
- console.log("[WidgetSDK] Widget URL:", url.substring(0, 100) + "...");
274
+ log("Widget URL:", url.substring(0, 100) + "...");
318
275
  return url;
319
276
  }, [accessToken, userWalletAddress, environment]);
320
277
 
@@ -333,7 +290,7 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
333
290
  }}
334
291
  onError={(syntheticEvent) => {
335
292
  const { nativeEvent } = syntheticEvent;
336
- console.error("[WidgetSDK] WebView error:", nativeEvent);
293
+ logError("WebView error:", nativeEvent);
337
294
  setIsLoading(false);
338
295
  onError?.("WEBVIEW_ERROR", "Failed to load widget");
339
296
  }}
@@ -348,9 +305,9 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
348
305
  url.includes("wallet.google.com") ||
349
306
  url.includes("wallet.apple.com")
350
307
  ) {
351
- console.log("[WidgetSDK] Opening external URL:", url);
308
+ log("Opening external URL:", url);
352
309
  Linking.openURL(url).catch((err) => {
353
- console.error("[WidgetSDK] Failed to open URL:", err);
310
+ logError("Failed to open URL:", err);
354
311
  });
355
312
  return false; // Don't load in WebView
356
313
  }
@@ -370,7 +327,7 @@ export const WidgetSDK = forwardRef<WidgetSDKRef, WidgetSDKConfig>(
370
327
  />
371
328
  {isLoading && (
372
329
  <View style={styles.loadingOverlay}>
373
- <ActivityIndicator size="large" color="#007AFF" />
330
+ <ActivityIndicator size="large" color={loadingIndicatorColor} />
374
331
  </View>
375
332
  )}
376
333
  </View>
package/src/index.ts CHANGED
@@ -3,59 +3,51 @@
3
3
  * Export all public components, types, and utilities
4
4
  */
5
5
 
6
- export { WidgetSDK } from './WidgetSDK';
7
- export type { WidgetSDKRef } from './WidgetSDK';
6
+ export { WidgetSDK } from "./WidgetSDK";
7
+ export type { WidgetSDKRef } from "./WidgetSDK";
8
8
 
9
9
  // Export all message types for clients to use
10
10
  export type {
11
- // Configuration
12
- WidgetSDKConfig,
13
- WidgetEnvironment,
14
- DepositToken,
15
-
16
- // Widget → Native message types
17
- WidgetMessageType,
18
11
  BaseWidgetMessage,
19
- WidgetMessage,
20
- WidgetReadyMessage,
21
- OpenWalletMessage,
22
- CardCreatedMessage,
23
- WidgetErrorMessage,
24
- WidgetCloseMessage,
25
- TransactionRequestedMessage,
26
- RequestCardDetailsSessionMessage,
27
-
12
+ CopyToClipboardMessage,
13
+ DepositToken,
14
+ NativeBackPressedMessage,
15
+ NativeBiometricFailedMessage,
16
+ NativeCardDetailsSessionMessage,
17
+ NativeMessage,
28
18
  // Native → Widget message types
29
19
  NativeMessageType,
30
- NativeMessage,
31
- NativeBackPressedMessage,
32
20
  NativeNavigateBackMessage,
33
21
  NativePlatformInfoMessage,
34
22
  NativeWalletOpenedMessage,
35
- NativeCardDetailsSessionMessage,
36
- NativeBiometricFailedMessage,
37
- } from './types';
23
+ OpenWalletMessage,
24
+ RequestCardDetailsSessionMessage,
25
+ TokenExpiredMessage,
26
+ TransactionRequestedMessage,
27
+ WidgetCloseMessage,
28
+ WidgetEnvironment,
29
+ WidgetErrorMessage,
30
+ WidgetMessage,
31
+ // Widget → Native message types
32
+ WidgetMessageType,
33
+ WidgetReadyMessage,
34
+ // Configuration
35
+ WidgetSDKConfig,
36
+ // Error codes
37
+ SDKErrorCode,
38
+ } from "./types";
38
39
 
39
40
  // Export constants
40
- export { WALLET_URLS, MessageTypes } from './types';
41
- export { WIDGET_URLS, getWidgetUrl } from './config';
41
+ export { getWidgetUrl, WIDGET_URLS } from "./config";
42
+ export { MessageTypes, WALLET_URLS } from "./types";
42
43
 
43
44
  // Export wallet utilities
44
- export { openNativeWallet, isWalletAvailable } from './walletUtils';
45
-
46
- // Export biometric utilities
47
- export {
48
- authenticateWithBiometrics,
49
- isBiometricAvailable,
50
- getBiometryTypeLabel,
51
- AuthenticationType,
52
- } from './biometricUtils';
53
- export type { BiometricResult, BiometricAvailability } from './biometricUtils';
45
+ export { isWalletAvailable, openNativeWallet } from "./walletUtils";
54
46
 
55
47
  // Export crypto utilities (for advanced usage)
56
48
  export {
57
- generateSessionCredentials,
58
49
  generateRandomHexKey,
50
+ generateSessionCredentials,
59
51
  hexToBase64,
60
- } from './cryptoUtils';
61
- export type { SessionCredentials } from './cryptoUtils';
52
+ } from "./cryptoUtils";
53
+ export type { SessionCredentials } from "./cryptoUtils";
package/src/types.ts CHANGED
@@ -17,12 +17,12 @@ export interface DepositToken {
17
17
  export type WidgetMessageType =
18
18
  | "widget:ready"
19
19
  | "widget:open-wallet"
20
- | "widget:card-created"
21
20
  | "widget:error"
22
21
  | "widget:close"
23
22
  | "widget:transaction-requested"
24
23
  | "widget:token-expired"
25
- | "widget:request-card-details-session";
24
+ | "widget:request-card-details-session"
25
+ | "widget:copy-to-clipboard";
26
26
 
27
27
  /**
28
28
  * Native Message Types
@@ -34,7 +34,8 @@ export type NativeMessageType =
34
34
  | "native:back-pressed"
35
35
  | "native:navigate-back"
36
36
  | "native:card-details-session"
37
- | "native:biometric-failed";
37
+ | "native:biometric-failed"
38
+ | "native:clipboard-result";
38
39
 
39
40
  export interface NativeBackPressedMessage {
40
41
  type: "native:back-pressed";
@@ -49,7 +50,7 @@ export interface NativeNavigateBackMessage {
49
50
  export interface NativePlatformInfoMessage {
50
51
  type: "native:platform-info";
51
52
  payload: {
52
- platform: string;
53
+ platform: "ios" | "android";
53
54
  walletAvailable: boolean;
54
55
  };
55
56
  }
@@ -77,13 +78,23 @@ export interface NativeBiometricFailedMessage {
77
78
  };
78
79
  }
79
80
 
81
+ export interface NativeClipboardResultMessage {
82
+ type: "native:clipboard-result";
83
+ payload: {
84
+ success: boolean;
85
+ label?: string; // Echo back the label for correlation
86
+ error?: string;
87
+ };
88
+ }
89
+
80
90
  export type NativeMessage =
81
91
  | NativeBackPressedMessage
82
92
  | NativeNavigateBackMessage
83
93
  | NativePlatformInfoMessage
84
94
  | NativeWalletOpenedMessage
85
95
  | NativeCardDetailsSessionMessage
86
- | NativeBiometricFailedMessage;
96
+ | NativeBiometricFailedMessage
97
+ | NativeClipboardResultMessage;
87
98
 
88
99
  export interface BaseWidgetMessage {
89
100
  type: WidgetMessageType;
@@ -101,15 +112,6 @@ export interface OpenWalletMessage extends BaseWidgetMessage {
101
112
  };
102
113
  }
103
114
 
104
- export interface CardCreatedMessage extends BaseWidgetMessage {
105
- type: "widget:card-created";
106
- payload: {
107
- cardId: string;
108
- cardType: "virtual" | "physical";
109
- last4: string;
110
- };
111
- }
112
-
113
115
  export interface WidgetErrorMessage extends BaseWidgetMessage {
114
116
  type: "widget:error";
115
117
  payload: {
@@ -146,15 +148,23 @@ export interface RequestCardDetailsSessionMessage extends BaseWidgetMessage {
146
148
  };
147
149
  }
148
150
 
151
+ export interface CopyToClipboardMessage extends BaseWidgetMessage {
152
+ type: "widget:copy-to-clipboard";
153
+ payload: {
154
+ text: string;
155
+ label?: string; // Optional: human-readable label for analytics/logging (e.g., "Card Number", "Deposit Address")
156
+ };
157
+ }
158
+
149
159
  export type WidgetMessage =
150
160
  | WidgetReadyMessage
151
161
  | OpenWalletMessage
152
- | CardCreatedMessage
153
162
  | WidgetErrorMessage
154
163
  | WidgetCloseMessage
155
164
  | TransactionRequestedMessage
156
165
  | TokenExpiredMessage
157
- | RequestCardDetailsSessionMessage;
166
+ | RequestCardDetailsSessionMessage
167
+ | CopyToClipboardMessage;
158
168
 
159
169
  /**
160
170
  * Widget environment configuration
@@ -163,6 +173,14 @@ export type WidgetMessage =
163
173
  */
164
174
  export type WidgetEnvironment = "development" | "production";
165
175
 
176
+ /**
177
+ * Known SDK error codes
178
+ * - TOKEN_EXPIRED: The access token has expired
179
+ * - PARSE_ERROR: Failed to parse a message from the widget
180
+ * - WEBVIEW_ERROR: The WebView failed to load
181
+ */
182
+ export type SDKErrorCode = "TOKEN_EXPIRED" | "PARSE_ERROR" | "WEBVIEW_ERROR";
183
+
166
184
  /**
167
185
  * SDK Configuration
168
186
  */
@@ -176,16 +194,44 @@ export interface WidgetSDKConfig {
176
194
  * @default 'production'
177
195
  */
178
196
  environment?: WidgetEnvironment;
197
+ /**
198
+ * Called when the widget is ready and loaded
199
+ */
179
200
  onReady?: () => void;
180
- onCardCreated?: (cardId: string, cardType: string, last4: string) => void;
181
- onError?: (code: string, message: string) => void;
201
+ /**
202
+ * Called when an error occurs
203
+ * @param code - Error code (SDKErrorCode or custom widget error code)
204
+ * @param message - Human-readable error message
205
+ */
206
+ onError?: (code: SDKErrorCode | string, message: string) => void;
207
+ /**
208
+ * Called when the widget requests to close
209
+ */
182
210
  onClose?: () => void;
211
+ /**
212
+ * Called when a crypto transaction is requested
213
+ */
183
214
  onTransactionRequested?: (
184
215
  token: DepositToken,
185
216
  cryptoAmount: string,
186
217
  depositAddress: string,
187
218
  depositAddressTag: string | null
188
219
  ) => void;
220
+ /**
221
+ * Called when the loading state changes
222
+ * @param isLoading - Whether the widget is currently loading
223
+ */
224
+ onLoadingChange?: (isLoading: boolean) => void;
225
+ /**
226
+ * Enable debug logging to console
227
+ * @default false
228
+ */
229
+ debug?: boolean;
230
+ /**
231
+ * Custom color for the loading indicator
232
+ * @default "#007AFF"
233
+ */
234
+ loadingIndicatorColor?: string;
189
235
  }
190
236
 
191
237
  /**
@@ -213,12 +259,12 @@ export const MessageTypes = {
213
259
  // Widget → Native
214
260
  READY: "widget:ready",
215
261
  OPEN_WALLET: "widget:open-wallet",
216
- CARD_CREATED: "widget:card-created",
217
262
  ERROR: "widget:error",
218
263
  CLOSE: "widget:close",
219
264
  TRANSACTION_REQUESTED: "widget:transaction-requested",
220
265
  TOKEN_EXPIRED: "widget:token-expired",
221
266
  REQUEST_CARD_DETAILS_SESSION: "widget:request-card-details-session",
267
+ COPY_TO_CLIPBOARD: "widget:copy-to-clipboard",
222
268
  // Native → Widget
223
269
  PLATFORM_INFO: "native:platform-info",
224
270
  WALLET_OPENED: "native:wallet-opened",
@@ -226,4 +272,5 @@ export const MessageTypes = {
226
272
  NAVIGATE_BACK: "native:navigate-back",
227
273
  CARD_DETAILS_SESSION: "native:card-details-session",
228
274
  BIOMETRIC_FAILED: "native:biometric-failed",
275
+ CLIPBOARD_RESULT: "native:clipboard-result",
229
276
  } as const;