@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,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotate Payment Button
|
|
3
|
+
*
|
|
4
|
+
* Drop-in payment button that handles the entire checkout flow.
|
|
5
|
+
* Just add to your page and you're accepting payments!
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Simple usage - just amount!
|
|
10
|
+
* <PaymentButton amount={29.99} />
|
|
11
|
+
*
|
|
12
|
+
* // With customization
|
|
13
|
+
* <PaymentButton
|
|
14
|
+
* amount={99.99}
|
|
15
|
+
* currency="USD"
|
|
16
|
+
* label="Buy Now"
|
|
17
|
+
* description="Premium Plan"
|
|
18
|
+
* onSuccess={(payment) => console.log('Paid!', payment)}
|
|
19
|
+
* />
|
|
20
|
+
*
|
|
21
|
+
* // Styled button
|
|
22
|
+
* <PaymentButton
|
|
23
|
+
* amount={49.99}
|
|
24
|
+
* variant="outline"
|
|
25
|
+
* size="lg"
|
|
26
|
+
* className="my-custom-class"
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import React, { useState, useCallback, CSSProperties } from 'react';
|
|
32
|
+
import { useRotateContext } from './RotateProvider';
|
|
33
|
+
|
|
34
|
+
// ==================== TYPES ====================
|
|
35
|
+
|
|
36
|
+
export interface PaymentButtonProps {
|
|
37
|
+
/** Amount to charge */
|
|
38
|
+
amount: number;
|
|
39
|
+
/** Currency (default: USD) */
|
|
40
|
+
currency?: 'SOL' | 'USDC' | 'USDT' | 'USD';
|
|
41
|
+
/** Button label (default: "Pay {amount}") */
|
|
42
|
+
label?: string;
|
|
43
|
+
/** Product/order description */
|
|
44
|
+
description?: string;
|
|
45
|
+
/** Your order reference ID */
|
|
46
|
+
orderRef?: string;
|
|
47
|
+
/** Allow customer tips */
|
|
48
|
+
allowTips?: boolean;
|
|
49
|
+
/** Button variant */
|
|
50
|
+
variant?: 'solid' | 'outline' | 'ghost';
|
|
51
|
+
/** Button size */
|
|
52
|
+
size?: 'sm' | 'md' | 'lg';
|
|
53
|
+
/** Disabled state */
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
/** Custom class name */
|
|
56
|
+
className?: string;
|
|
57
|
+
/** Custom inline styles */
|
|
58
|
+
style?: CSSProperties;
|
|
59
|
+
/** Success callback */
|
|
60
|
+
onSuccess?: (payment: PaymentResult) => void;
|
|
61
|
+
/** Error callback */
|
|
62
|
+
onError?: (error: Error) => void;
|
|
63
|
+
/** Cancel callback */
|
|
64
|
+
onCancel?: () => void;
|
|
65
|
+
/** Loading state change callback */
|
|
66
|
+
onLoadingChange?: (loading: boolean) => void;
|
|
67
|
+
/** Open in popup instead of redirect */
|
|
68
|
+
popup?: boolean;
|
|
69
|
+
/** Custom success URL */
|
|
70
|
+
successUrl?: string;
|
|
71
|
+
/** Custom cancel URL */
|
|
72
|
+
cancelUrl?: string;
|
|
73
|
+
/** Additional metadata */
|
|
74
|
+
metadata?: Record<string, string>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface PaymentResult {
|
|
78
|
+
linkId: number;
|
|
79
|
+
amount: number;
|
|
80
|
+
currency: string;
|
|
81
|
+
transactionId: string;
|
|
82
|
+
paidAt: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ==================== STYLES ====================
|
|
86
|
+
|
|
87
|
+
const baseStyles: CSSProperties = {
|
|
88
|
+
display: 'inline-flex',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
gap: '8px',
|
|
92
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
93
|
+
fontWeight: 600,
|
|
94
|
+
border: 'none',
|
|
95
|
+
borderRadius: '8px',
|
|
96
|
+
cursor: 'pointer',
|
|
97
|
+
transition: 'all 0.2s ease',
|
|
98
|
+
position: 'relative' as const,
|
|
99
|
+
overflow: 'hidden',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const sizeStyles: Record<string, CSSProperties> = {
|
|
103
|
+
sm: { padding: '8px 16px', fontSize: '14px', borderRadius: '6px' },
|
|
104
|
+
md: { padding: '12px 24px', fontSize: '16px', borderRadius: '8px' },
|
|
105
|
+
lg: { padding: '16px 32px', fontSize: '18px', borderRadius: '10px' },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const variantStyles: Record<string, (color: string) => CSSProperties> = {
|
|
109
|
+
solid: (color) => ({
|
|
110
|
+
background: `linear-gradient(135deg, ${color} 0%, ${adjustColor(color, -20)} 100%)`,
|
|
111
|
+
color: '#ffffff',
|
|
112
|
+
boxShadow: `0 4px 14px ${color}40`,
|
|
113
|
+
}),
|
|
114
|
+
outline: (color) => ({
|
|
115
|
+
background: 'transparent',
|
|
116
|
+
color: color,
|
|
117
|
+
border: `2px solid ${color}`,
|
|
118
|
+
boxShadow: 'none',
|
|
119
|
+
}),
|
|
120
|
+
ghost: (color) => ({
|
|
121
|
+
background: `${color}15`,
|
|
122
|
+
color: color,
|
|
123
|
+
border: 'none',
|
|
124
|
+
boxShadow: 'none',
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function adjustColor(hex: string, percent: number): string {
|
|
129
|
+
const num = parseInt(hex.replace('#', ''), 16);
|
|
130
|
+
const amt = Math.round(2.55 * percent);
|
|
131
|
+
const R = (num >> 16) + amt;
|
|
132
|
+
const G = (num >> 8 & 0x00FF) + amt;
|
|
133
|
+
const B = (num & 0x0000FF) + amt;
|
|
134
|
+
return '#' + (
|
|
135
|
+
0x1000000 +
|
|
136
|
+
(R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
|
|
137
|
+
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
|
|
138
|
+
(B < 255 ? B < 1 ? 0 : B : 255)
|
|
139
|
+
).toString(16).slice(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ==================== ICONS ====================
|
|
143
|
+
|
|
144
|
+
const WalletIcon = () => (
|
|
145
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
146
|
+
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
|
|
147
|
+
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
|
|
148
|
+
<path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/>
|
|
149
|
+
</svg>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const SpinnerIcon = () => (
|
|
153
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ animation: 'rotate-spin 1s linear infinite' }}>
|
|
154
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25"/>
|
|
155
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round"/>
|
|
156
|
+
</svg>
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const CheckIcon = () => (
|
|
160
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
161
|
+
<polyline points="20 6 9 17 4 12"/>
|
|
162
|
+
</svg>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// ==================== COMPONENT ====================
|
|
166
|
+
|
|
167
|
+
export function PaymentButton({
|
|
168
|
+
amount,
|
|
169
|
+
currency = 'USD',
|
|
170
|
+
label,
|
|
171
|
+
description,
|
|
172
|
+
orderRef,
|
|
173
|
+
allowTips = false,
|
|
174
|
+
variant = 'solid',
|
|
175
|
+
size = 'md',
|
|
176
|
+
disabled = false,
|
|
177
|
+
className = '',
|
|
178
|
+
style = {},
|
|
179
|
+
onSuccess,
|
|
180
|
+
onError,
|
|
181
|
+
onCancel,
|
|
182
|
+
onLoadingChange,
|
|
183
|
+
popup = true,
|
|
184
|
+
successUrl,
|
|
185
|
+
cancelUrl,
|
|
186
|
+
metadata,
|
|
187
|
+
}: PaymentButtonProps) {
|
|
188
|
+
const { config, createCheckoutSession, getCheckoutUrl, calculateTotal } = useRotateContext();
|
|
189
|
+
const [loading, setLoading] = useState(false);
|
|
190
|
+
const [success, setSuccess] = useState(false);
|
|
191
|
+
|
|
192
|
+
const brandColor = config.brandColor || '#8B5CF6';
|
|
193
|
+
const { total } = calculateTotal(amount);
|
|
194
|
+
|
|
195
|
+
// Format amount for display
|
|
196
|
+
const formatAmount = (amt: number, curr: string) => {
|
|
197
|
+
if (curr === 'SOL') {
|
|
198
|
+
return `${amt.toFixed(amt < 1 ? 4 : 2)} SOL`;
|
|
199
|
+
}
|
|
200
|
+
return `$${amt.toFixed(2)}`;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const buttonLabel = label || `Pay ${formatAmount(total, currency)}`;
|
|
204
|
+
|
|
205
|
+
// Handle payment click
|
|
206
|
+
const handleClick = useCallback(async () => {
|
|
207
|
+
if (disabled || loading || success) return;
|
|
208
|
+
|
|
209
|
+
setLoading(true);
|
|
210
|
+
onLoadingChange?.(true);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
// Create checkout session
|
|
214
|
+
const session = await createCheckoutSession({
|
|
215
|
+
amount,
|
|
216
|
+
currency,
|
|
217
|
+
description,
|
|
218
|
+
orderRef,
|
|
219
|
+
allowTips,
|
|
220
|
+
successUrl: successUrl || config.successUrl,
|
|
221
|
+
cancelUrl: cancelUrl || config.cancelUrl,
|
|
222
|
+
metadata,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Get checkout URL
|
|
226
|
+
const checkoutUrl = getCheckoutUrl(session.linkId);
|
|
227
|
+
|
|
228
|
+
if (popup) {
|
|
229
|
+
// Open in popup window
|
|
230
|
+
const width = 450;
|
|
231
|
+
const height = 700;
|
|
232
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
233
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
234
|
+
|
|
235
|
+
const popupWindow = window.open(
|
|
236
|
+
checkoutUrl,
|
|
237
|
+
'rotate_checkout',
|
|
238
|
+
`width=${width},height=${height},left=${left},top=${top},popup=1`
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Listen for popup close or success message
|
|
242
|
+
if (popupWindow) {
|
|
243
|
+
const checkClosed = setInterval(() => {
|
|
244
|
+
if (popupWindow.closed) {
|
|
245
|
+
clearInterval(checkClosed);
|
|
246
|
+
setLoading(false);
|
|
247
|
+
onLoadingChange?.(false);
|
|
248
|
+
// Could be cancelled or completed
|
|
249
|
+
}
|
|
250
|
+
}, 500);
|
|
251
|
+
|
|
252
|
+
// Listen for postMessage from checkout
|
|
253
|
+
window.addEventListener('message', function handler(event) {
|
|
254
|
+
if (event.data?.type === 'rotate_payment_success') {
|
|
255
|
+
clearInterval(checkClosed);
|
|
256
|
+
setLoading(false);
|
|
257
|
+
setSuccess(true);
|
|
258
|
+
onLoadingChange?.(false);
|
|
259
|
+
onSuccess?.(event.data.payment);
|
|
260
|
+
popupWindow.close();
|
|
261
|
+
window.removeEventListener('message', handler);
|
|
262
|
+
|
|
263
|
+
// Reset success state after animation
|
|
264
|
+
setTimeout(() => setSuccess(false), 2000);
|
|
265
|
+
} else if (event.data?.type === 'rotate_payment_cancel') {
|
|
266
|
+
clearInterval(checkClosed);
|
|
267
|
+
setLoading(false);
|
|
268
|
+
onLoadingChange?.(false);
|
|
269
|
+
onCancel?.();
|
|
270
|
+
popupWindow.close();
|
|
271
|
+
window.removeEventListener('message', handler);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// Redirect to checkout
|
|
277
|
+
window.location.href = checkoutUrl;
|
|
278
|
+
}
|
|
279
|
+
} catch (error: any) {
|
|
280
|
+
setLoading(false);
|
|
281
|
+
onLoadingChange?.(false);
|
|
282
|
+
onError?.(error);
|
|
283
|
+
}
|
|
284
|
+
}, [
|
|
285
|
+
amount, currency, description, orderRef, allowTips,
|
|
286
|
+
disabled, loading, success, popup,
|
|
287
|
+
config, createCheckoutSession, getCheckoutUrl,
|
|
288
|
+
successUrl, cancelUrl, metadata,
|
|
289
|
+
onSuccess, onError, onCancel, onLoadingChange,
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
// Combine styles
|
|
293
|
+
const combinedStyles: CSSProperties = {
|
|
294
|
+
...baseStyles,
|
|
295
|
+
...sizeStyles[size],
|
|
296
|
+
...variantStyles[variant](brandColor),
|
|
297
|
+
...(disabled && { opacity: 0.5, cursor: 'not-allowed' }),
|
|
298
|
+
...(success && {
|
|
299
|
+
background: '#10B981',
|
|
300
|
+
boxShadow: '0 4px 14px rgba(16, 185, 129, 0.4)',
|
|
301
|
+
}),
|
|
302
|
+
...style,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<>
|
|
307
|
+
<style>{`
|
|
308
|
+
@keyframes rotate-spin {
|
|
309
|
+
from { transform: rotate(0deg); }
|
|
310
|
+
to { transform: rotate(360deg); }
|
|
311
|
+
}
|
|
312
|
+
.rotate-btn:hover:not(:disabled) {
|
|
313
|
+
transform: translateY(-2px);
|
|
314
|
+
filter: brightness(1.1);
|
|
315
|
+
}
|
|
316
|
+
.rotate-btn:active:not(:disabled) {
|
|
317
|
+
transform: translateY(0);
|
|
318
|
+
}
|
|
319
|
+
`}</style>
|
|
320
|
+
<button
|
|
321
|
+
className={`rotate-btn ${className}`}
|
|
322
|
+
style={combinedStyles}
|
|
323
|
+
onClick={handleClick}
|
|
324
|
+
disabled={disabled || loading}
|
|
325
|
+
type="button"
|
|
326
|
+
>
|
|
327
|
+
{success ? (
|
|
328
|
+
<>
|
|
329
|
+
<CheckIcon />
|
|
330
|
+
<span>Paid!</span>
|
|
331
|
+
</>
|
|
332
|
+
) : loading ? (
|
|
333
|
+
<>
|
|
334
|
+
<SpinnerIcon />
|
|
335
|
+
<span>Processing...</span>
|
|
336
|
+
</>
|
|
337
|
+
) : (
|
|
338
|
+
<>
|
|
339
|
+
<WalletIcon />
|
|
340
|
+
<span>{buttonLabel}</span>
|
|
341
|
+
</>
|
|
342
|
+
)}
|
|
343
|
+
</button>
|
|
344
|
+
</>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export default PaymentButton;
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotate Provider - Context Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides Rotate SDK context to your entire app with one simple wrapper.
|
|
5
|
+
* Wrap your app once, and all Rotate components will work seamlessly.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { RotateProvider } from '@rotateprotocol/sdk/components';
|
|
10
|
+
*
|
|
11
|
+
* function App() {
|
|
12
|
+
* return (
|
|
13
|
+
* <RotateProvider
|
|
14
|
+
* merchantId={1000000}
|
|
15
|
+
* platformId={1000000}
|
|
16
|
+
* network="mainnet-beta"
|
|
17
|
+
* >
|
|
18
|
+
* <YourApp />
|
|
19
|
+
* </RotateProvider>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import React, { createContext, useContext, useMemo, useState, useCallback, useEffect, ReactNode } from 'react';
|
|
26
|
+
import { PublicKey } from '@solana/web3.js';
|
|
27
|
+
import RotateSDK, {
|
|
28
|
+
Network,
|
|
29
|
+
Platform,
|
|
30
|
+
Merchant,
|
|
31
|
+
calculateFees,
|
|
32
|
+
} from '../index';
|
|
33
|
+
|
|
34
|
+
// ==================== TYPES ====================
|
|
35
|
+
|
|
36
|
+
export interface RotateConfig {
|
|
37
|
+
/** Your merchant ID (7-digit number) */
|
|
38
|
+
merchantId: number;
|
|
39
|
+
/** Platform ID (7-digit number) */
|
|
40
|
+
platformId: number;
|
|
41
|
+
/** Network to use */
|
|
42
|
+
network?: Network;
|
|
43
|
+
/** Custom RPC endpoint */
|
|
44
|
+
rpcEndpoint?: string;
|
|
45
|
+
/** Your brand name (shown on checkout) */
|
|
46
|
+
brandName?: string;
|
|
47
|
+
/** Your brand logo URL */
|
|
48
|
+
brandLogo?: string;
|
|
49
|
+
/** Primary brand color (hex) */
|
|
50
|
+
brandColor?: string;
|
|
51
|
+
/** Success redirect URL */
|
|
52
|
+
successUrl?: string;
|
|
53
|
+
/** Cancel redirect URL */
|
|
54
|
+
cancelUrl?: string;
|
|
55
|
+
/** Webhook URL for payment notifications */
|
|
56
|
+
webhookUrl?: string;
|
|
57
|
+
/** Custom checkout base URL (default: https://rotate.app) */
|
|
58
|
+
checkoutBaseUrl?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CheckoutSession {
|
|
62
|
+
id: string;
|
|
63
|
+
linkId: number;
|
|
64
|
+
amount: number;
|
|
65
|
+
currency: 'SOL' | 'USDC' | 'USDT' | 'USD';
|
|
66
|
+
status: 'pending' | 'paid' | 'expired' | 'cancelled';
|
|
67
|
+
createdAt: number;
|
|
68
|
+
expiresAt?: number;
|
|
69
|
+
metadata?: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface RotateContextType {
|
|
73
|
+
// Configuration
|
|
74
|
+
config: RotateConfig;
|
|
75
|
+
sdk: RotateSDK;
|
|
76
|
+
connected: boolean;
|
|
77
|
+
|
|
78
|
+
// Merchant data
|
|
79
|
+
merchant: Merchant | null;
|
|
80
|
+
platform: Platform | null;
|
|
81
|
+
|
|
82
|
+
// Checkout methods
|
|
83
|
+
createCheckoutSession: (params: CreateCheckoutParams) => Promise<CheckoutSession>;
|
|
84
|
+
getCheckoutUrl: (linkId: number) => string;
|
|
85
|
+
|
|
86
|
+
// Payment helpers
|
|
87
|
+
calculateTotal: (amount: number) => {
|
|
88
|
+
subtotal: number;
|
|
89
|
+
fees: number;
|
|
90
|
+
total: number;
|
|
91
|
+
merchantReceives: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// State
|
|
95
|
+
loading: boolean;
|
|
96
|
+
error: string | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CreateCheckoutParams {
|
|
100
|
+
/** Amount in the specified currency */
|
|
101
|
+
amount: number;
|
|
102
|
+
/** Currency (default: USD) */
|
|
103
|
+
currency?: 'SOL' | 'USDC' | 'USDT' | 'USD';
|
|
104
|
+
/** Order description */
|
|
105
|
+
description?: string;
|
|
106
|
+
/** Order reference (your order ID) */
|
|
107
|
+
orderRef?: string;
|
|
108
|
+
/** Allow customer tips */
|
|
109
|
+
allowTips?: boolean;
|
|
110
|
+
/** Expiration in seconds (default: 3600 = 1 hour) */
|
|
111
|
+
expiresIn?: number;
|
|
112
|
+
/** Custom metadata */
|
|
113
|
+
metadata?: Record<string, string>;
|
|
114
|
+
/** Success redirect URL (overrides provider config) */
|
|
115
|
+
successUrl?: string;
|
|
116
|
+
/** Cancel redirect URL (overrides provider config) */
|
|
117
|
+
cancelUrl?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ==================== CONTEXT ====================
|
|
121
|
+
|
|
122
|
+
const RotateContext = createContext<RotateContextType | null>(null);
|
|
123
|
+
|
|
124
|
+
// ==================== PROVIDER ====================
|
|
125
|
+
|
|
126
|
+
export interface RotateProviderProps {
|
|
127
|
+
children: ReactNode;
|
|
128
|
+
merchantId: number;
|
|
129
|
+
platformId: number;
|
|
130
|
+
network?: Network;
|
|
131
|
+
rpcEndpoint?: string;
|
|
132
|
+
brandName?: string;
|
|
133
|
+
brandLogo?: string;
|
|
134
|
+
brandColor?: string;
|
|
135
|
+
successUrl?: string;
|
|
136
|
+
cancelUrl?: string;
|
|
137
|
+
webhookUrl?: string;
|
|
138
|
+
/** Custom checkout base URL (default: https://rotate.app) */
|
|
139
|
+
checkoutBaseUrl?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function RotateProvider({
|
|
143
|
+
children,
|
|
144
|
+
merchantId,
|
|
145
|
+
platformId,
|
|
146
|
+
network = 'mainnet-beta',
|
|
147
|
+
rpcEndpoint,
|
|
148
|
+
brandName,
|
|
149
|
+
brandLogo,
|
|
150
|
+
brandColor = '#8B5CF6',
|
|
151
|
+
successUrl,
|
|
152
|
+
cancelUrl,
|
|
153
|
+
webhookUrl,
|
|
154
|
+
checkoutBaseUrl,
|
|
155
|
+
}: RotateProviderProps) {
|
|
156
|
+
const [merchant, setMerchant] = useState<Merchant | null>(null);
|
|
157
|
+
const [platform, setPlatform] = useState<Platform | null>(null);
|
|
158
|
+
const [loading, setLoading] = useState(true);
|
|
159
|
+
const [error, setError] = useState<string | null>(null);
|
|
160
|
+
const [connected, setConnected] = useState(false);
|
|
161
|
+
|
|
162
|
+
// Config object
|
|
163
|
+
const config: RotateConfig = useMemo(() => ({
|
|
164
|
+
merchantId,
|
|
165
|
+
platformId,
|
|
166
|
+
network,
|
|
167
|
+
rpcEndpoint,
|
|
168
|
+
brandName,
|
|
169
|
+
brandLogo,
|
|
170
|
+
brandColor,
|
|
171
|
+
successUrl,
|
|
172
|
+
cancelUrl,
|
|
173
|
+
webhookUrl,
|
|
174
|
+
checkoutBaseUrl,
|
|
175
|
+
}), [merchantId, platformId, network, rpcEndpoint, brandName, brandLogo, brandColor, successUrl, cancelUrl, webhookUrl, checkoutBaseUrl]);
|
|
176
|
+
|
|
177
|
+
// Initialize SDK
|
|
178
|
+
const sdk = useMemo(() => {
|
|
179
|
+
return new RotateSDK({
|
|
180
|
+
network,
|
|
181
|
+
rpcEndpoint,
|
|
182
|
+
});
|
|
183
|
+
}, [network, rpcEndpoint]);
|
|
184
|
+
|
|
185
|
+
// Load merchant and platform data
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
async function loadData() {
|
|
188
|
+
setLoading(true);
|
|
189
|
+
setError(null);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const [merchantData, platformData] = await Promise.all([
|
|
193
|
+
sdk.getMerchant(merchantId),
|
|
194
|
+
sdk.getPlatform(platformId),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
if (!merchantData) {
|
|
198
|
+
setError(`Merchant #${merchantId} not found`);
|
|
199
|
+
} else if (!platformData) {
|
|
200
|
+
setError(`Platform #${platformId} not found`);
|
|
201
|
+
} else {
|
|
202
|
+
setMerchant(merchantData);
|
|
203
|
+
setPlatform(platformData);
|
|
204
|
+
setConnected(true);
|
|
205
|
+
}
|
|
206
|
+
} catch (e: any) {
|
|
207
|
+
setError(e.message);
|
|
208
|
+
} finally {
|
|
209
|
+
setLoading(false);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
loadData();
|
|
214
|
+
}, [sdk, merchantId, platformId]);
|
|
215
|
+
|
|
216
|
+
// Calculate total with fees
|
|
217
|
+
// `amount` is in human-readable units (e.g. dollars).
|
|
218
|
+
// calculateFees uses integer math, so we convert to micro-units first,
|
|
219
|
+
// then convert back to dollars for display.
|
|
220
|
+
const calculateTotal = useCallback((amount: number) => {
|
|
221
|
+
const platformFeeBps = platform?.feeBps || 0;
|
|
222
|
+
const microAmount = Math.floor(amount * 1_000_000);
|
|
223
|
+
const fees = calculateFees(microAmount, platformFeeBps);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
subtotal: amount,
|
|
227
|
+
fees: fees.buyerFeeShare / 1_000_000,
|
|
228
|
+
total: fees.buyerPays / 1_000_000,
|
|
229
|
+
merchantReceives: fees.merchantReceives / 1_000_000,
|
|
230
|
+
};
|
|
231
|
+
}, [platform]);
|
|
232
|
+
|
|
233
|
+
// Create checkout session — creates a real on-chain payment link via the SDK
|
|
234
|
+
const createCheckoutSession = useCallback(async (params: CreateCheckoutParams): Promise<CheckoutSession> => {
|
|
235
|
+
if (!merchant || !platform) {
|
|
236
|
+
throw new Error('Merchant or platform not loaded');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const currency = params.currency || 'USD';
|
|
240
|
+
const expiresAt = params.expiresIn
|
|
241
|
+
? Math.floor(Date.now() / 1000) + params.expiresIn
|
|
242
|
+
: 0;
|
|
243
|
+
const orderRef = params.orderRef || Date.now().toString();
|
|
244
|
+
|
|
245
|
+
// Convert amount to on-chain micro-units (6 decimal places for USD/USDC/USDT, lamports for SOL)
|
|
246
|
+
let amountMicro: bigint;
|
|
247
|
+
if (currency === 'SOL') {
|
|
248
|
+
// params.amount is in SOL; convert to lamports
|
|
249
|
+
amountMicro = BigInt(Math.floor(params.amount * 1_000_000_000));
|
|
250
|
+
} else {
|
|
251
|
+
// USD / USDC / USDT: params.amount is in dollars; convert to micro-units (6 decimals)
|
|
252
|
+
amountMicro = BigInt(Math.floor(params.amount * 1_000_000));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let linkId: number;
|
|
256
|
+
let txSig: string;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
if (currency === 'SOL') {
|
|
260
|
+
// Create a SOL-denominated payment link
|
|
261
|
+
const result = await sdk.createLinkSol({
|
|
262
|
+
merchantId: config.merchantId,
|
|
263
|
+
platformId: config.platformId,
|
|
264
|
+
amount: amountMicro,
|
|
265
|
+
expiresAt,
|
|
266
|
+
allowTips: params.allowTips ?? false,
|
|
267
|
+
allowPartial: false,
|
|
268
|
+
orderRef,
|
|
269
|
+
});
|
|
270
|
+
linkId = result.linkId;
|
|
271
|
+
txSig = result.tx;
|
|
272
|
+
} else if (currency === 'USDC' || currency === 'USDT') {
|
|
273
|
+
// Create a token-denominated payment link
|
|
274
|
+
const result = await sdk.createLinkToken({
|
|
275
|
+
merchantId: config.merchantId,
|
|
276
|
+
platformId: config.platformId,
|
|
277
|
+
amount: amountMicro,
|
|
278
|
+
expiresAt,
|
|
279
|
+
allowTips: params.allowTips ?? false,
|
|
280
|
+
allowPartial: false,
|
|
281
|
+
orderRef,
|
|
282
|
+
currency,
|
|
283
|
+
});
|
|
284
|
+
linkId = result.linkId;
|
|
285
|
+
txSig = result.tx;
|
|
286
|
+
} else {
|
|
287
|
+
// USD-denominated link (buyer chooses SOL/USDC/USDT at payment time)
|
|
288
|
+
const result = await sdk.createLinkUsd({
|
|
289
|
+
merchantId: config.merchantId,
|
|
290
|
+
platformId: config.platformId,
|
|
291
|
+
amount: amountMicro,
|
|
292
|
+
expiresAt,
|
|
293
|
+
allowTips: params.allowTips ?? false,
|
|
294
|
+
allowPartial: false,
|
|
295
|
+
orderRef,
|
|
296
|
+
});
|
|
297
|
+
linkId = result.linkId;
|
|
298
|
+
txSig = result.tx;
|
|
299
|
+
}
|
|
300
|
+
} catch (e: any) {
|
|
301
|
+
throw new Error(`Failed to create on-chain payment link: ${e.message}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
id: txSig,
|
|
306
|
+
linkId,
|
|
307
|
+
amount: params.amount,
|
|
308
|
+
currency,
|
|
309
|
+
status: 'pending',
|
|
310
|
+
createdAt: Date.now(),
|
|
311
|
+
expiresAt: expiresAt > 0 ? expiresAt * 1000 : undefined,
|
|
312
|
+
metadata: params.metadata,
|
|
313
|
+
};
|
|
314
|
+
}, [merchant, platform, sdk, config]);
|
|
315
|
+
|
|
316
|
+
// Get checkout URL — uses configurable base URL from config or environment
|
|
317
|
+
const getCheckoutUrl = useCallback((linkId: number): string => {
|
|
318
|
+
const baseUrl = config.checkoutBaseUrl || 'https://rotate.app';
|
|
319
|
+
const params = new URLSearchParams({
|
|
320
|
+
link: linkId.toString(),
|
|
321
|
+
network: config.network || 'mainnet-beta',
|
|
322
|
+
merchant: merchantId.toString(),
|
|
323
|
+
platform: platformId.toString(),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (config.brandName) params.set('brand', config.brandName);
|
|
327
|
+
if (config.brandColor) params.set('color', config.brandColor.replace('#', ''));
|
|
328
|
+
if (config.successUrl) params.set('success', config.successUrl);
|
|
329
|
+
if (config.cancelUrl) params.set('cancel', config.cancelUrl);
|
|
330
|
+
|
|
331
|
+
return `${baseUrl}/checkout/?${params.toString()}`;
|
|
332
|
+
}, [merchantId, platformId, config]);
|
|
333
|
+
|
|
334
|
+
// Context value
|
|
335
|
+
const value: RotateContextType = useMemo(() => ({
|
|
336
|
+
config,
|
|
337
|
+
sdk,
|
|
338
|
+
connected,
|
|
339
|
+
merchant,
|
|
340
|
+
platform,
|
|
341
|
+
createCheckoutSession,
|
|
342
|
+
getCheckoutUrl,
|
|
343
|
+
calculateTotal,
|
|
344
|
+
loading,
|
|
345
|
+
error,
|
|
346
|
+
}), [config, sdk, connected, merchant, platform, createCheckoutSession, getCheckoutUrl, calculateTotal, loading, error]);
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<RotateContext.Provider value={value}>
|
|
350
|
+
{children}
|
|
351
|
+
</RotateContext.Provider>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ==================== HOOK ====================
|
|
356
|
+
|
|
357
|
+
export function useRotateContext(): RotateContextType {
|
|
358
|
+
const context = useContext(RotateContext);
|
|
359
|
+
|
|
360
|
+
if (!context) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
'useRotateContext must be used within a RotateProvider. ' +
|
|
363
|
+
'Wrap your app with <RotateProvider merchantId={...} platformId={...}>'
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return context;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export default RotateProvider;
|