@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,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;
|