@swype-org/deposit-mobile 0.2.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,450 @@
1
+ # @swype-org/deposit-mobile
2
+
3
+ > **Note:** For the best native UX (biometric passkey prompts with no browser chrome), use the platform-specific SDKs instead:
4
+ > - **iOS:** [`checkout-ios-sdk`](../checkout-ios-sdk/) — Swift, ASAuthorization, direct passkey signing
5
+ > - **Android:** [`checkout-android-sdk`](../checkout-android-sdk/) — Kotlin, Credential Manager, direct passkey signing
6
+ >
7
+ > This SDK remains available as a fallback for React Native / Expo apps that prefer the simpler in-app browser integration without native bridging.
8
+
9
+ Lightweight mobile deposit SDK — open a hosted Swype payment flow via in-app browser, handle deep-link completion, zero runtime dependencies.
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ npm install @swype-org/deposit-mobile
15
+ ```
16
+
17
+ ```typescript
18
+ import { MobileDeposit } from '@swype-org/deposit-mobile';
19
+
20
+ const deposit = new MobileDeposit({
21
+ signer: 'https://api.merchant.com/sign-payment',
22
+ callbackScheme: 'myapp',
23
+ openUrl: (url) => openInAppBrowser(url),
24
+ });
25
+
26
+ // Listen for deep links and pass them to the SDK
27
+ onDeepLink((url) => deposit.handleDeepLink(url));
28
+
29
+ // Start a deposit
30
+ const { transfer } = await deposit.requestDeposit({
31
+ amount: 50,
32
+ chainId: 8453,
33
+ address: '0x...',
34
+ token: 'USDC',
35
+ });
36
+
37
+ console.log('Transfer complete:', transfer.id, transfer.status);
38
+ ```
39
+
40
+ ## React Native Hook
41
+
42
+ A dedicated React Native entry point eliminates all boilerplate:
43
+
44
+ ```bash
45
+ npm install @swype-org/deposit-mobile react
46
+ ```
47
+
48
+ ```tsx
49
+ import { useBlinkMobileDeposit } from '@swype-org/deposit-mobile/react-native';
50
+ import * as Linking from 'expo-linking';
51
+ import * as WebBrowser from 'expo-web-browser';
52
+
53
+ function DepositButton() {
54
+ const { status, result, error, displayMessage, requestDeposit, handleDeepLink } =
55
+ useBlinkMobileDeposit({
56
+ signer: 'https://api.merchant.com/sign-payment',
57
+ callbackScheme: 'myapp',
58
+ openUrl: (url) => WebBrowser.openBrowserAsync(url).then(() => {}),
59
+ });
60
+
61
+ useEffect(() => {
62
+ const sub = Linking.addEventListener('url', ({ url }) => handleDeepLink(url));
63
+ return () => sub.remove();
64
+ }, [handleDeepLink]);
65
+
66
+ return (
67
+ <>
68
+ <Button
69
+ title={status === 'signer-loading' ? 'Preparing…' : 'Deposit $50'}
70
+ disabled={status === 'signer-loading'}
71
+ onPress={() =>
72
+ requestDeposit({ amount: 50, chainId: 8453, address: '0x...', token: 'USDC' })
73
+ }
74
+ />
75
+ {error && <Text>{displayMessage}</Text>}
76
+ {result && <Text>Transfer {result.transfer.id} complete!</Text>}
77
+ </>
78
+ );
79
+ }
80
+ ```
81
+
82
+ ## How It Works
83
+
84
+ ```
85
+ Merchant App / SDK Merchant Signer Hosted Flow (in-app browser)
86
+ │ │ │
87
+ │ 1. requestDeposit(request) │ │
88
+ │──────────────────────────────►│ │
89
+ │ │ 2. signer(data) or POST URL │
90
+ │ │ (includes callbackScheme │
91
+ │ │ and webviewBaseUrl) │
92
+ │ 3. { signature, payload, ...}│ │
93
+ │◄──────────────────────────────│ │
94
+ │ │
95
+ │ 4. SDK builds URL, openUrl() → in-app browser │
96
+ │──────────────────────────────────────────────────────────────►│
97
+ │ │
98
+ │ 5. User completes payment in hosted flow │
99
+ │ │
100
+ │ 6. Redirect → myapp://swype/callback?transferId=... │
101
+ │◄─────────────────────────────────────────────────────────────│
102
+ │ │
103
+ │ 7. handleDeepLink(url) → Promise resolves with DepositResult │
104
+ ```
105
+
106
+ ## Signer Contract
107
+
108
+ The `signer` config option controls how the SDK obtains a signed payment link. It accepts either a **URL string** or a **custom function**, giving you full control over authentication, HTTP method, and request shape. This is the same contract as `@swype-org/deposit` (web), except `callbackScheme` is set to the mobile app's URL scheme instead of `null`.
109
+
110
+ ### Using a URL string (simple)
111
+
112
+ When `signer` is a string, the SDK sends a `POST` with a JSON body to that URL and expects a `SignerResponse` back:
113
+
114
+ ```typescript
115
+ const deposit = new MobileDeposit({
116
+ signer: 'https://api.merchant.com/sign-payment',
117
+ callbackScheme: 'myapp',
118
+ openUrl: (url) => WebBrowser.openBrowserAsync(url).then(() => {}),
119
+ });
120
+ ```
121
+
122
+ ### Using a custom function (full control)
123
+
124
+ When `signer` is a function, the SDK calls it with a `SignerRequest` object and expects a `Promise<SignerResponse>`. Use this when you need control over the HTTP method, authentication, request transformation, or any other aspect of the signing call:
125
+
126
+ ```typescript
127
+ import type { SignerFunction } from '@swype-org/deposit-mobile';
128
+
129
+ const deposit = new MobileDeposit({
130
+ signer: async (data) => {
131
+ const res = await fetch('https://api.merchant.com/sign-payment', {
132
+ method: 'PATCH',
133
+ headers: {
134
+ 'Content-Type': 'application/json',
135
+ 'Authorization': `Bearer ${getToken()}`,
136
+ },
137
+ body: JSON.stringify({ ...data, orderId: 'order-123' }),
138
+ });
139
+ if (!res.ok) throw new Error(`Signer error: ${res.status}`);
140
+ return res.json();
141
+ },
142
+ callbackScheme: 'myapp',
143
+ openUrl: (url) => WebBrowser.openBrowserAsync(url).then(() => {}),
144
+ });
145
+ ```
146
+
147
+ ### `SignerRequest` (input)
148
+
149
+ The data the SDK passes to your signer (as the JSON body for URL mode, or as the function argument for function mode):
150
+
151
+ | Field | Type | Description |
152
+ | --- | --- | --- |
153
+ | `amount` | `number` | USD amount to deposit (always > 0). |
154
+ | `chainId` | `number` | EVM chain ID for the destination (e.g. `8453` for Base). |
155
+ | `address` | `string` | Destination wallet address (0x-prefixed, 40 hex chars). |
156
+ | `token` | `string` | Token symbol on the destination chain (e.g. `"USDC"`). |
157
+ | `callbackScheme` | `string` | URL scheme registered by the mobile app (e.g. `"myapp"`). The hosted flow redirects to `{callbackScheme}://swype/callback?...` on completion. |
158
+ | `url` | `string` | Base webview URL the SDK will navigate to. Provided for logging/validation — your signer does **not** construct the final URL. |
159
+ | `version` | `string` | Protocol version (currently `"v1"`). |
160
+ | `reference` | `string?` | Merchant order or invoice ID for reconciliation. |
161
+ | `metadata` | `object?` | Arbitrary key-value pairs forwarded from the merchant app. |
162
+
163
+ Example:
164
+
165
+ ```json
166
+ {
167
+ "amount": 50,
168
+ "chainId": 8453,
169
+ "address": "0x...",
170
+ "token": "USDC",
171
+ "callbackScheme": "myapp",
172
+ "url": "https://pay-staging.tryblink.xyz",
173
+ "version": "v1"
174
+ }
175
+ ```
176
+
177
+ ### What the signer must do
178
+
179
+ 1. **Validate** the request fields.
180
+ 2. **Generate an idempotency key** (UUID) for this payment.
181
+ 3. **Build a payload** — a base64url-encoded JSON string containing the payment parameters:
182
+
183
+ ```json
184
+ {
185
+ "amount": 50,
186
+ "chainId": 8453,
187
+ "address": "0x...",
188
+ "token": "USDC",
189
+ "idempotencyKey": "generated-uuid",
190
+ "callbackScheme": "myapp",
191
+ "expiresAt": "2026-03-07T12:00:00Z",
192
+ "version": "v1"
193
+ }
194
+ ```
195
+
196
+ 4. **Sign the payload** string with your merchant private key (SHA-256) and base64url-encode the signature.
197
+
198
+ ### `SignerResponse` (output)
199
+
200
+ The response your signer must return (as JSON for URL mode, or as the resolved value for function mode):
201
+
202
+ | Field | Type | Description |
203
+ | --- | --- | --- |
204
+ | `merchantId` | `string` | Your merchant UUID. |
205
+ | `payload` | `string` | Base64url-encoded payment payload (see above). |
206
+ | `signature` | `string` | Base64url-encoded signature of the payload string. |
207
+ | `expiresAt` | `string` | ISO 8601 expiration timestamp for this payment link. |
208
+ | `preview` | `object` | Echo of the payment parameters for client-side display. |
209
+ | `preview.amount` | `number` | Deposit amount. |
210
+ | `preview.chainId` | `number` | Destination chain ID. |
211
+ | `preview.address` | `string` | Destination wallet address. |
212
+ | `preview.token` | `string` | Destination token symbol. |
213
+ | `preview.idempotencyKey` | `string` | The generated idempotency key. |
214
+
215
+ Example:
216
+
217
+ ```json
218
+ {
219
+ "merchantId": "uuid",
220
+ "payload": "base64url-encoded-payload",
221
+ "signature": "base64url-encoded-signature",
222
+ "expiresAt": "2026-03-07T12:00:00Z",
223
+ "preview": {
224
+ "amount": 50,
225
+ "chainId": 8453,
226
+ "address": "0x...",
227
+ "token": "USDC",
228
+ "idempotencyKey": "uuid"
229
+ }
230
+ }
231
+ ```
232
+
233
+ The SDK constructs the hosted flow URL by appending `merchantId`, `payload`, and `signature` as query parameters to the `webviewBaseUrl`, then opens it in the in-app browser. When the user completes payment the hosted flow redirects to `{callbackScheme}://swype/callback?...` and the SDK resolves the promise.
234
+
235
+ ## Configuration
236
+
237
+ ```typescript
238
+ const deposit = new MobileDeposit({
239
+ // Required: URL string or custom async function (see "Signer Contract")
240
+ signer: 'https://api.merchant.com/sign-payment',
241
+
242
+ // Required: URL scheme registered by your mobile app (e.g. 'myapp')
243
+ callbackScheme: 'myapp',
244
+
245
+ // Required: function that opens a URL in an in-app browser
246
+ openUrl: (url) => WebBrowser.openBrowserAsync(url).then(() => {}),
247
+
248
+ // Optional: base URL of the hosted payment webview app.
249
+ // Default: 'https://pay-staging.tryblink.xyz'
250
+ webviewBaseUrl: 'https://pay-staging.tryblink.xyz',
251
+
252
+ // Optional: path for the callback deep link (default: '/swype/callback')
253
+ callbackPath: '/swype/callback',
254
+
255
+ // Optional: max ms to wait for signer response (default: 15000)
256
+ signerTimeoutMs: 15_000,
257
+
258
+ // Optional: max ms for entire flow (signer + user completion)
259
+ flowTimeoutMs: 300_000,
260
+
261
+ // Optional: enable debug logging to console
262
+ debug: false,
263
+ });
264
+ ```
265
+
266
+ ## Deep Link Setup
267
+
268
+ Your mobile app must be configured to handle the callback URL scheme.
269
+
270
+ ### Expo / React Native
271
+
272
+ In `app.json`:
273
+
274
+ ```json
275
+ {
276
+ "expo": {
277
+ "scheme": "myapp"
278
+ }
279
+ }
280
+ ```
281
+
282
+ ### iOS (native)
283
+
284
+ Register your URL scheme in `Info.plist`:
285
+
286
+ ```xml
287
+ <key>CFBundleURLTypes</key>
288
+ <array>
289
+ <dict>
290
+ <key>CFBundleURLSchemes</key>
291
+ <array>
292
+ <string>myapp</string>
293
+ </array>
294
+ </dict>
295
+ </array>
296
+ ```
297
+
298
+ ### Android (native)
299
+
300
+ Add an intent filter in `AndroidManifest.xml`:
301
+
302
+ ```xml
303
+ <activity ...>
304
+ <intent-filter>
305
+ <action android:name="android.intent.action.VIEW" />
306
+ <category android:name="android.intent.category.DEFAULT" />
307
+ <category android:name="android.intent.category.BROWSABLE" />
308
+ <data android:scheme="myapp" />
309
+ </intent-filter>
310
+ </activity>
311
+ ```
312
+
313
+ ## Handling Deep Links
314
+
315
+ The SDK does not set up deep link listeners itself — this keeps it platform-agnostic. You must forward incoming URLs to `handleDeepLink()`:
316
+
317
+ ```typescript
318
+ // Expo
319
+ import * as Linking from 'expo-linking';
320
+ Linking.addEventListener('url', ({ url }) => deposit.handleDeepLink(url));
321
+
322
+ // React Native (bare)
323
+ import { Linking } from 'react-native';
324
+ Linking.addEventListener('url', ({ url }) => deposit.handleDeepLink(url));
325
+
326
+ // iOS native (AppDelegate)
327
+ func application(_ app: UIApplication, open url: URL, options: ...) -> Bool {
328
+ // bridge call to deposit.handleDeepLink(url.absoluteString)
329
+ }
330
+
331
+ // Android native (Activity)
332
+ override fun onNewIntent(intent: Intent) {
333
+ intent.data?.toString()?.let { deposit.handleDeepLink(it) }
334
+ }
335
+ ```
336
+
337
+ `handleDeepLink()` returns `true` if the URL was a Swype callback, `false` otherwise.
338
+
339
+ ## Observable Status
340
+
341
+ | Status | Meaning |
342
+ | ---------------- | ------------------------------------------- |
343
+ | `idle` | No active flow |
344
+ | `signer-loading` | Calling the merchant signer endpoint |
345
+ | `browser-active` | In-app browser is open, waiting for user |
346
+ | `completed` | Transfer succeeded |
347
+ | `error` | Something failed |
348
+
349
+ ```typescript
350
+ deposit.on('status-change', (status) => console.log('Status:', status));
351
+ deposit.status; // current status
352
+ deposit.result; // last DepositResult (when completed)
353
+ deposit.error; // last DepositError (when error)
354
+ deposit.isActive; // true during signer-loading or browser-active
355
+ ```
356
+
357
+ ## Error Handling
358
+
359
+ Every error is a `DepositError` with a machine-readable `code`:
360
+
361
+ | Code | Meaning |
362
+ | ------------------------ | ----------------------------------------------- |
363
+ | `BROWSER_FAILED` | Failed to open the in-app browser |
364
+ | `BROWSER_DISMISSED` | User dismissed the browser before completing |
365
+ | `DEEP_LINK_INVALID` | Callback deep link was malformed |
366
+ | `SIGNER_REQUEST_FAILED` | Signer returned a non-2xx response |
367
+ | `SIGNER_NETWORK_ERROR` | Network failure reaching the signer |
368
+ | `SIGNER_RESPONSE_INVALID`| Signer response missing required fields |
369
+ | `SIGNER_TIMEOUT` | Signer did not respond within `signerTimeoutMs` |
370
+ | `FLOW_TIMEOUT` | Entire flow exceeded `flowTimeoutMs` |
371
+ | `INVALID_REQUEST` | Bad input (amount, address, etc.) |
372
+
373
+ Use `getDisplayMessage()` for user-facing strings:
374
+
375
+ ```typescript
376
+ import { DepositError, getDisplayMessage } from '@swype-org/deposit-mobile';
377
+
378
+ try {
379
+ await deposit.requestDeposit({ ... });
380
+ } catch (err) {
381
+ if (err instanceof DepositError) {
382
+ Alert.alert('Payment Error', getDisplayMessage(err));
383
+ }
384
+ }
385
+ ```
386
+
387
+ ## Events
388
+
389
+ ```typescript
390
+ deposit.on('complete', (result) => { /* DepositResult */ });
391
+ deposit.on('error', (error) => { /* DepositError */ });
392
+ deposit.on('dismiss', () => { /* browser dismissed */ });
393
+ deposit.on('status-change', (status) => { /* MobileDepositStatus */ });
394
+ ```
395
+
396
+ ## Lifecycle
397
+
398
+ ```typescript
399
+ // Cancel the current flow and reset to idle
400
+ deposit.close();
401
+
402
+ // Tear down and release all resources (call on unmount)
403
+ deposit.destroy();
404
+ ```
405
+
406
+ ## Comparison with Other Swype SDKs
407
+
408
+ | Aspect | `@swype-org/deposit` (web) | `@swype-org/deposit-mobile` | `checkout-ios-sdk` | `checkout-android-sdk` |
409
+ | ----------------- | ------------------------------ | ---------------------------------- | -------------------------------- | -------------------------------- |
410
+ | Platform | Browser | React Native / iOS / Android | iOS 16+ | Android 9+ (API 28) |
411
+ | Language | TypeScript | TypeScript | Swift | Kotlin |
412
+ | Passkey handling | Hosted flow (iframe) | Hosted flow (in-app browser) | Native ASAuthorization | Native Credential Manager |
413
+ | UX | Modal iframe overlay | In-app browser with browser chrome | Direct biometric prompt | Direct biometric prompt |
414
+ | Flow mechanism | iframe + `postMessage` | In-app browser + deep link | Direct API calls + passkey | Direct API calls + passkey |
415
+ | Completion signal | `postMessage` | URL scheme callback | `async` return value | Coroutine return value |
416
+ | Dependencies | None | None | None (Apple frameworks only) | Credential Manager, OkHttp |
417
+
418
+ ## TypeScript
419
+
420
+ All types are exported:
421
+
422
+ ```typescript
423
+ import type {
424
+ MobileDepositConfig,
425
+ MobileDepositStatus,
426
+ DepositRequest,
427
+ DepositResult,
428
+ SignerFunction,
429
+ SignerRequest,
430
+ SignerResponse,
431
+ TransferSummary,
432
+ } from '@swype-org/deposit-mobile';
433
+
434
+ import type { DepositMobileErrorCode } from '@swype-org/deposit-mobile';
435
+ ```
436
+
437
+ ## Migrating from @swype-org/checkout-mobile
438
+
439
+ All old names are available as deprecated aliases. Update at your own pace:
440
+
441
+ | Old name | New name |
442
+ | --- | --- |
443
+ | `MobileCheckout` | `MobileDeposit` |
444
+ | `MobileCheckoutConfig` | `MobileDepositConfig` |
445
+ | `MobileCheckoutStatus` | `MobileDepositStatus` |
446
+ | `CheckoutError` | `DepositError` |
447
+ | `CheckoutErrorCode` | `DepositMobileErrorCode` |
448
+ | `useSwypeMobileCheckout` | `useBlinkMobileDeposit` |
449
+
450
+ The deprecated aliases will be removed in a future major version.