@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.
Files changed (74) hide show
  1. package/README.md +453 -0
  2. package/dist/catalog.d.ts +112 -0
  3. package/dist/catalog.d.ts.map +1 -0
  4. package/dist/catalog.js +210 -0
  5. package/dist/catalog.js.map +1 -0
  6. package/dist/components/CheckoutForm.d.ts +86 -0
  7. package/dist/components/CheckoutForm.d.ts.map +1 -0
  8. package/dist/components/CheckoutForm.js +332 -0
  9. package/dist/components/CheckoutForm.js.map +1 -0
  10. package/dist/components/HostedCheckout.d.ts +57 -0
  11. package/dist/components/HostedCheckout.d.ts.map +1 -0
  12. package/dist/components/HostedCheckout.js +414 -0
  13. package/dist/components/HostedCheckout.js.map +1 -0
  14. package/dist/components/PaymentButton.d.ts +80 -0
  15. package/dist/components/PaymentButton.d.ts.map +1 -0
  16. package/dist/components/PaymentButton.js +210 -0
  17. package/dist/components/PaymentButton.js.map +1 -0
  18. package/dist/components/RotateProvider.d.ts +115 -0
  19. package/dist/components/RotateProvider.d.ts.map +1 -0
  20. package/dist/components/RotateProvider.js +264 -0
  21. package/dist/components/RotateProvider.js.map +1 -0
  22. package/dist/components/index.d.ts +17 -0
  23. package/dist/components/index.d.ts.map +1 -0
  24. package/dist/components/index.js +27 -0
  25. package/dist/components/index.js.map +1 -0
  26. package/dist/embed.d.ts +85 -0
  27. package/dist/embed.d.ts.map +1 -0
  28. package/dist/embed.js +313 -0
  29. package/dist/embed.js.map +1 -0
  30. package/dist/hooks.d.ts +156 -0
  31. package/dist/hooks.d.ts.map +1 -0
  32. package/dist/hooks.js +280 -0
  33. package/dist/hooks.js.map +1 -0
  34. package/dist/idl/rotate_connect.json +2572 -0
  35. package/dist/index.d.ts +505 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +1197 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/marketplace.d.ts +257 -0
  40. package/dist/marketplace.d.ts.map +1 -0
  41. package/dist/marketplace.js +433 -0
  42. package/dist/marketplace.js.map +1 -0
  43. package/dist/platform.d.ts +234 -0
  44. package/dist/platform.d.ts.map +1 -0
  45. package/dist/platform.js +268 -0
  46. package/dist/platform.js.map +1 -0
  47. package/dist/react.d.ts +140 -0
  48. package/dist/react.d.ts.map +1 -0
  49. package/dist/react.js +429 -0
  50. package/dist/react.js.map +1 -0
  51. package/dist/store.d.ts +213 -0
  52. package/dist/store.d.ts.map +1 -0
  53. package/dist/store.js +404 -0
  54. package/dist/store.js.map +1 -0
  55. package/dist/webhooks.d.ts +149 -0
  56. package/dist/webhooks.d.ts.map +1 -0
  57. package/dist/webhooks.js +371 -0
  58. package/dist/webhooks.js.map +1 -0
  59. package/package.json +114 -0
  60. package/src/catalog.ts +299 -0
  61. package/src/components/CheckoutForm.tsx +608 -0
  62. package/src/components/HostedCheckout.tsx +675 -0
  63. package/src/components/PaymentButton.tsx +348 -0
  64. package/src/components/RotateProvider.tsx +370 -0
  65. package/src/components/index.ts +26 -0
  66. package/src/embed.ts +408 -0
  67. package/src/hooks.ts +518 -0
  68. package/src/idl/rotate_connect.json +2572 -0
  69. package/src/index.ts +1538 -0
  70. package/src/marketplace.ts +642 -0
  71. package/src/platform.ts +403 -0
  72. package/src/react.ts +459 -0
  73. package/src/store.ts +577 -0
  74. 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;