@yumi-finance/widget 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 +384 -0
- package/dist/loader/iframe-manager.d.ts +10 -0
- package/dist/loader/index.d.ts +1 -0
- package/dist/loader/message-bridge.d.ts +16 -0
- package/dist/loader/types.d.ts +57 -0
- package/dist/loader/yumi-client.d.ts +16 -0
- package/dist/react.js +75 -0
- package/dist/shared/types/flow.d.ts +64 -0
- package/dist/shared/types/messages.d.ts +60 -0
- package/dist/yumi-widget.js +290 -0
- package/dist/yumi-widget.min.js +1 -0
- package/package.json +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Yumi Widget
|
|
2
|
+
|
|
3
|
+
## How to run the project
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/Yumi-Finance/yumi-widget
|
|
7
|
+
npm install
|
|
8
|
+
cp .env.example .env
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- **Checkout (iframe):** [http://localhost:3001](http://localhost:3001)
|
|
13
|
+
- **Demo:** [http://localhost:8080/demo/](http://localhost:8080/demo/)
|
|
14
|
+
|
|
15
|
+
Edit `.env` if you need different `VITE_PRIVY_APP_ID`, `VITE_CHECKOUT_URL`, or `VITE_YUMI_API_BASE_URL`.
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
|
|
19
|
+
**Script (HTML):**
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<!DOCTYPE html>
|
|
23
|
+
<html>
|
|
24
|
+
<head>
|
|
25
|
+
<title>Checkout</title>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<button id="pay">Pay with Yumi</button>
|
|
29
|
+
<script src="https://cdn.jsdelivr.net/npm/@yumi-finance/widget@1.0.0/dist/yumi-widget.min.js"></script>
|
|
30
|
+
<script>
|
|
31
|
+
const yumi = Yumi.init({
|
|
32
|
+
publicKey: 'pk_live_xxx',
|
|
33
|
+
getAppSignature: async (payload) => {
|
|
34
|
+
const res = await fetch('/api/sign', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify(payload),
|
|
38
|
+
})
|
|
39
|
+
const data = await res.json()
|
|
40
|
+
if (!res.ok || data.error) throw new Error(data.error || 'Sign failed')
|
|
41
|
+
return data.appSignature
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
document.getElementById('pay').onclick = () => {
|
|
45
|
+
yumi.open({ orderId: 'order_' + Date.now(), amount: 19900 })
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**React:**
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { YumiProvider, useYumi } from '@yumi-finance/widget/react'
|
|
56
|
+
|
|
57
|
+
function CheckoutButton() {
|
|
58
|
+
const { open, isReady, error } = useYumi()
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
{error && <p>Error: {error.message}</p>}
|
|
63
|
+
<button
|
|
64
|
+
disabled={!isReady}
|
|
65
|
+
onClick={() => open({ orderId: 'order_' + Date.now(), amount: 19900 })}
|
|
66
|
+
>
|
|
67
|
+
Pay with Yumi
|
|
68
|
+
</button>
|
|
69
|
+
</>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function App() {
|
|
74
|
+
return (
|
|
75
|
+
<YumiProvider
|
|
76
|
+
config={{
|
|
77
|
+
publicKey: 'pk_live_xxx',
|
|
78
|
+
getAppSignature: async (payload) => {
|
|
79
|
+
const res = await fetch('/api/sign', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify(payload),
|
|
83
|
+
})
|
|
84
|
+
const data = await res.json()
|
|
85
|
+
if (!res.ok || data.error) throw new Error(data.error || 'Sign failed')
|
|
86
|
+
return data.appSignature
|
|
87
|
+
},
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<CheckoutButton />
|
|
91
|
+
</YumiProvider>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
# Yumi Widget Integration
|
|
99
|
+
|
|
100
|
+
Yumi BNPL integration: client (merchant site), merchant server, and Yumi Server SDK for signing.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Overview
|
|
105
|
+
|
|
106
|
+
A signature is required to authorize the user in Yumi after login via Privy in the iframe. The widget gives the merchant a payload with `publicKey`, `timestamp`, `nonce`, `privyUserId`; the merchant returns a signature; the SDK sends it to Yumi on login.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 1. Client (Frontend)
|
|
111
|
+
|
|
112
|
+
### 1.1. Script Integration
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
<script src="https://cdn.jsdelivr.net/npm/@yumi-finance/widget@1.0.0/dist/yumi-widget.min.js"></script>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Or from your own CDN:
|
|
119
|
+
|
|
120
|
+
```html
|
|
121
|
+
<script src="https://cdn.yumi.com/yumi-widget.js"></script>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 1.2. React
|
|
125
|
+
|
|
126
|
+
Install the package and wrap your app (or the part that needs checkout) with `YumiProvider`. Use the `useYumi()` hook to open checkout.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm install @yumi-finance/widget
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { YumiProvider, useYumi } from '@yumi-finance/widget/react'
|
|
134
|
+
|
|
135
|
+
function PayButton() {
|
|
136
|
+
const { open, isReady } = useYumi()
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<button
|
|
140
|
+
disabled={!isReady}
|
|
141
|
+
onClick={() => open({ orderId: 'order_123', amount: 19900 })}
|
|
142
|
+
>
|
|
143
|
+
Pay with Yumi
|
|
144
|
+
</button>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function App() {
|
|
149
|
+
return (
|
|
150
|
+
<YumiProvider
|
|
151
|
+
config={{
|
|
152
|
+
publicKey: 'pk_live_xxx',
|
|
153
|
+
getAppSignature: async (payload) => {
|
|
154
|
+
const res = await fetch('/your-api/sign', {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify(payload),
|
|
158
|
+
})
|
|
159
|
+
const data = await res.json()
|
|
160
|
+
if (!res.ok || data.error) throw new Error(data.error || 'Failed to get signature')
|
|
161
|
+
return data.appSignature
|
|
162
|
+
},
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<PayButton />
|
|
166
|
+
</YumiProvider>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Optional: pass `scriptUrl` to load the loader from a custom URL (default: jsDelivr). `useYumi()` returns `{ open, close, isReady, error }`.
|
|
172
|
+
|
|
173
|
+
### 1.3. Initialization and Checkout Opening (script tag only)
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
const yumi = Yumi.init({
|
|
177
|
+
publicKey: 'pk_live_xxx',
|
|
178
|
+
getAppSignature: async payload => {
|
|
179
|
+
const res = await fetch('/your-api/sign', {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify(payload),
|
|
183
|
+
})
|
|
184
|
+
const data = await res.json()
|
|
185
|
+
if (!res.ok || data.error) {
|
|
186
|
+
throw new Error(data.error || 'Failed to get signature')
|
|
187
|
+
}
|
|
188
|
+
return data.appSignature
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
document.getElementById('payBtn').addEventListener('click', () => {
|
|
193
|
+
yumi.open({
|
|
194
|
+
orderId: 'order_123',
|
|
195
|
+
amount: 19900,
|
|
196
|
+
currency: 'USD',
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 1.4. `Yumi.init()` Parameters
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
| Parameter | Required | Description |
|
|
205
|
+
| ----------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
206
|
+
| `publicKey` | Yes | Merchant public key (from Yumi). Used in the payload for signing and to identify the merchant on the backend. |
|
|
207
|
+
| `getAppSignature` | Yes (production) | Callback `(payload) => Promise<string>`. Widget passes `{ publicKey, timestamp, nonce, privyUserId }`; merchant returns `appSignature` (e.g. from their backend). |
|
|
208
|
+
| `onClose` | No | Callback when checkout is closed. |
|
|
209
|
+
| `onEvent` | No | Callback for checkout events. |
|
|
210
|
+
| `onError` | No | Callback for errors. |
|
|
211
|
+
| `meshConfig` | No | Optional Mesh Link config (clientId, clientSecret, useSandbox). |
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
### 1.5. Payload for Signing
|
|
215
|
+
|
|
216
|
+
In `getAppSignature(payload)` you receive:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
{
|
|
220
|
+
publicKey: string // same publicKey passed to init
|
|
221
|
+
timestamp: number // Unix timestamp (seconds)
|
|
222
|
+
nonce: string // unique string
|
|
223
|
+
privyUserId: string // Privy user ID after login in iframe
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The merchant sends this payload to their backend; the backend signs it and returns `appSignature`. On the backend you can use `payload.privyUserId`.
|
|
228
|
+
|
|
229
|
+
### 1.6. `yumi.open()` Options
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
| Option | Required | Description |
|
|
233
|
+
| ---------- | -------- | ------------------------------------------------ |
|
|
234
|
+
| `orderId` | Yes | Order/intent ID (e.g. `order_123` or intent id). |
|
|
235
|
+
| `amount` | Yes | Amount in smallest units (e.g. cents). |
|
|
236
|
+
| `currency` | No | Currency code (e.g. `USD`). |
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 2. Merchant Server and Yumi Server SDK
|
|
242
|
+
|
|
243
|
+
### 2.1. Yumi Server SDK Purpose
|
|
244
|
+
|
|
245
|
+
**Yumi Server SDK** is a library for the merchant backend. It does not start an HTTP server and does not call the Yumi API. The SDK provides **only a signing API**: input is the login payload, output is the `appSignature` string (Ed25519, canonicalize, base64).
|
|
246
|
+
|
|
247
|
+
The merchant installs the SDK on the server, stores the secret key (from Yumi), and in their endpoint receives the payload from the frontend, calls the signing method, and returns the signature in the response.
|
|
248
|
+
|
|
249
|
+
### 2.2. Installation
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
npm install @yumi/backend-sdk
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 2.3. Signing API
|
|
256
|
+
|
|
257
|
+
**Method:** sign payload for login.
|
|
258
|
+
|
|
259
|
+
**Input (payload):**
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
{
|
|
263
|
+
publicKey: string
|
|
264
|
+
timestamp: number
|
|
265
|
+
nonce: string
|
|
266
|
+
privyUserId: string
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Output:** `appSignature` string (base64 Ed25519 signature of canonicalize(payload) with the merchant secret key).
|
|
271
|
+
|
|
272
|
+
**Example (Node.js):**
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
const { signLoginPayload } = require('@yumi/backend-sdk')
|
|
276
|
+
|
|
277
|
+
app.post('/your-api/sign', async (req, res) => {
|
|
278
|
+
try {
|
|
279
|
+
const payload = req.body
|
|
280
|
+
const appSignature = signLoginPayload(payload, {
|
|
281
|
+
secretKeyBase64: process.env.YUMI_SECRET_KEY,
|
|
282
|
+
})
|
|
283
|
+
res.json({ appSignature })
|
|
284
|
+
} catch (err) {
|
|
285
|
+
res.status(400).json({ error: err.message })
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Example with pre-configuration:**
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
const { YumiBackend } = require('@yumi/backend-sdk')
|
|
294
|
+
|
|
295
|
+
const yumi = new YumiBackend({
|
|
296
|
+
secretKeyBase64: process.env.YUMI_SECRET_KEY,
|
|
297
|
+
publicKeyBase64: process.env.YUMI_PUBLIC_KEY,
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
app.post('/your-api/sign', async (req, res) => {
|
|
301
|
+
try {
|
|
302
|
+
const payload = req.body
|
|
303
|
+
const appSignature = yumi.signLoginPayload(payload)
|
|
304
|
+
res.json({ appSignature })
|
|
305
|
+
} catch (err) {
|
|
306
|
+
res.status(400).json({ error: err.message })
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Package and method names may differ; the SDK provides only signing of the payload and returns `appSignature`.
|
|
312
|
+
|
|
313
|
+
### 2.4. Security
|
|
314
|
+
|
|
315
|
+
- Store the secret key (`YUMI_SECRET_KEY`) only on the backend (env or secret store).
|
|
316
|
+
- Yumi Server SDK does not make network requests; it only computes the signature.
|
|
317
|
+
- The signing endpoint should be callable only from your frontend (CORS; optionally check origin or token).
|
|
318
|
+
|
|
319
|
+
### 2.5. Without Server SDK
|
|
320
|
+
|
|
321
|
+
The merchant can implement signing themselves: same algorithm (Ed25519, canonicalize payload, base64). Payload contract and `appSignature` format are as above; details are in Yumi cryptography docs. Using the Server SDK is recommended to avoid reimplementing the logic.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## 3. Login Sequence
|
|
326
|
+
|
|
327
|
+
1. User logs in inside the Yumi iframe with Privy.
|
|
328
|
+
2. SDK in the iframe builds the payload: `publicKey` (from init), `timestamp`, `nonce`, `privyUserId`.
|
|
329
|
+
3. SDK calls `getAppSignature(payload)` — the request is handled by the merchant frontend.
|
|
330
|
+
4. Merchant frontend sends the payload to their backend (e.g. `POST /your-api/sign`).
|
|
331
|
+
5. Backend signs the payload (Yumi Server SDK or custom) and returns `{ appSignature }`.
|
|
332
|
+
6. Frontend returns `appSignature` to the widget callback.
|
|
333
|
+
7. Widget sends the login request to Yumi with `accessToken`, `identityToken`, `payload`, and `appSignature`.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 4. Quick Checklist
|
|
338
|
+
|
|
339
|
+
**Client:**
|
|
340
|
+
|
|
341
|
+
- Include `yumi-widget.js` (CDN or self-hosted).
|
|
342
|
+
- `Yumi.init({ publicKey, getAppSignature, ... })` — in `getAppSignature` call your backend and return `appSignature`.
|
|
343
|
+
- On payment button click: `yumi.open({ orderId, amount, currency? })`.
|
|
344
|
+
|
|
345
|
+
**Server:**
|
|
346
|
+
|
|
347
|
+
- Install Yumi Server SDK (or implement signing yourself).
|
|
348
|
+
- Endpoint: accept payload, call signing, return `{ appSignature }`.
|
|
349
|
+
- Configure CORS for the frontend domain and, if needed, access checks.
|
|
350
|
+
|
|
351
|
+
**Yumi Server SDK:** provides only the signing API (one method for the login payload); it does not provide HTTP and does not call the Yumi API.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 5. Development
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
npm install
|
|
359
|
+
npm run dev
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
- Checkout (iframe): [http://localhost:3001](http://localhost:3001)
|
|
363
|
+
- Demo: [http://localhost:8080/demo/](http://localhost:8080/demo/)
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
npm run build
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Builds loader (`dist/yumi-widget.js`, `dist/yumi-widget.min.js`) and checkout (`dist/checkout/`).
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
| Command | Description |
|
|
373
|
+
| ---------------------- | ------------------------------------- |
|
|
374
|
+
| `npm run dev` | Loader watch + checkout + demo server |
|
|
375
|
+
| `npm run build` | Production build |
|
|
376
|
+
| `npm run dev:loader` | Loader only (watch) |
|
|
377
|
+
| `npm run dev:checkout` | Checkout only (Vite, port 3001) |
|
|
378
|
+
| `npm run type-check` | TypeScript check |
|
|
379
|
+
| `npm run lint` | ESLint |
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
## License
|
|
383
|
+
|
|
384
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HostMessage } from '../shared/types/messages';
|
|
2
|
+
export declare class MessageBridge {
|
|
3
|
+
private targetWindow;
|
|
4
|
+
private allowedOrigin;
|
|
5
|
+
private onEvent;
|
|
6
|
+
private messageHandler;
|
|
7
|
+
constructor(options: {
|
|
8
|
+
onEvent: (event: any) => void;
|
|
9
|
+
});
|
|
10
|
+
connect(targetWindow: Window | null, options: {
|
|
11
|
+
allowedOrigin: string;
|
|
12
|
+
}): void;
|
|
13
|
+
disconnect(): void;
|
|
14
|
+
send(message: HostMessage): void;
|
|
15
|
+
private handleMessage;
|
|
16
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { SignLoginPayload } from '../shared/types/messages';
|
|
2
|
+
export interface YumiConfig {
|
|
3
|
+
publicKey: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
/** Callback to get app signature for login. Receives payload from iframe; returns signature (e.g. from merchant backend). */
|
|
6
|
+
getAppSignature: (payload: SignLoginPayload) => Promise<string>;
|
|
7
|
+
onEvent?: (event: YumiEvent) => void;
|
|
8
|
+
onClose?: () => void;
|
|
9
|
+
onError?: (error: any) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface OpenOptions {
|
|
12
|
+
orderId: string;
|
|
13
|
+
amount: number;
|
|
14
|
+
}
|
|
15
|
+
export type YumiEvent = {
|
|
16
|
+
type: 'READY';
|
|
17
|
+
} | {
|
|
18
|
+
type: 'LOADED';
|
|
19
|
+
} | {
|
|
20
|
+
type: 'RESIZE';
|
|
21
|
+
payload: {
|
|
22
|
+
height: number;
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
type: 'CLOSE';
|
|
26
|
+
} | {
|
|
27
|
+
type: 'SUCCESS';
|
|
28
|
+
payload: {
|
|
29
|
+
intentId: string;
|
|
30
|
+
status: string;
|
|
31
|
+
};
|
|
32
|
+
} | {
|
|
33
|
+
type: 'ERROR';
|
|
34
|
+
payload: {
|
|
35
|
+
code: string;
|
|
36
|
+
message: string;
|
|
37
|
+
};
|
|
38
|
+
} | {
|
|
39
|
+
type: 'EVENT';
|
|
40
|
+
payload: {
|
|
41
|
+
name: string;
|
|
42
|
+
data: any;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
declare global {
|
|
46
|
+
interface Window {
|
|
47
|
+
Yumi?: {
|
|
48
|
+
init: (config: YumiConfig) => YumiClient;
|
|
49
|
+
version: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export interface YumiClient {
|
|
54
|
+
open: (options: OpenOptions) => Promise<void>;
|
|
55
|
+
close: () => void;
|
|
56
|
+
destroy: () => void;
|
|
57
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { YumiConfig, OpenOptions } from './types';
|
|
2
|
+
export declare class YumiClient {
|
|
3
|
+
private config;
|
|
4
|
+
private iframeManager;
|
|
5
|
+
private bridge;
|
|
6
|
+
private currentSessionId;
|
|
7
|
+
private currentOpenOptions;
|
|
8
|
+
constructor(config: YumiConfig);
|
|
9
|
+
open(options: OpenOptions): Promise<void>;
|
|
10
|
+
close(): void;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
private handleIframeEvent;
|
|
13
|
+
private getCheckoutUrl;
|
|
14
|
+
private getCheckoutOrigin;
|
|
15
|
+
private generateSessionId;
|
|
16
|
+
}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as h } from "react/jsx-runtime";
|
|
2
|
+
import { createContext as v, useRef as f, useState as m, useEffect as x, useCallback as w, useContext as R } from "react";
|
|
3
|
+
const g = "https://cdn.jsdelivr.net/npm/@yumi/widget@1.0.0/dist/yumi-widget.min.js", p = v(null);
|
|
4
|
+
function C(n) {
|
|
5
|
+
return new Promise((o, u) => {
|
|
6
|
+
if (typeof document > "u") {
|
|
7
|
+
u(new Error("Document is not available"));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const t = document.querySelector(`script[src="${n}"]`);
|
|
11
|
+
if (t) {
|
|
12
|
+
const c = window;
|
|
13
|
+
if (c.Yumi) {
|
|
14
|
+
o();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let d = !1;
|
|
18
|
+
const s = (l) => {
|
|
19
|
+
d || (d = !0, l ? u(l) : o());
|
|
20
|
+
};
|
|
21
|
+
t.addEventListener("load", () => s()), t.addEventListener("error", () => s(new Error("Failed to load Yumi script"))), setTimeout(() => {
|
|
22
|
+
c.Yumi && s();
|
|
23
|
+
}, 0);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const i = document.createElement("script");
|
|
27
|
+
i.src = n, i.async = !0, i.onload = () => o(), i.onerror = () => u(new Error("Failed to load Yumi script")), document.head.appendChild(i);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function b({ config: n, scriptUrl: o, children: u }) {
|
|
31
|
+
const t = f(null), [i, c] = m(!1), [d, s] = m(null), l = f(n);
|
|
32
|
+
l.current = n, x(() => {
|
|
33
|
+
let r = !1;
|
|
34
|
+
return C(o ?? g).then(() => {
|
|
35
|
+
if (r) return;
|
|
36
|
+
const e = window.Yumi;
|
|
37
|
+
if (!(e != null && e.init)) {
|
|
38
|
+
s(new Error("Yumi widget not available"));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
t.current = e.init(l.current), c(!0);
|
|
42
|
+
}).catch((e) => {
|
|
43
|
+
r || s(e instanceof Error ? e : new Error(String(e)));
|
|
44
|
+
}), () => {
|
|
45
|
+
var e;
|
|
46
|
+
r = !0, (e = t.current) == null || e.destroy(), t.current = null, c(!1);
|
|
47
|
+
};
|
|
48
|
+
}, [o]);
|
|
49
|
+
const E = w(async (r) => {
|
|
50
|
+
const a = t.current;
|
|
51
|
+
if (!a)
|
|
52
|
+
throw new Error("Yumi widget is not ready yet");
|
|
53
|
+
await a.open(r);
|
|
54
|
+
}, []), y = w(() => {
|
|
55
|
+
var r;
|
|
56
|
+
(r = t.current) == null || r.close();
|
|
57
|
+
}, []), Y = {
|
|
58
|
+
open: E,
|
|
59
|
+
close: y,
|
|
60
|
+
isReady: i,
|
|
61
|
+
error: d
|
|
62
|
+
};
|
|
63
|
+
return /* @__PURE__ */ h(p.Provider, { value: Y, children: u });
|
|
64
|
+
}
|
|
65
|
+
function L() {
|
|
66
|
+
const n = R(p);
|
|
67
|
+
if (n === null)
|
|
68
|
+
throw new Error("useYumi must be used within YumiProvider");
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
p as YumiContext,
|
|
73
|
+
b as YumiProvider,
|
|
74
|
+
L as useYumi
|
|
75
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type DataSource = 'wallet' | 'cex' | 'bank';
|
|
2
|
+
export type KycProviders = {
|
|
3
|
+
plaid?: {
|
|
4
|
+
isPassed: boolean;
|
|
5
|
+
};
|
|
6
|
+
mesh?: {
|
|
7
|
+
isPassed: boolean;
|
|
8
|
+
};
|
|
9
|
+
didit?: {
|
|
10
|
+
isPassed: boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export type Step = 'idle' | 'login' | 'selectSource' | 'initialSync' | 'connectingBank' | 'connectingCex' | 'addWallet' | 'verifyDidit' | 'verificationSuccess' | 'underwriting' | 'otimDelegation' | 'underwritingDeclined' | 'syncProviders' | 'checkLimit' | 'limitDeclined' | 'confirmation';
|
|
14
|
+
export type User = {
|
|
15
|
+
id: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
};
|
|
19
|
+
export type FlowState = {
|
|
20
|
+
step: Step;
|
|
21
|
+
user: User | null;
|
|
22
|
+
dataSource: DataSource | null;
|
|
23
|
+
kycProviders: KycProviders | null;
|
|
24
|
+
metadata: Record<string, any>;
|
|
25
|
+
error: string | null;
|
|
26
|
+
};
|
|
27
|
+
export type FlowAction = {
|
|
28
|
+
type: 'START';
|
|
29
|
+
user?: User | null;
|
|
30
|
+
initialSource?: DataSource;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'LOGIN_SUCCESS';
|
|
33
|
+
user: User;
|
|
34
|
+
kycProviders?: KycProviders | null;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'SET_STEP';
|
|
37
|
+
step: Step;
|
|
38
|
+
data?: any;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'SELECT_SOURCE';
|
|
41
|
+
source: DataSource;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'CONTINUE';
|
|
44
|
+
data?: any;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'GO_BACK';
|
|
47
|
+
} | {
|
|
48
|
+
type: 'RESET';
|
|
49
|
+
} | {
|
|
50
|
+
type: 'LIMIT_APPROVED';
|
|
51
|
+
} | {
|
|
52
|
+
type: 'LIMIT_DECLINED';
|
|
53
|
+
} | {
|
|
54
|
+
type: 'UNDERWRITING_APPROVED';
|
|
55
|
+
} | {
|
|
56
|
+
type: 'UNDERWRITING_DECLINED';
|
|
57
|
+
} | {
|
|
58
|
+
type: 'SET_ERROR';
|
|
59
|
+
error: string;
|
|
60
|
+
} | {
|
|
61
|
+
type: 'SET_METADATA';
|
|
62
|
+
key: string;
|
|
63
|
+
value: any;
|
|
64
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type InitPayload = {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
publicKey: string;
|
|
4
|
+
privyAppId: string;
|
|
5
|
+
orderId: string;
|
|
6
|
+
amount?: number;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
};
|
|
9
|
+
/** Payload for signing login (publicKey, timestamp, nonce, privyUserId). */
|
|
10
|
+
export type SignLoginPayload = {
|
|
11
|
+
publicKey: string;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
nonce: string;
|
|
14
|
+
privyUserId: string;
|
|
15
|
+
};
|
|
16
|
+
export type HostMessage = {
|
|
17
|
+
type: 'INIT';
|
|
18
|
+
payload: InitPayload;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'CLOSE_REQUEST';
|
|
21
|
+
} | {
|
|
22
|
+
type: 'APP_SIGNATURE_RESPONSE';
|
|
23
|
+
requestId: string;
|
|
24
|
+
appSignature?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
};
|
|
27
|
+
export type IframeMessage = {
|
|
28
|
+
type: 'LOADED';
|
|
29
|
+
} | {
|
|
30
|
+
type: 'READY';
|
|
31
|
+
} | {
|
|
32
|
+
type: 'RESIZE';
|
|
33
|
+
payload: {
|
|
34
|
+
height: number;
|
|
35
|
+
};
|
|
36
|
+
} | {
|
|
37
|
+
type: 'CLOSE';
|
|
38
|
+
} | {
|
|
39
|
+
type: 'SUCCESS';
|
|
40
|
+
payload: {
|
|
41
|
+
intentId: string;
|
|
42
|
+
status: string;
|
|
43
|
+
};
|
|
44
|
+
} | {
|
|
45
|
+
type: 'ERROR';
|
|
46
|
+
payload: {
|
|
47
|
+
code: string;
|
|
48
|
+
message: string;
|
|
49
|
+
};
|
|
50
|
+
} | {
|
|
51
|
+
type: 'EVENT';
|
|
52
|
+
payload: {
|
|
53
|
+
name: string;
|
|
54
|
+
data: any;
|
|
55
|
+
};
|
|
56
|
+
} | {
|
|
57
|
+
type: 'GET_APP_SIGNATURE';
|
|
58
|
+
payload: SignLoginPayload;
|
|
59
|
+
requestId: string;
|
|
60
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
class IframeManager {
|
|
5
|
+
constructor() {
|
|
6
|
+
Object.defineProperty(this, "iframe", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
writable: true,
|
|
10
|
+
value: null
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(this, "overlay", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: null
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
create(options) {
|
|
20
|
+
this.overlay = document.createElement('div');
|
|
21
|
+
this.overlay.id = 'yumi-checkout-overlay';
|
|
22
|
+
this.overlay.style.cssText = `
|
|
23
|
+
position: fixed;
|
|
24
|
+
inset: 0;
|
|
25
|
+
z-index: 999999;
|
|
26
|
+
background: rgba(0, 0, 0, 0.5);
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
`;
|
|
31
|
+
this.iframe = document.createElement('iframe');
|
|
32
|
+
this.iframe.id = 'yumi-checkout-iframe';
|
|
33
|
+
this.iframe.src = options.url;
|
|
34
|
+
this.iframe.style.cssText = `
|
|
35
|
+
width: 100%;
|
|
36
|
+
max-width: 500px;
|
|
37
|
+
height: 90vh;
|
|
38
|
+
max-height: 800px;
|
|
39
|
+
border: none;
|
|
40
|
+
border-radius: 12px;
|
|
41
|
+
background: white;
|
|
42
|
+
`;
|
|
43
|
+
this.iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox');
|
|
44
|
+
this.iframe.setAttribute('allow', 'payment; clipboard-read; clipboard-write');
|
|
45
|
+
this.overlay.appendChild(this.iframe);
|
|
46
|
+
document.body.appendChild(this.overlay);
|
|
47
|
+
return this.iframe;
|
|
48
|
+
}
|
|
49
|
+
resize(height) {
|
|
50
|
+
if (this.iframe) {
|
|
51
|
+
this.iframe.style.height = `${Math.min(height, window.innerHeight * 0.9)}px`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
destroy() {
|
|
55
|
+
if (this.overlay) {
|
|
56
|
+
this.overlay.remove();
|
|
57
|
+
this.overlay = null;
|
|
58
|
+
}
|
|
59
|
+
this.iframe = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class MessageBridge {
|
|
64
|
+
constructor(options) {
|
|
65
|
+
Object.defineProperty(this, "targetWindow", {
|
|
66
|
+
enumerable: true,
|
|
67
|
+
configurable: true,
|
|
68
|
+
writable: true,
|
|
69
|
+
value: null
|
|
70
|
+
});
|
|
71
|
+
Object.defineProperty(this, "allowedOrigin", {
|
|
72
|
+
enumerable: true,
|
|
73
|
+
configurable: true,
|
|
74
|
+
writable: true,
|
|
75
|
+
value: '*'
|
|
76
|
+
});
|
|
77
|
+
Object.defineProperty(this, "onEvent", {
|
|
78
|
+
enumerable: true,
|
|
79
|
+
configurable: true,
|
|
80
|
+
writable: true,
|
|
81
|
+
value: void 0
|
|
82
|
+
});
|
|
83
|
+
Object.defineProperty(this, "messageHandler", {
|
|
84
|
+
enumerable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
writable: true,
|
|
87
|
+
value: void 0
|
|
88
|
+
});
|
|
89
|
+
this.onEvent = options.onEvent;
|
|
90
|
+
this.messageHandler = this.handleMessage.bind(this);
|
|
91
|
+
}
|
|
92
|
+
connect(targetWindow, options) {
|
|
93
|
+
this.targetWindow = targetWindow;
|
|
94
|
+
this.allowedOrigin = options.allowedOrigin;
|
|
95
|
+
window.addEventListener('message', this.messageHandler);
|
|
96
|
+
}
|
|
97
|
+
disconnect() {
|
|
98
|
+
window.removeEventListener('message', this.messageHandler);
|
|
99
|
+
this.targetWindow = null;
|
|
100
|
+
}
|
|
101
|
+
send(message) {
|
|
102
|
+
if (!this.targetWindow) {
|
|
103
|
+
console.warn('Bridge not connected, cannot send message');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.targetWindow.postMessage(message, this.allowedOrigin);
|
|
107
|
+
}
|
|
108
|
+
handleMessage(event) {
|
|
109
|
+
if (event.source !== this.targetWindow) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const message = event.data;
|
|
113
|
+
if (!message || !message.type) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.onEvent(message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const VITE_PRIVY_APP_ID = 'cmhx0hub300mgjl0dfm9pg1e6';
|
|
121
|
+
const VITE_CHECKOUT_URL = 'https://yumi-sdk-checkout.vercel.app/';
|
|
122
|
+
|
|
123
|
+
class YumiClient {
|
|
124
|
+
constructor(config) {
|
|
125
|
+
Object.defineProperty(this, "config", {
|
|
126
|
+
enumerable: true,
|
|
127
|
+
configurable: true,
|
|
128
|
+
writable: true,
|
|
129
|
+
value: void 0
|
|
130
|
+
});
|
|
131
|
+
Object.defineProperty(this, "iframeManager", {
|
|
132
|
+
enumerable: true,
|
|
133
|
+
configurable: true,
|
|
134
|
+
writable: true,
|
|
135
|
+
value: void 0
|
|
136
|
+
});
|
|
137
|
+
Object.defineProperty(this, "bridge", {
|
|
138
|
+
enumerable: true,
|
|
139
|
+
configurable: true,
|
|
140
|
+
writable: true,
|
|
141
|
+
value: void 0
|
|
142
|
+
});
|
|
143
|
+
Object.defineProperty(this, "currentSessionId", {
|
|
144
|
+
enumerable: true,
|
|
145
|
+
configurable: true,
|
|
146
|
+
writable: true,
|
|
147
|
+
value: null
|
|
148
|
+
});
|
|
149
|
+
Object.defineProperty(this, "currentOpenOptions", {
|
|
150
|
+
enumerable: true,
|
|
151
|
+
configurable: true,
|
|
152
|
+
writable: true,
|
|
153
|
+
value: null
|
|
154
|
+
});
|
|
155
|
+
this.config = config;
|
|
156
|
+
this.iframeManager = new IframeManager();
|
|
157
|
+
this.bridge = new MessageBridge({
|
|
158
|
+
onEvent: this.handleIframeEvent.bind(this),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async open(options) {
|
|
162
|
+
if (!options?.orderId) {
|
|
163
|
+
throw new Error('Yumi.open: orderId is required');
|
|
164
|
+
}
|
|
165
|
+
if (options.amount == null || !Number.isFinite(Number(options.amount))) {
|
|
166
|
+
throw new Error('Yumi.open: amount is required and must be a finite number');
|
|
167
|
+
}
|
|
168
|
+
this.currentSessionId = this.generateSessionId();
|
|
169
|
+
this.currentOpenOptions = options;
|
|
170
|
+
const iframe = this.iframeManager.create({
|
|
171
|
+
url: this.getCheckoutUrl(options),
|
|
172
|
+
sessionId: this.currentSessionId,
|
|
173
|
+
});
|
|
174
|
+
this.bridge.connect(iframe.contentWindow, {
|
|
175
|
+
allowedOrigin: this.getCheckoutOrigin(),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
close() {
|
|
179
|
+
this.iframeManager.destroy();
|
|
180
|
+
this.bridge.disconnect();
|
|
181
|
+
this.currentSessionId = null;
|
|
182
|
+
this.currentOpenOptions = null;
|
|
183
|
+
}
|
|
184
|
+
destroy() {
|
|
185
|
+
this.close();
|
|
186
|
+
}
|
|
187
|
+
handleIframeEvent(event) {
|
|
188
|
+
switch (event.type) {
|
|
189
|
+
case 'LOADED':
|
|
190
|
+
if (this.currentSessionId && this.currentOpenOptions) {
|
|
191
|
+
const payload = {
|
|
192
|
+
sessionId: this.currentSessionId,
|
|
193
|
+
publicKey: this.config.publicKey,
|
|
194
|
+
privyAppId: VITE_PRIVY_APP_ID,
|
|
195
|
+
orderId: this.currentOpenOptions.orderId,
|
|
196
|
+
amount: this.currentOpenOptions.amount,
|
|
197
|
+
apiKey: this.config.apiKey,
|
|
198
|
+
};
|
|
199
|
+
this.bridge.send({ type: 'INIT', payload });
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
case 'READY':
|
|
203
|
+
break;
|
|
204
|
+
case 'RESIZE':
|
|
205
|
+
this.iframeManager.resize(event.payload.height);
|
|
206
|
+
break;
|
|
207
|
+
case 'CLOSE':
|
|
208
|
+
this.close();
|
|
209
|
+
this.config.onClose?.();
|
|
210
|
+
break;
|
|
211
|
+
case 'SUCCESS':
|
|
212
|
+
this.config.onEvent?.(event);
|
|
213
|
+
break;
|
|
214
|
+
case 'ERROR':
|
|
215
|
+
this.config.onError?.(event.payload);
|
|
216
|
+
break;
|
|
217
|
+
case 'EVENT':
|
|
218
|
+
this.config.onEvent?.(event);
|
|
219
|
+
break;
|
|
220
|
+
case 'GET_APP_SIGNATURE': {
|
|
221
|
+
const { payload, requestId } = event;
|
|
222
|
+
const getAppSignature = this.config.getAppSignature;
|
|
223
|
+
if (!getAppSignature) {
|
|
224
|
+
this.bridge.send({
|
|
225
|
+
type: 'APP_SIGNATURE_RESPONSE',
|
|
226
|
+
requestId,
|
|
227
|
+
error: 'getAppSignature is not configured',
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
getAppSignature(payload)
|
|
232
|
+
.then(appSignature => {
|
|
233
|
+
this.bridge.send({ type: 'APP_SIGNATURE_RESPONSE', requestId, appSignature });
|
|
234
|
+
})
|
|
235
|
+
.catch(err => {
|
|
236
|
+
this.bridge.send({
|
|
237
|
+
type: 'APP_SIGNATURE_RESPONSE',
|
|
238
|
+
requestId,
|
|
239
|
+
error: err instanceof Error ? err.message : String(err),
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
getCheckoutUrl(options) {
|
|
247
|
+
const base = VITE_CHECKOUT_URL;
|
|
248
|
+
const params = new URLSearchParams({
|
|
249
|
+
orderId: options.orderId,
|
|
250
|
+
session: this.currentSessionId,
|
|
251
|
+
});
|
|
252
|
+
if (options.amount)
|
|
253
|
+
params.set('amount', options.amount.toString());
|
|
254
|
+
return `${base}?${params.toString()}`;
|
|
255
|
+
}
|
|
256
|
+
getCheckoutOrigin() {
|
|
257
|
+
const url = VITE_CHECKOUT_URL;
|
|
258
|
+
try {
|
|
259
|
+
return new URL(url).origin;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return '*';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
generateSessionId() {
|
|
266
|
+
return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
(function () {
|
|
271
|
+
if (window.Yumi) {
|
|
272
|
+
console.warn('Yumi widget already loaded');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
function init(config) {
|
|
276
|
+
if (!config?.publicKey) {
|
|
277
|
+
throw new Error('Yumi.init: publicKey is required');
|
|
278
|
+
}
|
|
279
|
+
if (typeof config.getAppSignature !== 'function') {
|
|
280
|
+
throw new Error('Yumi.init: getAppSignature is required');
|
|
281
|
+
}
|
|
282
|
+
return new YumiClient(config);
|
|
283
|
+
}
|
|
284
|
+
window.Yumi = {
|
|
285
|
+
init,
|
|
286
|
+
version: "1.0.0" ,
|
|
287
|
+
};
|
|
288
|
+
})();
|
|
289
|
+
|
|
290
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(){"use strict";class e{constructor(){Object.defineProperty(this,"iframe",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"overlay",{enumerable:!0,configurable:!0,writable:!0,value:null})}create(e){return this.overlay=document.createElement("div"),this.overlay.id="yumi-checkout-overlay",this.overlay.style.cssText="\n\t\t\tposition: fixed;\n\t\t\tinset: 0;\n\t\t\tz-index: 999999;\n\t\t\tbackground: rgba(0, 0, 0, 0.5);\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t",this.iframe=document.createElement("iframe"),this.iframe.id="yumi-checkout-iframe",this.iframe.src=e.url,this.iframe.style.cssText="\n\t\t\twidth: 100%;\n\t\t\tmax-width: 500px;\n\t\t\theight: 90vh;\n\t\t\tmax-height: 800px;\n\t\t\tborder: none;\n\t\t\tborder-radius: 12px;\n\t\t\tbackground: white;\n\t\t",this.iframe.setAttribute("sandbox","allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"),this.iframe.setAttribute("allow","payment; clipboard-read; clipboard-write"),this.overlay.appendChild(this.iframe),document.body.appendChild(this.overlay),this.iframe}resize(e){this.iframe&&(this.iframe.style.height=`${Math.min(e,.9*window.innerHeight)}px`)}destroy(){this.overlay&&(this.overlay.remove(),this.overlay=null),this.iframe=null}}class t{constructor(e){Object.defineProperty(this,"targetWindow",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"allowedOrigin",{enumerable:!0,configurable:!0,writable:!0,value:"*"}),Object.defineProperty(this,"onEvent",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"messageHandler",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.onEvent=e.onEvent,this.messageHandler=this.handleMessage.bind(this)}connect(e,t){this.targetWindow=e,this.allowedOrigin=t.allowedOrigin,window.addEventListener("message",this.messageHandler)}disconnect(){window.removeEventListener("message",this.messageHandler),this.targetWindow=null}send(e){this.targetWindow?this.targetWindow.postMessage(e,this.allowedOrigin):console.warn("Bridge not connected, cannot send message")}handleMessage(e){if(e.source!==this.targetWindow)return;const t=e.data;t&&t.type&&this.onEvent(t)}}const i="https://yumi-sdk-checkout.vercel.app/";class r{constructor(i){Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"iframeManager",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"bridge",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"currentSessionId",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"currentOpenOptions",{enumerable:!0,configurable:!0,writable:!0,value:null}),this.config=i,this.iframeManager=new e,this.bridge=new t({onEvent:this.handleIframeEvent.bind(this)})}async open(e){if(!e?.orderId)throw new Error("Yumi.open: orderId is required");if(null==e.amount||!Number.isFinite(Number(e.amount)))throw new Error("Yumi.open: amount is required and must be a finite number");this.currentSessionId=this.generateSessionId(),this.currentOpenOptions=e;const t=this.iframeManager.create({url:this.getCheckoutUrl(e),sessionId:this.currentSessionId});this.bridge.connect(t.contentWindow,{allowedOrigin:this.getCheckoutOrigin()})}close(){this.iframeManager.destroy(),this.bridge.disconnect(),this.currentSessionId=null,this.currentOpenOptions=null}destroy(){this.close()}handleIframeEvent(e){switch(e.type){case"LOADED":if(this.currentSessionId&&this.currentOpenOptions){const e={sessionId:this.currentSessionId,publicKey:this.config.publicKey,privyAppId:"cmhx0hub300mgjl0dfm9pg1e6",orderId:this.currentOpenOptions.orderId,amount:this.currentOpenOptions.amount,apiKey:this.config.apiKey};this.bridge.send({type:"INIT",payload:e})}break;case"READY":break;case"RESIZE":this.iframeManager.resize(e.payload.height);break;case"CLOSE":this.close(),this.config.onClose?.();break;case"SUCCESS":case"EVENT":this.config.onEvent?.(e);break;case"ERROR":this.config.onError?.(e.payload);break;case"GET_APP_SIGNATURE":{const{payload:t,requestId:i}=e,r=this.config.getAppSignature;if(!r)return void this.bridge.send({type:"APP_SIGNATURE_RESPONSE",requestId:i,error:"getAppSignature is not configured"});r(t).then(e=>{this.bridge.send({type:"APP_SIGNATURE_RESPONSE",requestId:i,appSignature:e})}).catch(e=>{this.bridge.send({type:"APP_SIGNATURE_RESPONSE",requestId:i,error:e instanceof Error?e.message:String(e)})});break}}}getCheckoutUrl(e){const t=i,r=new URLSearchParams({orderId:e.orderId,session:this.currentSessionId});return e.amount&&r.set("amount",e.amount.toString()),`${t}?${r.toString()}`}getCheckoutOrigin(){const e=i;try{return new URL(e).origin}catch{return"*"}}generateSessionId(){return`sess_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}}window.Yumi?console.warn("Yumi widget already loaded"):window.Yumi={init:function(e){if(!e?.publicKey)throw new Error("Yumi.init: publicKey is required");if("function"!=typeof e.getAppSignature)throw new Error("Yumi.init: getAppSignature is required");return new r(e)},version:"1.0.0"}}();
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yumi-finance/widget",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Yumi BNPL checkout widget",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/yumi-widget.js",
|
|
7
|
+
"types": "dist/loader/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "npm run build:loader && npm run build:checkout && npm run build:react",
|
|
10
|
+
"prepublishOnly": "npm run build:loader",
|
|
11
|
+
"build:loader": "rollup -c && tsc -p tsconfig.declarations.json",
|
|
12
|
+
"build:checkout": "vite build",
|
|
13
|
+
"build:react": "vite build --config vite.react.config.ts",
|
|
14
|
+
"dev:loader": "rollup -c -w",
|
|
15
|
+
"dev:checkout": "vite",
|
|
16
|
+
"dev": "npm run build:loader && concurrently -n loader,checkout,demo \"npm run dev:loader\" \"npm run dev:checkout\" \"npx http-server . -p 8080 -c-1\"",
|
|
17
|
+
"type-check": "tsc --noEmit",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
|
|
20
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
|
|
21
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
22
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"yumi",
|
|
26
|
+
"bnpl",
|
|
27
|
+
"payment",
|
|
28
|
+
"checkout",
|
|
29
|
+
"widget"
|
|
30
|
+
],
|
|
31
|
+
"author": "Yumi Finance",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist/yumi-widget.js",
|
|
35
|
+
"dist/yumi-widget.min.js",
|
|
36
|
+
"dist/react.js",
|
|
37
|
+
"dist/loader",
|
|
38
|
+
"dist/shared"
|
|
39
|
+
],
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"import": "./dist/yumi-widget.min.js",
|
|
43
|
+
"require": "./dist/yumi-widget.js"
|
|
44
|
+
},
|
|
45
|
+
"./react": {
|
|
46
|
+
"import": "./dist/react.js",
|
|
47
|
+
"types": "./src/react/index.ts"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"unpkg": "dist/yumi-widget.min.js",
|
|
51
|
+
"jsdelivr": "dist/yumi-widget.min.js",
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": ">=17.0.0",
|
|
54
|
+
"react-dom": ">=17.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@rollup/plugin-commonjs": "^25.0.7",
|
|
58
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
59
|
+
"@rollup/plugin-replace": "^5.0.5",
|
|
60
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
61
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
62
|
+
"@types/react": "^18.2.55",
|
|
63
|
+
"@types/react-dom": "^18.2.19",
|
|
64
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
65
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
66
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
67
|
+
"autoprefixer": "^10.4.20",
|
|
68
|
+
"concurrently": "^9.1.0",
|
|
69
|
+
"dotenv": "^17.3.1",
|
|
70
|
+
"eslint": "^8.57.1",
|
|
71
|
+
"eslint-plugin-react": "^7.37.5",
|
|
72
|
+
"eslint-plugin-react-hooks": "^4.6.2",
|
|
73
|
+
"http-server": "^14.1.1",
|
|
74
|
+
"postcss": "^8.4.49",
|
|
75
|
+
"prettier": "^3.8.1",
|
|
76
|
+
"rollup": "^4.9.6",
|
|
77
|
+
"tailwindcss": "^3.4.15",
|
|
78
|
+
"tslib": "^2.8.1",
|
|
79
|
+
"typescript": "^5.3.3",
|
|
80
|
+
"vite": "^5.0.12"
|
|
81
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"@meshconnect/web-link-sdk": "^3.4.1",
|
|
84
|
+
"@privy-io/react-auth": "^3.4.0",
|
|
85
|
+
"@privy-io/wagmi": "^4.0.0",
|
|
86
|
+
"buffer": "^6.0.3",
|
|
87
|
+
"json-canonicalize": "^1.2.0",
|
|
88
|
+
"lucide-react": "^0.462.0",
|
|
89
|
+
"react": "^18.2.0",
|
|
90
|
+
"react-dom": "^18.2.0",
|
|
91
|
+
"tweetnacl": "^1.0.3",
|
|
92
|
+
"tweetnacl-util": "^0.15.1",
|
|
93
|
+
"viem": "^2.24.0",
|
|
94
|
+
"wagmi": "^3.5.0",
|
|
95
|
+
"@tanstack/react-query": "^5.59.0"
|
|
96
|
+
}
|
|
97
|
+
}
|