@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,675 @@
1
+ /**
2
+ * Rotate Hosted Checkout
3
+ *
4
+ * A complete, hosted checkout page that handles the entire payment flow.
5
+ * Redirect customers here to pay.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // Use as a full page checkout
10
+ * <HostedCheckout
11
+ * linkId={12345}
12
+ * onSuccess={(result) => router.push(`/success?tx=${result.transactionId}`)}
13
+ * onCancel={() => router.push('/cart')}
14
+ * />
15
+ *
16
+ * // Or redirect to the hosted URL
17
+ * window.location.href = `https://rotate.app/checkout/?link=${linkId}&network=mainnet-beta`;
18
+ * ```
19
+ */
20
+
21
+ import React, { useState, useCallback, useEffect, useMemo, CSSProperties } from 'react';
22
+ import { PublicKey } from '@solana/web3.js';
23
+ import RotateSDK, { PaymentLink, Platform, Merchant } from '../index';
24
+
25
+ // ==================== TYPES ====================
26
+
27
+ export interface HostedCheckoutProps {
28
+ /** Payment link ID */
29
+ linkId: number;
30
+ /** Merchant ID */
31
+ merchantId: number;
32
+ /** Platform ID */
33
+ platformId: number;
34
+ /** Network */
35
+ network?: 'devnet' | 'mainnet-beta';
36
+ /** Custom RPC endpoint */
37
+ rpcEndpoint?: string;
38
+ /** Brand name */
39
+ brandName?: string;
40
+ /** Brand logo URL */
41
+ brandLogo?: string;
42
+ /** Brand color */
43
+ brandColor?: string;
44
+ /** Success callback */
45
+ onSuccess?: (result: PaymentResult) => void;
46
+ /** Cancel callback */
47
+ onCancel?: () => void;
48
+ /** Error callback */
49
+ onError?: (error: Error) => void;
50
+ /** Success redirect URL */
51
+ successUrl?: string;
52
+ /** Cancel redirect URL */
53
+ cancelUrl?: string;
54
+ }
55
+
56
+ export interface PaymentResult {
57
+ linkId: number;
58
+ amount: bigint;
59
+ currency: string;
60
+ transactionId: string;
61
+ paidAt: number;
62
+ }
63
+
64
+ // ==================== STYLES ====================
65
+
66
+ const pageStyles: CSSProperties = {
67
+ minHeight: '100vh',
68
+ background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%)',
69
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
70
+ display: 'flex',
71
+ alignItems: 'center',
72
+ justifyContent: 'center',
73
+ padding: '20px',
74
+ };
75
+
76
+ const cardStyles: CSSProperties = {
77
+ background: '#ffffff',
78
+ borderRadius: '16px',
79
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
80
+ maxWidth: '420px',
81
+ width: '100%',
82
+ overflow: 'hidden',
83
+ };
84
+
85
+ const headerStyles = (color: string): CSSProperties => ({
86
+ background: `linear-gradient(135deg, ${color} 0%, ${adjustColor(color, -30)} 100%)`,
87
+ padding: '24px',
88
+ color: '#ffffff',
89
+ textAlign: 'center' as const,
90
+ });
91
+
92
+ const logoStyles: CSSProperties = {
93
+ width: '48px',
94
+ height: '48px',
95
+ borderRadius: '12px',
96
+ objectFit: 'cover' as const,
97
+ marginBottom: '12px',
98
+ };
99
+
100
+ const amountStyles: CSSProperties = {
101
+ fontSize: '36px',
102
+ fontWeight: 700,
103
+ marginBottom: '4px',
104
+ };
105
+
106
+ const bodyStyles: CSSProperties = {
107
+ padding: '24px',
108
+ };
109
+
110
+ const sectionStyles: CSSProperties = {
111
+ marginBottom: '20px',
112
+ };
113
+
114
+ const labelStyles: CSSProperties = {
115
+ display: 'block',
116
+ fontSize: '12px',
117
+ fontWeight: 600,
118
+ color: '#64748b',
119
+ marginBottom: '8px',
120
+ textTransform: 'uppercase' as const,
121
+ letterSpacing: '0.5px',
122
+ };
123
+
124
+ const currencyGridStyles: CSSProperties = {
125
+ display: 'grid',
126
+ gridTemplateColumns: 'repeat(3, 1fr)',
127
+ gap: '8px',
128
+ };
129
+
130
+ const currencyButtonStyles = (selected: boolean, color: string): CSSProperties => ({
131
+ padding: '16px 12px',
132
+ border: `2px solid ${selected ? color : '#e2e8f0'}`,
133
+ borderRadius: '12px',
134
+ background: selected ? `${color}10` : '#ffffff',
135
+ cursor: 'pointer',
136
+ transition: 'all 0.2s ease',
137
+ display: 'flex',
138
+ flexDirection: 'column' as const,
139
+ alignItems: 'center',
140
+ gap: '8px',
141
+ });
142
+
143
+ const walletSectionStyles: CSSProperties = {
144
+ background: '#f8fafc',
145
+ borderRadius: '12px',
146
+ padding: '20px',
147
+ marginBottom: '20px',
148
+ };
149
+
150
+ const walletButtonStyles = (color: string, loading: boolean): CSSProperties => ({
151
+ width: '100%',
152
+ padding: '16px',
153
+ background: loading ? '#94a3b8' : `linear-gradient(135deg, ${color} 0%, ${adjustColor(color, -20)} 100%)`,
154
+ border: 'none',
155
+ borderRadius: '12px',
156
+ color: '#ffffff',
157
+ fontSize: '16px',
158
+ fontWeight: 600,
159
+ cursor: loading ? 'wait' : 'pointer',
160
+ display: 'flex',
161
+ alignItems: 'center',
162
+ justifyContent: 'center',
163
+ gap: '10px',
164
+ boxShadow: `0 4px 14px ${color}40`,
165
+ transition: 'all 0.2s ease',
166
+ });
167
+
168
+ const detailRowStyles: CSSProperties = {
169
+ display: 'flex',
170
+ justifyContent: 'space-between',
171
+ padding: '12px 0',
172
+ borderBottom: '1px solid #f1f5f9',
173
+ fontSize: '14px',
174
+ };
175
+
176
+ const footerStyles: CSSProperties = {
177
+ padding: '16px 24px',
178
+ background: '#f8fafc',
179
+ textAlign: 'center' as const,
180
+ fontSize: '12px',
181
+ color: '#94a3b8',
182
+ };
183
+
184
+ const cancelLinkStyles: CSSProperties = {
185
+ color: '#64748b',
186
+ textDecoration: 'none',
187
+ fontSize: '14px',
188
+ display: 'block',
189
+ textAlign: 'center' as const,
190
+ marginTop: '16px',
191
+ cursor: 'pointer',
192
+ };
193
+
194
+ const statusStyles = (status: string): CSSProperties => ({
195
+ textAlign: 'center' as const,
196
+ padding: '40px 20px',
197
+ });
198
+
199
+ const statusIconStyles = (success: boolean): CSSProperties => ({
200
+ width: '80px',
201
+ height: '80px',
202
+ borderRadius: '50%',
203
+ background: success ? '#10B981' : '#EF4444',
204
+ display: 'flex',
205
+ alignItems: 'center',
206
+ justifyContent: 'center',
207
+ margin: '0 auto 20px',
208
+ fontSize: '40px',
209
+ });
210
+
211
+ function adjustColor(hex: string, percent: number): string {
212
+ const num = parseInt(hex.replace('#', ''), 16);
213
+ const amt = Math.round(2.55 * percent);
214
+ const R = (num >> 16) + amt;
215
+ const G = (num >> 8 & 0x00FF) + amt;
216
+ const B = (num & 0x0000FF) + amt;
217
+ return '#' + (
218
+ 0x1000000 +
219
+ (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
220
+ (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
221
+ (B < 255 ? B < 1 ? 0 : B : 255)
222
+ ).toString(16).slice(1);
223
+ }
224
+
225
+ // ==================== ICONS ====================
226
+
227
+ const WalletIcon = ({ name }: { name: string }) => (
228
+ <div style={{
229
+ width: '40px',
230
+ height: '40px',
231
+ borderRadius: '10px',
232
+ background: '#f1f5f9',
233
+ display: 'flex',
234
+ alignItems: 'center',
235
+ justifyContent: 'center',
236
+ fontSize: '20px',
237
+ }}>
238
+ {name === 'Phantom' ? '👻' : name === 'Solflare' ? '🌞' : '💳'}
239
+ </div>
240
+ );
241
+
242
+ const SpinnerIcon = () => (
243
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
244
+ style={{ animation: 'rotate-spin 1s linear infinite' }}>
245
+ <circle cx="12" cy="12" r="10" strokeOpacity="0.25"/>
246
+ <path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round"/>
247
+ </svg>
248
+ );
249
+
250
+ // ==================== COMPONENT ====================
251
+
252
+ export function HostedCheckout({
253
+ linkId,
254
+ merchantId,
255
+ platformId,
256
+ network = 'mainnet-beta',
257
+ rpcEndpoint,
258
+ brandName = 'Checkout',
259
+ brandLogo,
260
+ brandColor = '#8B5CF6',
261
+ onSuccess,
262
+ onCancel,
263
+ onError,
264
+ successUrl,
265
+ cancelUrl,
266
+ }: HostedCheckoutProps) {
267
+ // State
268
+ const [loading, setLoading] = useState(true);
269
+ const [paying, setPaying] = useState(false);
270
+ const [link, setLink] = useState<PaymentLink | null>(null);
271
+ const [merchant, setMerchant] = useState<Merchant | null>(null);
272
+ const [platform, setPlatform] = useState<Platform | null>(null);
273
+ const [selectedCurrency, setSelectedCurrency] = useState<'SOL' | 'USDC' | 'USDT'>('USDC');
274
+ const [status, setStatus] = useState<'loading' | 'ready' | 'paying' | 'success' | 'error' | 'expired'>('loading');
275
+ const [error, setError] = useState<string | null>(null);
276
+ const [solPrice, setSolPrice] = useState<number>(100);
277
+ const [selectedWallet, setSelectedWallet] = useState<string | null>(null);
278
+
279
+ // Initialize SDK (memoized to prevent re-creation on every render)
280
+ const sdk = useMemo(() => new RotateSDK({ network, rpcEndpoint }), [network, rpcEndpoint]);
281
+
282
+ // Load payment data
283
+ useEffect(() => {
284
+ async function loadData() {
285
+ try {
286
+ const [linkData, merchantData, platformData, price] = await Promise.all([
287
+ sdk.getPaymentLink(linkId),
288
+ sdk.getMerchant(merchantId),
289
+ sdk.getPlatform(platformId),
290
+ sdk.getSolPrice(),
291
+ ]);
292
+
293
+ if (!linkData) {
294
+ setStatus('error');
295
+ setError('Payment link not found');
296
+ return;
297
+ }
298
+
299
+ if (linkData.status === 'Paid') {
300
+ setStatus('success');
301
+ return;
302
+ }
303
+
304
+ if (linkData.status === 'Cancelled') {
305
+ setStatus('error');
306
+ setError('This payment was cancelled');
307
+ return;
308
+ }
309
+
310
+ if (linkData.expiresAt > 0 && Date.now() / 1000 > linkData.expiresAt) {
311
+ setStatus('expired');
312
+ return;
313
+ }
314
+
315
+ setLink(linkData);
316
+ setMerchant(merchantData);
317
+ setPlatform(platformData);
318
+ setSolPrice(price);
319
+ setStatus('ready');
320
+ } catch (e: any) {
321
+ setStatus('error');
322
+ setError(e.message);
323
+ onError?.(e);
324
+ } finally {
325
+ setLoading(false);
326
+ }
327
+ }
328
+
329
+ loadData();
330
+ // eslint-disable-next-line react-hooks/exhaustive-deps
331
+ }, [linkId, merchantId, platformId, sdk]);
332
+
333
+ // Format amount
334
+ const formatAmount = (lamports: bigint, currency: string): string => {
335
+ const amount = Number(lamports) / (currency === 'SOL' ? 1e9 : 1e6);
336
+
337
+ if (currency === 'SOL') {
338
+ return `${amount.toFixed(amount < 1 ? 4 : 3)} SOL`;
339
+ }
340
+ return `$${amount.toFixed(2)}`;
341
+ };
342
+
343
+ // Get amount in selected currency
344
+ const getAmountInCurrency = (): string => {
345
+ if (!link) return '—';
346
+
347
+ const usdAmount = Number(link.amount) / 1e6; // Assuming USD base
348
+
349
+ if (selectedCurrency === 'SOL') {
350
+ const solAmount = usdAmount / solPrice;
351
+ return `${solAmount.toFixed(solAmount < 1 ? 4 : 3)} SOL`;
352
+ }
353
+
354
+ return `$${usdAmount.toFixed(2)}`;
355
+ };
356
+
357
+ // Handle wallet connect and payment
358
+ const connectWallet = useCallback(async (walletName: string) => {
359
+ setSelectedWallet(walletName);
360
+ setPaying(true);
361
+ setStatus('paying');
362
+
363
+ try {
364
+ // Detect wallet adapter
365
+ let wallet: any = null;
366
+
367
+ if (walletName === 'Phantom' && (window as any).solana?.isPhantom) {
368
+ wallet = (window as any).solana;
369
+ } else if (walletName === 'Solflare' && (window as any).solflare?.isSolflare) {
370
+ wallet = (window as any).solflare;
371
+ }
372
+
373
+ if (!wallet) {
374
+ throw new Error(`${walletName} wallet not found. Please install it.`);
375
+ }
376
+
377
+ // Connect wallet
378
+ const response = await wallet.connect();
379
+ const buyerPublicKey = response.publicKey;
380
+
381
+ if (!link) throw new Error('Payment link not loaded');
382
+
383
+ // Initialize SDK with the connected wallet for transaction signing
384
+ sdk.initWithWallet({
385
+ publicKey: buyerPublicKey,
386
+ signTransaction: (tx: any) => wallet.signTransaction(tx),
387
+ signAllTransactions: (txs: any[]) => wallet.signAllTransactions(txs),
388
+ } as any);
389
+
390
+ let txSignature: string;
391
+
392
+ const baseParams = {
393
+ linkId,
394
+ merchantId,
395
+ platformId,
396
+ amount: link.amount,
397
+ };
398
+
399
+ // Route to the correct payment method based on link type and selected currency.
400
+ // For USD-denominated links (tokenType === 'Usd'), the user selects which
401
+ // currency to pay with. For native SOL/token links, use the matching payment
402
+ // method directly.
403
+ if (link.tokenType === 'Usdc' || link.tokenType === 'Usdt') {
404
+ // Native token link — pay with the link's token
405
+ const currency = link.tokenType === 'Usdc' ? 'USDC' : 'USDT';
406
+ txSignature = await sdk.payLinkToken({ ...baseParams, currency } as any);
407
+ } else if (link.tokenType === 'Sol') {
408
+ // Native SOL link — pay with SOL directly
409
+ txSignature = await sdk.payLinkSol({ ...baseParams } as any);
410
+ } else if (selectedCurrency === 'SOL') {
411
+ // USD-denominated link paid in SOL
412
+ const lamportsAmount = await sdk.microUsdToLamports(link.amount);
413
+ txSignature = await sdk.payLinkUsdSol({
414
+ ...baseParams,
415
+ lamportsAmount,
416
+ } as any);
417
+ } else {
418
+ // USD-denominated link paid with USDC/USDT
419
+ txSignature = await sdk.payLinkUsdToken({
420
+ ...baseParams,
421
+ currency: selectedCurrency as 'USDC' | 'USDT',
422
+ } as any);
423
+ }
424
+
425
+ const result: PaymentResult = {
426
+ linkId,
427
+ amount: link.amount,
428
+ currency: selectedCurrency,
429
+ transactionId: txSignature,
430
+ paidAt: Date.now(),
431
+ };
432
+
433
+ setStatus('success');
434
+ onSuccess?.(result);
435
+
436
+ // Post message to parent if in popup
437
+ if (window.opener) {
438
+ window.opener.postMessage({ type: 'rotate_payment_success', payment: result }, '*');
439
+ }
440
+
441
+ // Redirect if URL provided
442
+ if (successUrl) {
443
+ setTimeout(() => {
444
+ window.location.href = successUrl;
445
+ }, 2000);
446
+ }
447
+
448
+ } catch (e: any) {
449
+ setPaying(false);
450
+ setStatus('ready');
451
+ setError(e.message);
452
+ onError?.(e);
453
+ }
454
+ }, [link, linkId, merchantId, platformId, sdk, selectedCurrency, onSuccess, onError, successUrl]);
455
+
456
+ // Handle cancel
457
+ const handleCancel = useCallback(() => {
458
+ if (window.opener) {
459
+ window.opener.postMessage({ type: 'rotate_payment_cancel' }, '*');
460
+ window.close();
461
+ } else if (cancelUrl) {
462
+ window.location.href = cancelUrl;
463
+ } else {
464
+ onCancel?.();
465
+ }
466
+ }, [cancelUrl, onCancel]);
467
+
468
+ // Render loading state
469
+ if (status === 'loading') {
470
+ return (
471
+ <div style={pageStyles}>
472
+ <style>{`@keyframes rotate-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`}</style>
473
+ <div style={cardStyles}>
474
+ <div style={{ ...statusStyles('loading'), padding: '60px 20px' }}>
475
+ <SpinnerIcon />
476
+ <p style={{ marginTop: '16px', color: '#64748b' }}>Loading payment details...</p>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ );
481
+ }
482
+
483
+ // Render success state
484
+ if (status === 'success') {
485
+ return (
486
+ <div style={pageStyles}>
487
+ <div style={cardStyles}>
488
+ <div style={statusStyles('success')}>
489
+ <div style={statusIconStyles(true)}>✓</div>
490
+ <h2 style={{ fontSize: '24px', fontWeight: 700, marginBottom: '8px' }}>Payment Successful!</h2>
491
+ <p style={{ color: '#64748b' }}>Thank you for your payment.</p>
492
+ {successUrl && (
493
+ <p style={{ color: '#64748b', marginTop: '16px', fontSize: '14px' }}>
494
+ Redirecting you back...
495
+ </p>
496
+ )}
497
+ </div>
498
+ </div>
499
+ </div>
500
+ );
501
+ }
502
+
503
+ // Render error state
504
+ if (status === 'error' || status === 'expired') {
505
+ return (
506
+ <div style={pageStyles}>
507
+ <div style={cardStyles}>
508
+ <div style={statusStyles('error')}>
509
+ <div style={statusIconStyles(false)}>✕</div>
510
+ <h2 style={{ fontSize: '24px', fontWeight: 700, marginBottom: '8px' }}>
511
+ {status === 'expired' ? 'Payment Expired' : 'Payment Error'}
512
+ </h2>
513
+ <p style={{ color: '#64748b' }}>
514
+ {status === 'expired'
515
+ ? 'This payment link has expired.'
516
+ : error || 'Something went wrong.'}
517
+ </p>
518
+ <button
519
+ onClick={handleCancel}
520
+ style={{
521
+ marginTop: '24px',
522
+ padding: '12px 24px',
523
+ background: '#f1f5f9',
524
+ border: 'none',
525
+ borderRadius: '8px',
526
+ fontSize: '14px',
527
+ fontWeight: 500,
528
+ cursor: 'pointer',
529
+ }}
530
+ >
531
+ Go Back
532
+ </button>
533
+ </div>
534
+ </div>
535
+ </div>
536
+ );
537
+ }
538
+
539
+ // Main checkout render
540
+ return (
541
+ <div style={pageStyles}>
542
+ <style>{`@keyframes rotate-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`}</style>
543
+
544
+ <div style={cardStyles}>
545
+ {/* Header */}
546
+ <div style={headerStyles(brandColor)}>
547
+ {brandLogo && <img src={brandLogo} alt={brandName} style={logoStyles} />}
548
+ <div style={{ fontSize: '14px', opacity: 0.9, marginBottom: '8px' }}>{brandName}</div>
549
+ <div style={amountStyles}>{getAmountInCurrency()}</div>
550
+ <div style={{ fontSize: '14px', opacity: 0.8 }}>Link #{linkId}</div>
551
+ </div>
552
+
553
+ {/* Body */}
554
+ <div style={bodyStyles}>
555
+ {/* Error Alert */}
556
+ {error && (
557
+ <div style={{
558
+ padding: '12px 16px',
559
+ background: '#fef2f2',
560
+ border: '1px solid #fecaca',
561
+ borderRadius: '8px',
562
+ color: '#dc2626',
563
+ fontSize: '14px',
564
+ marginBottom: '16px',
565
+ }}>
566
+ {error}
567
+ </div>
568
+ )}
569
+
570
+ {/* Currency Selection */}
571
+ <div style={sectionStyles}>
572
+ <label style={labelStyles}>Pay with</label>
573
+ <div style={currencyGridStyles}>
574
+ {(['USDC', 'USDT', 'SOL'] as const).map((curr) => (
575
+ <button
576
+ key={curr}
577
+ type="button"
578
+ onClick={() => setSelectedCurrency(curr)}
579
+ style={currencyButtonStyles(selectedCurrency === curr, brandColor)}
580
+ >
581
+ <span style={{ fontSize: '20px' }}>
582
+ {curr === 'SOL' ? '◎' : '$'}
583
+ </span>
584
+ <span style={{
585
+ fontSize: '14px',
586
+ fontWeight: selectedCurrency === curr ? 600 : 400,
587
+ color: selectedCurrency === curr ? brandColor : '#64748b',
588
+ }}>
589
+ {curr}
590
+ </span>
591
+ </button>
592
+ ))}
593
+ </div>
594
+ </div>
595
+
596
+ {/* Payment Details */}
597
+ <div style={sectionStyles}>
598
+ <label style={labelStyles}>Payment Details</label>
599
+ <div style={{ background: '#f8fafc', borderRadius: '8px', padding: '4px 16px' }}>
600
+ <div style={detailRowStyles}>
601
+ <span style={{ color: '#64748b' }}>Amount</span>
602
+ <span style={{ fontWeight: 500 }}>{getAmountInCurrency()}</span>
603
+ </div>
604
+ <div style={detailRowStyles}>
605
+ <span style={{ color: '#64748b' }}>Network Fee</span>
606
+ <span style={{ fontWeight: 500 }}>~$0.01</span>
607
+ </div>
608
+ <div style={{ ...detailRowStyles, borderBottom: 'none' }}>
609
+ <span style={{ fontWeight: 600 }}>Total</span>
610
+ <span style={{ fontWeight: 700, color: brandColor }}>{getAmountInCurrency()}</span>
611
+ </div>
612
+ </div>
613
+ </div>
614
+
615
+ {/* Wallet Selection */}
616
+ <div style={walletSectionStyles}>
617
+ <label style={labelStyles}>Connect Wallet</label>
618
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
619
+ {['Phantom', 'Solflare'].map((wallet) => (
620
+ <button
621
+ key={wallet}
622
+ onClick={() => connectWallet(wallet)}
623
+ disabled={paying}
624
+ style={{
625
+ display: 'flex',
626
+ alignItems: 'center',
627
+ gap: '12px',
628
+ padding: '14px 16px',
629
+ background: '#ffffff',
630
+ border: '2px solid #e2e8f0',
631
+ borderRadius: '10px',
632
+ cursor: paying ? 'wait' : 'pointer',
633
+ transition: 'all 0.2s ease',
634
+ width: '100%',
635
+ }}
636
+ >
637
+ <WalletIcon name={wallet} />
638
+ <span style={{ flex: 1, textAlign: 'left', fontWeight: 500 }}>
639
+ {paying && selectedWallet === wallet ? (
640
+ <span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
641
+ <SpinnerIcon /> Confirming...
642
+ </span>
643
+ ) : (
644
+ `Pay with ${wallet}`
645
+ )}
646
+ </span>
647
+ </button>
648
+ ))}
649
+ </div>
650
+ </div>
651
+
652
+ {/* Cancel Link */}
653
+ <a onClick={handleCancel} style={cancelLinkStyles}>
654
+ Cancel and go back
655
+ </a>
656
+ </div>
657
+
658
+ {/* Footer */}
659
+ <div style={footerStyles}>
660
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
661
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="#94a3b8">
662
+ <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-7h14v7z"/>
663
+ </svg>
664
+ <span>Secured by <strong style={{ color: brandColor }}>Rotate Protocol</strong></span>
665
+ </div>
666
+ <div style={{ marginTop: '4px', fontSize: '11px' }}>
667
+ Funds go directly to merchant • Non-custodial
668
+ </div>
669
+ </div>
670
+ </div>
671
+ </div>
672
+ );
673
+ }
674
+
675
+ export default HostedCheckout;