@rotateprotocol/sdk 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 +453 -0
- package/dist/catalog.d.ts +112 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +210 -0
- package/dist/catalog.js.map +1 -0
- package/dist/components/CheckoutForm.d.ts +86 -0
- package/dist/components/CheckoutForm.d.ts.map +1 -0
- package/dist/components/CheckoutForm.js +332 -0
- package/dist/components/CheckoutForm.js.map +1 -0
- package/dist/components/HostedCheckout.d.ts +57 -0
- package/dist/components/HostedCheckout.d.ts.map +1 -0
- package/dist/components/HostedCheckout.js +414 -0
- package/dist/components/HostedCheckout.js.map +1 -0
- package/dist/components/PaymentButton.d.ts +80 -0
- package/dist/components/PaymentButton.d.ts.map +1 -0
- package/dist/components/PaymentButton.js +210 -0
- package/dist/components/PaymentButton.js.map +1 -0
- package/dist/components/RotateProvider.d.ts +115 -0
- package/dist/components/RotateProvider.d.ts.map +1 -0
- package/dist/components/RotateProvider.js +264 -0
- package/dist/components/RotateProvider.js.map +1 -0
- package/dist/components/index.d.ts +17 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +27 -0
- package/dist/components/index.js.map +1 -0
- package/dist/embed.d.ts +85 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +313 -0
- package/dist/embed.js.map +1 -0
- package/dist/hooks.d.ts +156 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +280 -0
- package/dist/hooks.js.map +1 -0
- package/dist/idl/rotate_connect.json +2572 -0
- package/dist/index.d.ts +505 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1197 -0
- package/dist/index.js.map +1 -0
- package/dist/marketplace.d.ts +257 -0
- package/dist/marketplace.d.ts.map +1 -0
- package/dist/marketplace.js +433 -0
- package/dist/marketplace.js.map +1 -0
- package/dist/platform.d.ts +234 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +268 -0
- package/dist/platform.js.map +1 -0
- package/dist/react.d.ts +140 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +429 -0
- package/dist/react.js.map +1 -0
- package/dist/store.d.ts +213 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +404 -0
- package/dist/store.js.map +1 -0
- package/dist/webhooks.d.ts +149 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +371 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +114 -0
- package/src/catalog.ts +299 -0
- package/src/components/CheckoutForm.tsx +608 -0
- package/src/components/HostedCheckout.tsx +675 -0
- package/src/components/PaymentButton.tsx +348 -0
- package/src/components/RotateProvider.tsx +370 -0
- package/src/components/index.ts +26 -0
- package/src/embed.ts +408 -0
- package/src/hooks.ts +518 -0
- package/src/idl/rotate_connect.json +2572 -0
- package/src/index.ts +1538 -0
- package/src/marketplace.ts +642 -0
- package/src/platform.ts +403 -0
- package/src/react.ts +459 -0
- package/src/store.ts +577 -0
- package/src/webhooks.ts +506 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotate Checkout Form
|
|
3
|
+
*
|
|
4
|
+
* Embeddable checkout form that can be styled to match your site.
|
|
5
|
+
* Drop-in component for seamless crypto payments.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Basic usage
|
|
10
|
+
* <CheckoutForm
|
|
11
|
+
* amount={99.99}
|
|
12
|
+
* onSuccess={(payment) => {
|
|
13
|
+
* console.log('Paid!', payment);
|
|
14
|
+
* router.push('/success');
|
|
15
|
+
* }}
|
|
16
|
+
* />
|
|
17
|
+
*
|
|
18
|
+
* // With product details
|
|
19
|
+
* <CheckoutForm
|
|
20
|
+
* amount={149.99}
|
|
21
|
+
* productName="Pro Plan"
|
|
22
|
+
* productDescription="Unlimited access for 1 year"
|
|
23
|
+
* productImage="/images/pro-plan.png"
|
|
24
|
+
* onSuccess={handleSuccess}
|
|
25
|
+
* onError={handleError}
|
|
26
|
+
* />
|
|
27
|
+
*
|
|
28
|
+
* // Embedded in your own form
|
|
29
|
+
* <form onSubmit={handleSubmit}>
|
|
30
|
+
* <input name="email" placeholder="Email" />
|
|
31
|
+
* <CheckoutForm
|
|
32
|
+
* amount={total}
|
|
33
|
+
* embedded
|
|
34
|
+
* onSuccess={onPaymentSuccess}
|
|
35
|
+
* />
|
|
36
|
+
* </form>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import React, { useState, useCallback, useEffect, CSSProperties } from 'react';
|
|
41
|
+
import { useRotateContext } from './RotateProvider';
|
|
42
|
+
|
|
43
|
+
// ==================== TYPES ====================
|
|
44
|
+
|
|
45
|
+
export interface CheckoutFormProps {
|
|
46
|
+
/** Amount to charge */
|
|
47
|
+
amount: number;
|
|
48
|
+
/** Currency (default: USD) */
|
|
49
|
+
currency?: 'SOL' | 'USDC' | 'USDT' | 'USD';
|
|
50
|
+
/** Product name to display */
|
|
51
|
+
productName?: string;
|
|
52
|
+
/** Product description */
|
|
53
|
+
productDescription?: string;
|
|
54
|
+
/** Product image URL */
|
|
55
|
+
productImage?: string;
|
|
56
|
+
/** Your order reference ID */
|
|
57
|
+
orderRef?: string;
|
|
58
|
+
/** Allow customer tips */
|
|
59
|
+
allowTips?: boolean;
|
|
60
|
+
/** Show currency selector */
|
|
61
|
+
showCurrencySelector?: boolean;
|
|
62
|
+
/** Default payment currency */
|
|
63
|
+
defaultPaymentCurrency?: 'SOL' | 'USDC' | 'USDT';
|
|
64
|
+
/** Compact mode (no product display) */
|
|
65
|
+
compact?: boolean;
|
|
66
|
+
/** Embedded mode (no container styling) */
|
|
67
|
+
embedded?: boolean;
|
|
68
|
+
/** Success callback */
|
|
69
|
+
onSuccess?: (payment: PaymentResult) => void;
|
|
70
|
+
/** Error callback */
|
|
71
|
+
onError?: (error: Error) => void;
|
|
72
|
+
/** Custom class name */
|
|
73
|
+
className?: string;
|
|
74
|
+
/** Custom styles */
|
|
75
|
+
style?: CSSProperties;
|
|
76
|
+
/** Submit button text */
|
|
77
|
+
submitText?: string;
|
|
78
|
+
/** Show "Powered by Rotate" badge */
|
|
79
|
+
showBranding?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PaymentResult {
|
|
83
|
+
linkId: number;
|
|
84
|
+
amount: number;
|
|
85
|
+
currency: string;
|
|
86
|
+
transactionId: string;
|
|
87
|
+
paidAt: number;
|
|
88
|
+
tip?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ==================== STYLES ====================
|
|
92
|
+
|
|
93
|
+
const containerStyles: CSSProperties = {
|
|
94
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
95
|
+
maxWidth: '400px',
|
|
96
|
+
width: '100%',
|
|
97
|
+
background: '#ffffff',
|
|
98
|
+
borderRadius: '12px',
|
|
99
|
+
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.08)',
|
|
100
|
+
overflow: 'hidden',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const headerStyles: CSSProperties = {
|
|
104
|
+
padding: '24px',
|
|
105
|
+
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
|
|
106
|
+
borderBottom: '1px solid #e2e8f0',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const productStyles: CSSProperties = {
|
|
110
|
+
display: 'flex',
|
|
111
|
+
gap: '16px',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const productImageStyles: CSSProperties = {
|
|
116
|
+
width: '60px',
|
|
117
|
+
height: '60px',
|
|
118
|
+
borderRadius: '8px',
|
|
119
|
+
objectFit: 'cover' as const,
|
|
120
|
+
background: '#e2e8f0',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const bodyStyles: CSSProperties = {
|
|
124
|
+
padding: '24px',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const sectionStyles: CSSProperties = {
|
|
128
|
+
marginBottom: '20px',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const labelStyles: CSSProperties = {
|
|
132
|
+
display: 'block',
|
|
133
|
+
fontSize: '13px',
|
|
134
|
+
fontWeight: 600,
|
|
135
|
+
color: '#64748b',
|
|
136
|
+
marginBottom: '8px',
|
|
137
|
+
textTransform: 'uppercase' as const,
|
|
138
|
+
letterSpacing: '0.5px',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const currencySelectorStyles: CSSProperties = {
|
|
142
|
+
display: 'flex',
|
|
143
|
+
gap: '8px',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const currencyOptionStyles = (selected: boolean, color: string): CSSProperties => ({
|
|
147
|
+
flex: 1,
|
|
148
|
+
padding: '12px 16px',
|
|
149
|
+
border: `2px solid ${selected ? color : '#e2e8f0'}`,
|
|
150
|
+
borderRadius: '8px',
|
|
151
|
+
background: selected ? `${color}10` : '#ffffff',
|
|
152
|
+
cursor: 'pointer',
|
|
153
|
+
transition: 'all 0.2s ease',
|
|
154
|
+
display: 'flex',
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
justifyContent: 'center',
|
|
157
|
+
gap: '8px',
|
|
158
|
+
fontSize: '14px',
|
|
159
|
+
fontWeight: selected ? 600 : 400,
|
|
160
|
+
color: selected ? color : '#64748b',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const tipSelectorStyles: CSSProperties = {
|
|
164
|
+
display: 'flex',
|
|
165
|
+
gap: '8px',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const tipOptionStyles = (selected: boolean, color: string): CSSProperties => ({
|
|
169
|
+
flex: 1,
|
|
170
|
+
padding: '10px',
|
|
171
|
+
border: `2px solid ${selected ? color : '#e2e8f0'}`,
|
|
172
|
+
borderRadius: '8px',
|
|
173
|
+
background: selected ? `${color}10` : '#ffffff',
|
|
174
|
+
cursor: 'pointer',
|
|
175
|
+
transition: 'all 0.2s ease',
|
|
176
|
+
textAlign: 'center' as const,
|
|
177
|
+
fontSize: '14px',
|
|
178
|
+
fontWeight: selected ? 600 : 400,
|
|
179
|
+
color: selected ? color : '#64748b',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const summaryStyles: CSSProperties = {
|
|
183
|
+
background: '#f8fafc',
|
|
184
|
+
borderRadius: '8px',
|
|
185
|
+
padding: '16px',
|
|
186
|
+
marginBottom: '20px',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const summaryRowStyles: CSSProperties = {
|
|
190
|
+
display: 'flex',
|
|
191
|
+
justifyContent: 'space-between',
|
|
192
|
+
alignItems: 'center',
|
|
193
|
+
padding: '8px 0',
|
|
194
|
+
fontSize: '14px',
|
|
195
|
+
color: '#64748b',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const summaryTotalStyles: CSSProperties = {
|
|
199
|
+
...summaryRowStyles,
|
|
200
|
+
borderTop: '1px solid #e2e8f0',
|
|
201
|
+
marginTop: '8px',
|
|
202
|
+
paddingTop: '16px',
|
|
203
|
+
fontSize: '18px',
|
|
204
|
+
fontWeight: 700,
|
|
205
|
+
color: '#1e293b',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const buttonStyles = (color: string, loading: boolean): CSSProperties => ({
|
|
209
|
+
width: '100%',
|
|
210
|
+
padding: '16px 24px',
|
|
211
|
+
background: `linear-gradient(135deg, ${color} 0%, ${adjustColor(color, -20)} 100%)`,
|
|
212
|
+
border: 'none',
|
|
213
|
+
borderRadius: '8px',
|
|
214
|
+
color: '#ffffff',
|
|
215
|
+
fontSize: '16px',
|
|
216
|
+
fontWeight: 600,
|
|
217
|
+
cursor: loading ? 'wait' : 'pointer',
|
|
218
|
+
transition: 'all 0.2s ease',
|
|
219
|
+
display: 'flex',
|
|
220
|
+
alignItems: 'center',
|
|
221
|
+
justifyContent: 'center',
|
|
222
|
+
gap: '8px',
|
|
223
|
+
boxShadow: `0 4px 14px ${color}40`,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const footerStyles: CSSProperties = {
|
|
227
|
+
textAlign: 'center' as const,
|
|
228
|
+
padding: '16px',
|
|
229
|
+
borderTop: '1px solid #e2e8f0',
|
|
230
|
+
fontSize: '12px',
|
|
231
|
+
color: '#94a3b8',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const errorStyles: CSSProperties = {
|
|
235
|
+
padding: '12px 16px',
|
|
236
|
+
background: '#fef2f2',
|
|
237
|
+
border: '1px solid #fecaca',
|
|
238
|
+
borderRadius: '8px',
|
|
239
|
+
color: '#dc2626',
|
|
240
|
+
fontSize: '14px',
|
|
241
|
+
marginBottom: '16px',
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
function adjustColor(hex: string, percent: number): string {
|
|
245
|
+
const num = parseInt(hex.replace('#', ''), 16);
|
|
246
|
+
const amt = Math.round(2.55 * percent);
|
|
247
|
+
const R = (num >> 16) + amt;
|
|
248
|
+
const G = (num >> 8 & 0x00FF) + amt;
|
|
249
|
+
const B = (num & 0x0000FF) + amt;
|
|
250
|
+
return '#' + (
|
|
251
|
+
0x1000000 +
|
|
252
|
+
(R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
|
|
253
|
+
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
|
|
254
|
+
(B < 255 ? B < 1 ? 0 : B : 255)
|
|
255
|
+
).toString(16).slice(1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ==================== ICONS ====================
|
|
259
|
+
|
|
260
|
+
const CurrencyIcon: Record<string, () => React.ReactElement> = {
|
|
261
|
+
SOL: () => (
|
|
262
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
263
|
+
<circle cx="12" cy="12" r="10" fillOpacity="0.2"/>
|
|
264
|
+
<text x="12" y="16" textAnchor="middle" fontSize="10" fontWeight="bold">◎</text>
|
|
265
|
+
</svg>
|
|
266
|
+
),
|
|
267
|
+
USDC: () => (
|
|
268
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
269
|
+
<circle cx="12" cy="12" r="10" fillOpacity="0.2"/>
|
|
270
|
+
<text x="12" y="16" textAnchor="middle" fontSize="10" fontWeight="bold">$</text>
|
|
271
|
+
</svg>
|
|
272
|
+
),
|
|
273
|
+
USDT: () => (
|
|
274
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
275
|
+
<circle cx="12" cy="12" r="10" fillOpacity="0.2"/>
|
|
276
|
+
<text x="12" y="16" textAnchor="middle" fontSize="10" fontWeight="bold">₮</text>
|
|
277
|
+
</svg>
|
|
278
|
+
),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const WalletIcon = () => (
|
|
282
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
283
|
+
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
|
|
284
|
+
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
|
|
285
|
+
<path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/>
|
|
286
|
+
</svg>
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const SpinnerIcon = () => (
|
|
290
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
|
291
|
+
style={{ animation: 'rotate-spin 1s linear infinite' }}>
|
|
292
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25"/>
|
|
293
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round"/>
|
|
294
|
+
</svg>
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const LockIcon = () => (
|
|
298
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
299
|
+
<path d="M19 11H5c-1.1 0-2 .9-2 2v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7c0-1.1-.9-2-2-2zm0 9H5v-7h14v7zm-7-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm3-10V5c0-1.7-1.3-3-3-3S9 3.3 9 5v2H7V5c0-2.8 2.2-5 5-5s5 2.2 5 5v2h-2z"/>
|
|
300
|
+
</svg>
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// ==================== COMPONENT ====================
|
|
304
|
+
|
|
305
|
+
export function CheckoutForm({
|
|
306
|
+
amount,
|
|
307
|
+
currency = 'USD',
|
|
308
|
+
productName,
|
|
309
|
+
productDescription,
|
|
310
|
+
productImage,
|
|
311
|
+
orderRef,
|
|
312
|
+
allowTips = false,
|
|
313
|
+
showCurrencySelector = true,
|
|
314
|
+
defaultPaymentCurrency = 'USDC',
|
|
315
|
+
compact = false,
|
|
316
|
+
embedded = false,
|
|
317
|
+
onSuccess,
|
|
318
|
+
onError,
|
|
319
|
+
className = '',
|
|
320
|
+
style = {},
|
|
321
|
+
submitText,
|
|
322
|
+
showBranding = true,
|
|
323
|
+
}: CheckoutFormProps) {
|
|
324
|
+
const { config, createCheckoutSession, calculateTotal, sdk } = useRotateContext();
|
|
325
|
+
const [selectedCurrency, setSelectedCurrency] = useState<'SOL' | 'USDC' | 'USDT'>(defaultPaymentCurrency);
|
|
326
|
+
const [selectedTip, setSelectedTip] = useState<number>(0);
|
|
327
|
+
const [loading, setLoading] = useState(false);
|
|
328
|
+
const [error, setError] = useState<string | null>(null);
|
|
329
|
+
const [solPrice, setSolPrice] = useState<number | null>(null);
|
|
330
|
+
|
|
331
|
+
const brandColor = config.brandColor || '#8B5CF6';
|
|
332
|
+
const tipOptions = [0, 10, 15, 20]; // Percentage options
|
|
333
|
+
|
|
334
|
+
// Fetch SOL price
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
async function fetchPrice() {
|
|
337
|
+
try {
|
|
338
|
+
const price = await sdk.getSolPrice();
|
|
339
|
+
setSolPrice(price);
|
|
340
|
+
} catch (e) {
|
|
341
|
+
setSolPrice(100); // Fallback
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
fetchPrice();
|
|
345
|
+
}, [sdk]);
|
|
346
|
+
|
|
347
|
+
// Calculate totals
|
|
348
|
+
const tipAmount = (amount * selectedTip) / 100;
|
|
349
|
+
const subtotalWithTip = amount + tipAmount;
|
|
350
|
+
const { total, fees, merchantReceives } = calculateTotal(subtotalWithTip);
|
|
351
|
+
|
|
352
|
+
// Convert to selected currency
|
|
353
|
+
const getAmountInCurrency = (usdAmount: number): number => {
|
|
354
|
+
if (selectedCurrency === 'SOL' && solPrice) {
|
|
355
|
+
return usdAmount / solPrice;
|
|
356
|
+
}
|
|
357
|
+
return usdAmount; // USDC/USDT are 1:1
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const formatAmount = (amt: number, curr: string): string => {
|
|
361
|
+
if (curr === 'SOL') {
|
|
362
|
+
const solAmt = getAmountInCurrency(amt);
|
|
363
|
+
return `${solAmt.toFixed(solAmt < 1 ? 4 : 3)} SOL`;
|
|
364
|
+
}
|
|
365
|
+
return `$${amt.toFixed(2)}`;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Handle payment — creates an on-chain checkout session and opens the
|
|
369
|
+
// hosted checkout page so the buyer can connect their wallet and pay.
|
|
370
|
+
const handleSubmit = useCallback(async () => {
|
|
371
|
+
setLoading(true);
|
|
372
|
+
setError(null);
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const session = await createCheckoutSession({
|
|
376
|
+
amount: subtotalWithTip,
|
|
377
|
+
currency,
|
|
378
|
+
description: productName,
|
|
379
|
+
orderRef,
|
|
380
|
+
allowTips,
|
|
381
|
+
metadata: {
|
|
382
|
+
tip: tipAmount.toString(),
|
|
383
|
+
paymentCurrency: selectedCurrency,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Open hosted checkout in a popup so the buyer can connect a wallet and pay
|
|
388
|
+
const checkoutUrl = sdk.getPaymentUrl(session.linkId);
|
|
389
|
+
const width = 450;
|
|
390
|
+
const height = 700;
|
|
391
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
392
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
393
|
+
|
|
394
|
+
const popup = window.open(
|
|
395
|
+
checkoutUrl,
|
|
396
|
+
'rotate_checkout',
|
|
397
|
+
`width=${width},height=${height},left=${left},top=${top},popup=1`
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (popup) {
|
|
401
|
+
const messageHandler = (event: MessageEvent) => {
|
|
402
|
+
if (event.data?.type === 'rotate_payment_success') {
|
|
403
|
+
setLoading(false);
|
|
404
|
+
window.removeEventListener('message', messageHandler);
|
|
405
|
+
onSuccess?.({
|
|
406
|
+
linkId: session.linkId,
|
|
407
|
+
amount: total,
|
|
408
|
+
currency: selectedCurrency,
|
|
409
|
+
transactionId: event.data.payment?.transactionId || session.id,
|
|
410
|
+
paidAt: Date.now(),
|
|
411
|
+
tip: tipAmount,
|
|
412
|
+
});
|
|
413
|
+
} else if (event.data?.type === 'rotate_payment_cancel') {
|
|
414
|
+
setLoading(false);
|
|
415
|
+
window.removeEventListener('message', messageHandler);
|
|
416
|
+
} else if (event.data?.type === 'rotate_payment_error') {
|
|
417
|
+
setLoading(false);
|
|
418
|
+
setError(event.data.message || 'Payment failed');
|
|
419
|
+
window.removeEventListener('message', messageHandler);
|
|
420
|
+
onError?.(new Error(event.data.message || 'Payment failed'));
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
window.addEventListener('message', messageHandler);
|
|
425
|
+
|
|
426
|
+
// Detect popup closed without completing
|
|
427
|
+
const checkClosed = setInterval(() => {
|
|
428
|
+
if (popup.closed) {
|
|
429
|
+
clearInterval(checkClosed);
|
|
430
|
+
setLoading(false);
|
|
431
|
+
window.removeEventListener('message', messageHandler);
|
|
432
|
+
}
|
|
433
|
+
}, 500);
|
|
434
|
+
} else {
|
|
435
|
+
// Popup blocked — fall back to redirect
|
|
436
|
+
window.location.href = checkoutUrl;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
} catch (err: any) {
|
|
440
|
+
setLoading(false);
|
|
441
|
+
setError(err.message);
|
|
442
|
+
onError?.(err);
|
|
443
|
+
}
|
|
444
|
+
}, [
|
|
445
|
+
subtotalWithTip, currency, productName, orderRef, allowTips,
|
|
446
|
+
tipAmount, selectedCurrency, total, sdk,
|
|
447
|
+
createCheckoutSession, onSuccess, onError,
|
|
448
|
+
]);
|
|
449
|
+
|
|
450
|
+
// Render
|
|
451
|
+
return (
|
|
452
|
+
<>
|
|
453
|
+
<style>{`
|
|
454
|
+
@keyframes rotate-spin {
|
|
455
|
+
from { transform: rotate(0deg); }
|
|
456
|
+
to { transform: rotate(360deg); }
|
|
457
|
+
}
|
|
458
|
+
`}</style>
|
|
459
|
+
|
|
460
|
+
<div
|
|
461
|
+
className={className}
|
|
462
|
+
style={embedded ? style : { ...containerStyles, ...style }}
|
|
463
|
+
>
|
|
464
|
+
{/* Product Header */}
|
|
465
|
+
{!compact && productName && (
|
|
466
|
+
<div style={headerStyles}>
|
|
467
|
+
<div style={productStyles}>
|
|
468
|
+
{productImage && (
|
|
469
|
+
<img
|
|
470
|
+
src={productImage}
|
|
471
|
+
alt={productName}
|
|
472
|
+
style={productImageStyles}
|
|
473
|
+
/>
|
|
474
|
+
)}
|
|
475
|
+
<div>
|
|
476
|
+
<div style={{ fontSize: '16px', fontWeight: 600, color: '#1e293b' }}>
|
|
477
|
+
{productName}
|
|
478
|
+
</div>
|
|
479
|
+
{productDescription && (
|
|
480
|
+
<div style={{ fontSize: '14px', color: '#64748b', marginTop: '4px' }}>
|
|
481
|
+
{productDescription}
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
484
|
+
<div style={{ fontSize: '18px', fontWeight: 700, color: brandColor, marginTop: '8px' }}>
|
|
485
|
+
{formatAmount(amount, currency)}
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
|
|
492
|
+
{/* Form Body */}
|
|
493
|
+
<div style={bodyStyles}>
|
|
494
|
+
{/* Error Display */}
|
|
495
|
+
{error && (
|
|
496
|
+
<div style={errorStyles}>
|
|
497
|
+
{error}
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
|
|
501
|
+
{/* Currency Selector */}
|
|
502
|
+
{showCurrencySelector && (
|
|
503
|
+
<div style={sectionStyles}>
|
|
504
|
+
<label style={labelStyles}>Pay with</label>
|
|
505
|
+
<div style={currencySelectorStyles}>
|
|
506
|
+
{(['SOL', 'USDC', 'USDT'] as const).map((curr) => (
|
|
507
|
+
<button
|
|
508
|
+
key={curr}
|
|
509
|
+
type="button"
|
|
510
|
+
style={currencyOptionStyles(selectedCurrency === curr, brandColor)}
|
|
511
|
+
onClick={() => setSelectedCurrency(curr)}
|
|
512
|
+
>
|
|
513
|
+
{CurrencyIcon[curr]()}
|
|
514
|
+
<span>{curr}</span>
|
|
515
|
+
</button>
|
|
516
|
+
))}
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
)}
|
|
520
|
+
|
|
521
|
+
{/* Tip Selector */}
|
|
522
|
+
{allowTips && (
|
|
523
|
+
<div style={sectionStyles}>
|
|
524
|
+
<label style={labelStyles}>Add a tip</label>
|
|
525
|
+
<div style={tipSelectorStyles}>
|
|
526
|
+
{tipOptions.map((tip) => (
|
|
527
|
+
<button
|
|
528
|
+
key={tip}
|
|
529
|
+
type="button"
|
|
530
|
+
style={tipOptionStyles(selectedTip === tip, brandColor)}
|
|
531
|
+
onClick={() => setSelectedTip(tip)}
|
|
532
|
+
>
|
|
533
|
+
{tip === 0 ? 'None' : `${tip}%`}
|
|
534
|
+
</button>
|
|
535
|
+
))}
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
539
|
+
|
|
540
|
+
{/* Order Summary */}
|
|
541
|
+
<div style={summaryStyles}>
|
|
542
|
+
<div style={summaryRowStyles}>
|
|
543
|
+
<span>Subtotal</span>
|
|
544
|
+
<span>{formatAmount(amount, currency)}</span>
|
|
545
|
+
</div>
|
|
546
|
+
{tipAmount > 0 && (
|
|
547
|
+
<div style={summaryRowStyles}>
|
|
548
|
+
<span>Tip ({selectedTip}%)</span>
|
|
549
|
+
<span>{formatAmount(tipAmount, currency)}</span>
|
|
550
|
+
</div>
|
|
551
|
+
)}
|
|
552
|
+
<div style={summaryRowStyles}>
|
|
553
|
+
<span>Processing fee</span>
|
|
554
|
+
<span>{formatAmount(fees, currency)}</span>
|
|
555
|
+
</div>
|
|
556
|
+
<div style={summaryTotalStyles}>
|
|
557
|
+
<span>Total</span>
|
|
558
|
+
<span>{formatAmount(total, selectedCurrency)}</span>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
{/* Pay Button */}
|
|
563
|
+
<button
|
|
564
|
+
type="button"
|
|
565
|
+
onClick={handleSubmit}
|
|
566
|
+
disabled={loading}
|
|
567
|
+
style={buttonStyles(brandColor, loading)}
|
|
568
|
+
>
|
|
569
|
+
{loading ? (
|
|
570
|
+
<>
|
|
571
|
+
<SpinnerIcon />
|
|
572
|
+
<span>Connecting Wallet...</span>
|
|
573
|
+
</>
|
|
574
|
+
) : (
|
|
575
|
+
<>
|
|
576
|
+
<WalletIcon />
|
|
577
|
+
<span>{submitText || `Pay ${formatAmount(total, selectedCurrency)}`}</span>
|
|
578
|
+
</>
|
|
579
|
+
)}
|
|
580
|
+
</button>
|
|
581
|
+
|
|
582
|
+
{/* Security Badge */}
|
|
583
|
+
<div style={{
|
|
584
|
+
display: 'flex',
|
|
585
|
+
alignItems: 'center',
|
|
586
|
+
justifyContent: 'center',
|
|
587
|
+
gap: '6px',
|
|
588
|
+
marginTop: '16px',
|
|
589
|
+
fontSize: '12px',
|
|
590
|
+
color: '#94a3b8',
|
|
591
|
+
}}>
|
|
592
|
+
<LockIcon />
|
|
593
|
+
<span>Secure payment • Funds go directly to merchant</span>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
{/* Footer Branding */}
|
|
598
|
+
{showBranding && !embedded && (
|
|
599
|
+
<div style={footerStyles}>
|
|
600
|
+
Powered by <strong style={{ color: brandColor }}>Rotate</strong> • Non-custodial payments
|
|
601
|
+
</div>
|
|
602
|
+
)}
|
|
603
|
+
</div>
|
|
604
|
+
</>
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export default CheckoutForm;
|