@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 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,10 @@
1
+ export declare class IframeManager {
2
+ private iframe;
3
+ private overlay;
4
+ create(options: {
5
+ url: string;
6
+ sessionId: string;
7
+ }): HTMLIFrameElement;
8
+ resize(height: number): void;
9
+ destroy(): void;
10
+ }
@@ -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
+ }