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