forgelayer-react 1.0.0 → 1.1.1

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/package.json CHANGED
@@ -1,16 +1,25 @@
1
1
  {
2
2
  "name": "forgelayer-react",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "React components for crypto checkout powered by ForgeLayer",
5
- "main": "src/index.js",
6
- "module": "src/index.js",
5
+ "main": "dist/index.cjs.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
7
8
  "exports": {
8
- ".": "./src/index.js"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.esm.js",
12
+ "require": "./dist/index.cjs.js"
13
+ }
9
14
  },
10
15
  "files": [
11
- "src/",
16
+ "dist/",
12
17
  "README.md"
13
18
  ],
19
+ "scripts": {
20
+ "build": "node build.js",
21
+ "prepublishOnly": "node build.js"
22
+ },
14
23
  "keywords": [
15
24
  "forgelayer", "crypto", "payments", "bitcoin", "ethereum",
16
25
  "usdt", "react", "checkout", "modal", "components"
@@ -28,5 +37,8 @@
28
37
  "peerDependencies": {
29
38
  "react": ">=17.0.0",
30
39
  "react-dom": ">=17.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "esbuild": "^0.28.1"
31
43
  }
32
44
  }
@@ -1,94 +0,0 @@
1
- import React from 'react';
2
- import { useForgeLayerCheckout } from './useForgeLayerCheckout.js';
3
- import { ForgeLayerModal } from './ForgeLayerModal.jsx';
4
-
5
- /**
6
- * Drop-in React checkout button.
7
- *
8
- * Usage:
9
- * <ForgeLayerButton
10
- * amount={49.99}
11
- * currency="USD"
12
- * chain="ethereum"
13
- * token="USDT"
14
- * orderId="ORDER-123"
15
- * onSuccess={(order) => router.push('/thank-you')}
16
- * >
17
- * Pay $49.99
18
- * </ForgeLayerButton>
19
- */
20
- export function ForgeLayerButton({
21
- // Payment params
22
- amount,
23
- currency = 'USD',
24
- chain = 'ethereum',
25
- token = 'USDT',
26
- orderId,
27
- paymentWindow,
28
- reuseAddress,
29
- // Backend base path (proxied or absolute)
30
- baseUrl = '/fl',
31
- // Button UI
32
- label,
33
- children,
34
- className,
35
- style,
36
- disabled,
37
- // Callbacks
38
- onSuccess,
39
- onExpired,
40
- onError,
41
- }) {
42
- const { modalState, order, timeLeft, error, open, close } = useForgeLayerCheckout({
43
- baseUrl,
44
- onSuccess,
45
- onExpired,
46
- onError,
47
- });
48
-
49
- const isOpen = modalState !== 'closed';
50
-
51
- const handleClick = () => {
52
- open({ amount, currency, chain, token, orderId, paymentWindow, reuseAddress });
53
- };
54
-
55
- return (
56
- <>
57
- <button
58
- onClick={handleClick}
59
- disabled={disabled || isOpen}
60
- className={className}
61
- style={{
62
- display: 'inline-flex',
63
- alignItems: 'center',
64
- gap: 8,
65
- padding: '12px 24px',
66
- background: '#f7931a',
67
- color: '#fff',
68
- border: 'none',
69
- borderRadius: 8,
70
- fontSize: 15,
71
- fontWeight: 600,
72
- cursor: (disabled || isOpen) ? 'not-allowed' : 'pointer',
73
- opacity: (disabled || isOpen) ? 0.65 : 1,
74
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
75
- lineHeight: 1.2,
76
- transition: 'opacity .15s',
77
- ...style,
78
- }}
79
- >
80
- {label ?? children ?? 'Pay with Crypto'}
81
- </button>
82
-
83
- {isOpen && (
84
- <ForgeLayerModal
85
- modalState={modalState}
86
- order={order}
87
- timeLeft={timeLeft}
88
- error={error}
89
- onClose={close}
90
- />
91
- )}
92
- </>
93
- );
94
- }
@@ -1,267 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
-
3
- // Injected once — handles animations, hover, and pseudo-elements
4
- // that can't be done with inline styles.
5
- const MODAL_CSS = `
6
- @keyframes fl-r-fade{from{opacity:0}to{opacity:1}}
7
- @keyframes fl-r-up{from{transform:translateY(16px);opacity:0}to{transform:translateY(0);opacity:1}}
8
- @keyframes fl-r-spin{to{transform:rotate(360deg)}}
9
- @keyframes fl-r-pulse{0%,100%{opacity:1}50%{opacity:.35}}
10
- .fl-r-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:99999;
11
- display:flex;align-items:center;justify-content:center;padding:16px;
12
- animation:fl-r-fade .18s ease}
13
- .fl-r-modal{background:#fff;border-radius:16px;width:100%;max-width:520px;
14
- box-shadow:0 24px 64px rgba(0,0,0,.28);overflow:hidden;
15
- animation:fl-r-up .22s ease;
16
- font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
17
- font-size:14px;color:#111}
18
- .fl-r-spinner{width:38px;height:38px;border:3px solid #e5e7eb;
19
- border-top-color:#f7931a;border-radius:50%;
20
- animation:fl-r-spin .7s linear infinite;margin:0 auto 14px}
21
- .fl-r-dot-pending{background:#f59e0b;animation:fl-r-pulse 1.6s ease-in-out infinite}
22
- .fl-r-dot-confirmed{background:#10b981}
23
- .fl-r-dot-expired{background:#ef4444}
24
- .fl-r-xbtn:hover{background:#f3f4f6 !important;color:#111 !important}
25
- .fl-r-cpbtn:hover{background:#f3f4f6 !important}
26
- .fl-r-cpbtn-copied{border-color:#10b981 !important;color:#059669 !important}
27
- @media(max-width:460px){.fl-r-grid{flex-direction:column !important;align-items:center !important}}
28
- `;
29
-
30
- let _cssInjected = false;
31
- function ensureCSS() {
32
- if (_cssInjected || typeof document === 'undefined') return;
33
- _cssInjected = true;
34
- const el = document.createElement('style');
35
- el.textContent = MODAL_CSS;
36
- document.head.appendChild(el);
37
- }
38
-
39
- function fmtTime(secs) {
40
- const m = Math.floor(secs / 60);
41
- const s = secs % 60;
42
- return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
43
- }
44
-
45
- export function ForgeLayerModal({ modalState, order, timeLeft, error, onClose }) {
46
- const [copied, setCopied] = useState(false);
47
-
48
- useEffect(() => { ensureCSS(); }, []);
49
-
50
- // Escape key
51
- useEffect(() => {
52
- const handler = (e) => { if (e.key === 'Escape') onClose(); };
53
- document.addEventListener('keydown', handler);
54
- return () => document.removeEventListener('keydown', handler);
55
- }, [onClose]);
56
-
57
- // Lock body scroll
58
- useEffect(() => {
59
- const prev = document.body.style.overflow;
60
- document.body.style.overflow = 'hidden';
61
- return () => { document.body.style.overflow = prev; };
62
- }, []);
63
-
64
- const copyAddress = () => {
65
- if (!order?.address) return;
66
- navigator.clipboard?.writeText(order.address).catch(() => {});
67
- setCopied(true);
68
- setTimeout(() => setCopied(false), 2000);
69
- };
70
-
71
- const urgent = timeLeft !== null && timeLeft <= 120;
72
-
73
- return (
74
- <div
75
- className="fl-r-backdrop"
76
- role="dialog"
77
- aria-modal="true"
78
- aria-label="Crypto payment"
79
- onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
80
- >
81
- <div className="fl-r-modal">
82
-
83
- {/* ── Header ── */}
84
- <div style={{
85
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
86
- padding: '16px 18px', borderBottom: '1px solid #e5e7eb', background: '#fafafa',
87
- }}>
88
- <div style={{ display: 'flex', alignItems: 'center', gap: 9, fontSize: 15, fontWeight: 700 }}>
89
- <div style={{
90
- width: 26, height: 26, background: '#f7931a', borderRadius: 6,
91
- display: 'flex', alignItems: 'center', justifyContent: 'center',
92
- color: '#fff', fontSize: 10, fontWeight: 800, flexShrink: 0,
93
- }}>FL</div>
94
- Pay with Crypto
95
- </div>
96
- <button
97
- className="fl-r-xbtn"
98
- onClick={onClose}
99
- aria-label="Close"
100
- style={{
101
- background: 'none', border: 'none', fontSize: 22, lineHeight: 1,
102
- cursor: 'pointer', color: '#6b7280', padding: '4px 8px',
103
- borderRadius: 5, transition: 'background .12s',
104
- }}
105
- >×</button>
106
- </div>
107
-
108
- {/* ── Body ── */}
109
- <div style={{ padding: '20px 18px' }}>
110
-
111
- {/* Loading */}
112
- {modalState === 'loading' && (
113
- <div style={{ textAlign: 'center', padding: '28px 0' }}>
114
- <div className="fl-r-spinner" />
115
- <p style={{ color: '#6b7280', fontSize: 13, margin: 0 }}>
116
- Generating payment address…
117
- </p>
118
- </div>
119
- )}
120
-
121
- {/* Error */}
122
- {modalState === 'error' && (
123
- <div style={{ textAlign: 'center', padding: '28px 0' }}>
124
- <div style={{ fontSize: 48, marginBottom: 12 }}>⚠️</div>
125
- <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 8 }}>Something went wrong</div>
126
- <div style={{ fontSize: 13, color: '#6b7280' }}>{error}</div>
127
- </div>
128
- )}
129
-
130
- {/* Payment */}
131
- {modalState === 'payment' && order && (
132
- <>
133
- {/* Status bar */}
134
- <div style={{
135
- display: 'flex', alignItems: 'center', gap: 9,
136
- padding: '9px 13px', borderRadius: 8,
137
- background: '#f9fafb', border: '1px solid #e5e7eb',
138
- marginBottom: 14, fontSize: 13,
139
- }}>
140
- <span
141
- className="fl-r-dot-pending"
142
- style={{ width: 8, height: 8, borderRadius: '50%', flexShrink: 0 }}
143
- />
144
- <span>Awaiting payment…</span>
145
- <span style={{
146
- marginLeft: 'auto', fontWeight: 600, fontSize: 13,
147
- color: urgent ? '#ef4444' : '#374151',
148
- fontVariantNumeric: 'tabular-nums',
149
- }}>
150
- {timeLeft !== null ? fmtTime(timeLeft) : '--:--'}
151
- </span>
152
- </div>
153
-
154
- {/* Network warning */}
155
- <div style={{
156
- background: '#fffbeb', border: '1px solid #fde68a', borderRadius: 8,
157
- padding: '9px 13px', fontSize: 12, color: '#92400e',
158
- marginBottom: 14, lineHeight: 1.5,
159
- }}>
160
- <strong>⚠ Important:</strong> Send only <strong>{order.token}</strong> on the{' '}
161
- <strong>{order.chainName}</strong> network only. Wrong network = permanent loss.
162
- </div>
163
-
164
- {/* QR + info grid */}
165
- <div className="fl-r-grid" style={{ display: 'flex', gap: 18, marginBottom: 14 }}>
166
- {/* QR side */}
167
- <div style={{ flexShrink: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
168
- <img
169
- src={order.qrUrl}
170
- alt={`Send to ${order.address}`}
171
- width={148}
172
- height={148}
173
- style={{ border: '1px solid #e5e7eb', borderRadius: 10, display: 'block', background: '#f9fafb' }}
174
- />
175
- <p style={{ fontSize: 11, color: '#9ca3af', margin: 0 }}>Scan with wallet</p>
176
- </div>
177
-
178
- {/* Info side */}
179
- <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 13 }}>
180
- {/* Amount */}
181
- <div>
182
- <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#6b7280', textTransform: 'uppercase', letterSpacing: '.06em', marginBottom: 4 }}>
183
- Amount to Send
184
- </label>
185
- <div style={{ fontSize: 22, fontWeight: 700, lineHeight: 1.2 }}>
186
- {order.cryptoAmount
187
- ? `${parseFloat(order.cryptoAmount).toFixed(8).replace(/\.?0+$/, '')} ${order.token}`
188
- : `${order.currency} ${parseFloat(order.amount).toFixed(2)}`}
189
- </div>
190
- {order.cryptoAmount && (
191
- <div style={{ fontSize: 12, color: '#6b7280', marginTop: 2 }}>
192
- ≈ {order.currency} {parseFloat(order.amount).toFixed(2)}
193
- </div>
194
- )}
195
- </div>
196
-
197
- {/* Address */}
198
- <div>
199
- <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#6b7280', textTransform: 'uppercase', letterSpacing: '.06em', marginBottom: 4 }}>
200
- Deposit Address
201
- </label>
202
- <div style={{ display: 'flex', alignItems: 'flex-start', gap: 7 }}>
203
- <span style={{ fontFamily: 'monospace', fontSize: 12, color: '#374151', wordBreak: 'break-all', flex: 1, lineHeight: 1.5 }}>
204
- {order.address}
205
- </span>
206
- <button
207
- className={`fl-r-cpbtn${copied ? ' fl-r-cpbtn-copied' : ''}`}
208
- onClick={copyAddress}
209
- style={{
210
- flexShrink: 0, background: '#fff', border: '1px solid #d1d5db',
211
- borderRadius: 6, padding: '5px 11px', fontSize: 12,
212
- cursor: 'pointer', color: '#374151',
213
- transition: 'background .12s', whiteSpace: 'nowrap',
214
- }}
215
- >
216
- {copied ? 'Copied!' : 'Copy'}
217
- </button>
218
- </div>
219
- </div>
220
-
221
- {/* Network badge */}
222
- <div>
223
- <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#6b7280', textTransform: 'uppercase', letterSpacing: '.06em', marginBottom: 4 }}>
224
- Network
225
- </label>
226
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '3px 10px', background: '#f3f4f6', borderRadius: 100, fontSize: 12, fontWeight: 500, color: '#374151' }}>
227
- <span style={{ width: 7, height: 7, borderRadius: '50%', background: '#10b981', flexShrink: 0 }} />
228
- {order.chainName} · {order.token}
229
- </span>
230
- </div>
231
- </div>
232
- </div>
233
- </>
234
- )}
235
-
236
- {/* Success */}
237
- {modalState === 'success' && (
238
- <div style={{ textAlign: 'center', padding: '30px 16px' }}>
239
- <div style={{ fontSize: 52, marginBottom: 14 }}>✅</div>
240
- <div style={{ fontSize: 19, fontWeight: 700, color: '#111', marginBottom: 7 }}>Payment Confirmed!</div>
241
- <div style={{ fontSize: 13, color: '#6b7280' }}>Your payment has been received.</div>
242
- </div>
243
- )}
244
-
245
- {/* Expired */}
246
- {modalState === 'expired' && (
247
- <div style={{ textAlign: 'center', padding: '30px 16px' }}>
248
- <div style={{ fontSize: 52, marginBottom: 14 }}>⏳</div>
249
- <div style={{ fontSize: 19, fontWeight: 700, color: '#111', marginBottom: 7 }}>Payment Expired</div>
250
- <div style={{ fontSize: 13, color: '#6b7280' }}>
251
- The payment window has closed. Please start a new payment.
252
- </div>
253
- </div>
254
- )}
255
- </div>
256
-
257
- {/* ── Footer ── */}
258
- <div style={{ padding: '10px 18px 14px', textAlign: 'center', fontSize: 11, color: '#9ca3af', borderTop: '1px solid #f3f4f6' }}>
259
- Secured by{' '}
260
- <a href="https://forgelayer.io" target="_blank" rel="noopener noreferrer" style={{ color: '#f7931a', textDecoration: 'none' }}>
261
- ForgeLayer
262
- </a>
263
- </div>
264
- </div>
265
- </div>
266
- );
267
- }
package/src/index.js DELETED
@@ -1,3 +0,0 @@
1
- export { ForgeLayerButton } from './ForgeLayerButton.jsx';
2
- export { ForgeLayerModal } from './ForgeLayerModal.jsx';
3
- export { useForgeLayerCheckout } from './useForgeLayerCheckout.js';
@@ -1,93 +0,0 @@
1
- 'use strict';
2
- import { useState, useRef, useCallback, useEffect } from 'react';
3
-
4
- /**
5
- * Core hook — handles all state and API calls for the checkout flow.
6
- *
7
- * modalState values:
8
- * 'closed' | 'loading' | 'payment' | 'success' | 'expired' | 'error'
9
- */
10
- export function useForgeLayerCheckout({ baseUrl = '/fl', onSuccess, onExpired, onError } = {}) {
11
- const [modalState, setModalState] = useState('closed');
12
- const [order, setOrder] = useState(null);
13
- const [timeLeft, setTimeLeft] = useState(null);
14
- const [error, setError] = useState(null);
15
-
16
- const pollRef = useRef(null);
17
- const cdRef = useRef(null);
18
-
19
- const stopTimers = useCallback(() => {
20
- if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
21
- if (cdRef.current) { clearInterval(cdRef.current); cdRef.current = null; }
22
- }, []);
23
-
24
- // Cleanup on unmount
25
- useEffect(() => stopTimers, [stopTimers]);
26
-
27
- const close = useCallback(() => {
28
- stopTimers();
29
- setModalState('closed');
30
- setOrder(null);
31
- setTimeLeft(null);
32
- setError(null);
33
- }, [stopTimers]);
34
-
35
- const startCountdown = useCallback((expiresAt) => {
36
- if (cdRef.current) clearInterval(cdRef.current);
37
- const tick = () => {
38
- const rem = expiresAt - Math.floor(Date.now() / 1000);
39
- setTimeLeft(rem <= 0 ? 0 : rem);
40
- if (rem <= 0) clearInterval(cdRef.current);
41
- };
42
- tick();
43
- cdRef.current = setInterval(tick, 1000);
44
- }, []);
45
-
46
- const startPolling = useCallback((orderData) => {
47
- if (pollRef.current) clearInterval(pollRef.current);
48
- pollRef.current = setInterval(async () => {
49
- try {
50
- const res = await fetch(`${baseUrl}/status?session=${encodeURIComponent(orderData.sessionKey)}`);
51
- const data = await res.json();
52
- if (!data.ok) return;
53
- if (data.status === 'confirmed') {
54
- stopTimers();
55
- setModalState('success');
56
- onSuccess?.(orderData);
57
- } else if (data.status === 'expired') {
58
- stopTimers();
59
- setModalState('expired');
60
- onExpired?.();
61
- }
62
- } catch (_) {}
63
- }, 15_000);
64
- }, [baseUrl, stopTimers, onSuccess, onExpired]);
65
-
66
- const open = useCallback(async (params) => {
67
- stopTimers();
68
- setModalState('loading');
69
- setOrder(null);
70
- setTimeLeft(null);
71
- setError(null);
72
-
73
- try {
74
- const res = await fetch(`${baseUrl}/create`, {
75
- method: 'POST',
76
- headers: { 'Content-Type': 'application/json' },
77
- body: JSON.stringify(params),
78
- });
79
- const data = await res.json();
80
- if (!data.ok) throw new Error(data.error || 'Failed to generate payment address.');
81
- setOrder(data);
82
- setModalState('payment');
83
- startCountdown(data.expiresAt);
84
- startPolling(data);
85
- } catch (e) {
86
- setError(e.message);
87
- setModalState('error');
88
- onError?.(e);
89
- }
90
- }, [baseUrl, stopTimers, startCountdown, startPolling, onError]);
91
-
92
- return { modalState, order, timeLeft, error, open, close };
93
- }