@doujins/payments-ui 0.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/dist/index.js ADDED
@@ -0,0 +1,2810 @@
1
+ import { createContext, useMemo, useEffect, useContext, useState, useCallback, useRef } from 'react';
2
+ import { createStore } from 'zustand/vanilla';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { User, MapPin, ChevronDown, Search, Loader2, CreditCard, WalletCards, Trash2, X, Sparkles, CheckCircle, XCircle, RotateCcw, Wallet, RefreshCw } from 'lucide-react';
5
+ import countryList from 'country-list';
6
+ import clsx from 'clsx';
7
+ import * as Dialog2 from '@radix-ui/react-dialog';
8
+ import { useQueryClient, useQuery, useMutation } from '@tanstack/react-query';
9
+ import { useWallet, useConnection } from '@solana/wallet-adapter-react';
10
+ import { Buffer } from 'buffer';
11
+ import { Connection, VersionedTransaction, Transaction, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
12
+ import { getAssociatedTokenAddress, getAccount, TOKEN_PROGRAM_ID } from '@solana/spl-token';
13
+ import QRCode from 'qrcode';
14
+ import { useStore } from 'zustand';
15
+
16
+ // src/context/PaymentContext.tsx
17
+
18
+ // src/utils/collect.ts
19
+ var SCRIPT_SRC = "https://secure.networkmerchants.com/token/Collect.js";
20
+ var loadCollectJs = (tokenizationKey) => {
21
+ if (typeof document === "undefined") return;
22
+ const trimmed = tokenizationKey?.trim();
23
+ if (!trimmed || trimmed.length < 10) {
24
+ console.warn("payments-ui: invalid Collect.js key, skipping load");
25
+ return;
26
+ }
27
+ const existing = document.querySelector(`script[src="${SCRIPT_SRC}"]`);
28
+ if (existing) return;
29
+ const script = document.createElement("script");
30
+ script.src = SCRIPT_SRC;
31
+ script.setAttribute("data-tokenization-key", trimmed);
32
+ script.setAttribute("data-field-ccnumber-placeholder", "0000 0000 0000 0000");
33
+ script.setAttribute("data-field-ccexp-placeholder", "10 / 25");
34
+ script.setAttribute("data-field-cvv-placeholder", "123");
35
+ script.setAttribute("data-variant", "inline");
36
+ script.async = true;
37
+ document.head.appendChild(script);
38
+ };
39
+
40
+ // src/services/apiClient.ts
41
+ var buildUrl = (baseUrl, path, { params, query }) => {
42
+ let resolved = `${baseUrl}${path}`;
43
+ if (params) {
44
+ Object.entries(params).forEach(([key, value]) => {
45
+ resolved = resolved.replace(`:${key}`, encodeURIComponent(String(value)));
46
+ });
47
+ }
48
+ if (query) {
49
+ const queryString = new URLSearchParams();
50
+ Object.entries(query).forEach(([key, value]) => {
51
+ if (value === void 0 || value === null) return;
52
+ queryString.append(key, String(value));
53
+ });
54
+ if ([...queryString.keys()].length > 0) {
55
+ resolved = `${resolved}?${queryString.toString()}`;
56
+ }
57
+ }
58
+ return resolved;
59
+ };
60
+ var createApiClient = (paymentConfig, baseUrl, fetcher, resolveAuthToken) => {
61
+ const request = async (method, path, options) => {
62
+ const url = buildUrl(baseUrl, path, options ?? {});
63
+ const headers = {
64
+ "Content-Type": "application/json",
65
+ ...paymentConfig.defaultHeaders ?? {},
66
+ ...options?.headers ?? {}
67
+ };
68
+ const token = await resolveAuthToken();
69
+ if (token) {
70
+ headers.Authorization = `Bearer ${token}`;
71
+ }
72
+ const response = await fetcher(url, {
73
+ method,
74
+ headers,
75
+ body: options?.body ? JSON.stringify(options.body) : void 0
76
+ });
77
+ if (!response.ok) {
78
+ const message = await response.text();
79
+ throw new Error(message || `Request failed with status ${response.status}`);
80
+ }
81
+ if (response.status === 204) {
82
+ return void 0;
83
+ }
84
+ const data = await response.json();
85
+ return data;
86
+ };
87
+ return {
88
+ request,
89
+ get: (path, options) => request("GET", path, options),
90
+ post: (path, options) => request("POST", path, options),
91
+ put: (path, options) => request("PUT", path, options),
92
+ patch: (path, options) => request("PATCH", path, options),
93
+ delete: (path, options) => request("DELETE", path, options)
94
+ };
95
+ };
96
+
97
+ // src/services/CardPaymentService.ts
98
+ var CardPaymentService = class {
99
+ constructor(config) {
100
+ this.config = config;
101
+ this.collectLoaded = false;
102
+ }
103
+ async ensureCollectLoaded() {
104
+ if (this.collectLoaded) return;
105
+ if (!this.config.collectJsKey) {
106
+ throw new Error("payments-ui: collect.js key missing");
107
+ }
108
+ loadCollectJs(this.config.collectJsKey);
109
+ this.collectLoaded = true;
110
+ }
111
+ buildCreatePayload(result) {
112
+ return {
113
+ payment_token: result.token,
114
+ first_name: result.billing.firstName,
115
+ last_name: result.billing.lastName,
116
+ address1: result.billing.address1,
117
+ address2: result.billing.address2,
118
+ city: result.billing.city,
119
+ state: result.billing.stateRegion,
120
+ zip: result.billing.postalCode,
121
+ country: result.billing.country,
122
+ email: result.billing.email,
123
+ provider: result.billing.provider
124
+ };
125
+ }
126
+ };
127
+
128
+ // src/services/PaymentMethodService.ts
129
+ var PaymentMethodService = class {
130
+ constructor(api) {
131
+ this.api = api;
132
+ }
133
+ async list(params) {
134
+ return this.api.get("/payment-methods", {
135
+ query: {
136
+ page: params?.page ?? 1,
137
+ page_size: params?.pageSize ?? 50
138
+ }
139
+ });
140
+ }
141
+ async create(payload) {
142
+ return this.api.post("/payment-methods", {
143
+ body: { ...payload }
144
+ });
145
+ }
146
+ async remove(id) {
147
+ await this.api.delete(`/payment-methods/${id}`);
148
+ }
149
+ async activate(id) {
150
+ await this.api.put(`/payment-methods/${id}/activate`);
151
+ }
152
+ };
153
+
154
+ // src/services/SolanaPaymentService.ts
155
+ var SolanaPaymentService = class {
156
+ constructor(api) {
157
+ this.api = api;
158
+ }
159
+ async generatePayment(priceId, token, userWallet) {
160
+ return this.api.post("/solana/generate", {
161
+ body: { price_id: priceId, token, user_wallet: userWallet }
162
+ });
163
+ }
164
+ async submitPayment(signedTransaction, priceId, intentId, memo) {
165
+ return this.api.post("/solana/submit", {
166
+ body: {
167
+ signed_transaction: signedTransaction,
168
+ price_id: priceId,
169
+ intent_id: intentId,
170
+ ...memo ? { memo } : {}
171
+ }
172
+ });
173
+ }
174
+ async fetchSupportedTokens() {
175
+ const response = await this.api.get(
176
+ "/solana/tokens"
177
+ );
178
+ return response.tokens;
179
+ }
180
+ async getSupportedTokens() {
181
+ return this.fetchSupportedTokens();
182
+ }
183
+ async generateQrCode(priceId, token, userWallet) {
184
+ return this.api.post("/solana/qr", {
185
+ body: {
186
+ price_id: priceId,
187
+ token,
188
+ ...userWallet ? { user_wallet: userWallet } : {}
189
+ }
190
+ });
191
+ }
192
+ async generateQRCode(priceId, token, userWallet) {
193
+ return this.generateQrCode(priceId, token, userWallet);
194
+ }
195
+ async checkPaymentStatus(reference, memo) {
196
+ return this.api.get("/solana/check", {
197
+ query: {
198
+ reference,
199
+ ...memo ? { memo } : {}
200
+ }
201
+ });
202
+ }
203
+ };
204
+
205
+ // src/services/TokenCatalog.ts
206
+ var TokenCatalog = class {
207
+ constructor(solanaService, options = {}) {
208
+ this.solanaService = solanaService;
209
+ this.options = options;
210
+ this.cache = [];
211
+ this.lastFetched = 0;
212
+ }
213
+ get ttl() {
214
+ return this.options.ttlMs ?? 5 * 60 * 1e3;
215
+ }
216
+ async getTokens(force = false) {
217
+ const isStale = Date.now() - this.lastFetched > this.ttl;
218
+ if (!force && this.cache.length > 0 && !isStale) {
219
+ return this.cache;
220
+ }
221
+ const tokens = await this.solanaService.fetchSupportedTokens();
222
+ this.cache = tokens;
223
+ this.lastFetched = Date.now();
224
+ return tokens;
225
+ }
226
+ getCached() {
227
+ return this.cache;
228
+ }
229
+ };
230
+
231
+ // src/services/WalletGateway.ts
232
+ var WalletGateway = class {
233
+ constructor() {
234
+ this.adapter = null;
235
+ }
236
+ setAdapter(adapter) {
237
+ this.adapter = adapter;
238
+ }
239
+ getPublicKey() {
240
+ if (!this.adapter?.publicKey) return null;
241
+ try {
242
+ return this.adapter.publicKey.toBase58();
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ async sign(transaction) {
248
+ if (!this.adapter) {
249
+ throw new Error("payments-ui: wallet adapter not set");
250
+ }
251
+ if (typeof this.adapter.signVersionedTransaction === "function") {
252
+ return this.adapter.signVersionedTransaction(transaction);
253
+ }
254
+ if (typeof this.adapter.signTransaction === "function") {
255
+ return this.adapter.signTransaction(transaction);
256
+ }
257
+ throw new Error("payments-ui: wallet adapter cannot sign transactions");
258
+ }
259
+ };
260
+
261
+ // src/services/SubscriptionService.ts
262
+ var SubscriptionService = class {
263
+ constructor(api) {
264
+ this.api = api;
265
+ }
266
+ async subscribe(platform, payload) {
267
+ const body = this.serializePayload(platform, payload);
268
+ return this.api.post(
269
+ `/subscriptions/process/${platform}`,
270
+ {
271
+ body
272
+ }
273
+ );
274
+ }
275
+ async generateFlexFormUrl(payload) {
276
+ return this.api.post("/subscriptions/flexform", {
277
+ body: { ...payload }
278
+ });
279
+ }
280
+ serializePayload(platform, payload) {
281
+ if (platform === "nmi") {
282
+ const data2 = payload;
283
+ if (!data2.priceId) {
284
+ throw new Error("payments-ui: priceId is required for NMI subscriptions");
285
+ }
286
+ if (!data2.paymentToken && !data2.paymentMethodId) {
287
+ throw new Error(
288
+ "payments-ui: paymentToken or paymentMethodId is required for NMI subscriptions"
289
+ );
290
+ }
291
+ const body = {
292
+ price_id: data2.priceId,
293
+ processor: data2.processor ?? "nmi",
294
+ provider: data2.provider
295
+ };
296
+ if (data2.email) body.email = data2.email;
297
+ if (data2.firstName) body.first_name = data2.firstName;
298
+ if (data2.lastName) body.last_name = data2.lastName;
299
+ if (data2.address1) body.address1 = data2.address1;
300
+ if (data2.city) body.city = data2.city;
301
+ if (data2.state) body.state = data2.state;
302
+ if (data2.zipCode) body.zip = data2.zipCode;
303
+ if (data2.country) body.country = data2.country;
304
+ if (data2.paymentToken) body.payment_token = data2.paymentToken;
305
+ if (data2.paymentMethodId) body.payment_method_id = data2.paymentMethodId;
306
+ return body;
307
+ }
308
+ const data = payload;
309
+ if (!data.priceId) {
310
+ throw new Error("payments-ui: priceId is required for CCBill subscriptions");
311
+ }
312
+ return {
313
+ price_id: data.priceId,
314
+ processor: data.processor ?? "ccbill",
315
+ email: data.email,
316
+ first_name: data.firstName,
317
+ last_name: data.lastName,
318
+ zip: data.zipCode,
319
+ country: data.country
320
+ };
321
+ }
322
+ };
323
+
324
+ // src/core/PaymentApp.ts
325
+ var PaymentApp = class {
326
+ constructor(options) {
327
+ this.resolveAuthToken = async () => {
328
+ if (!this.config.getAuthToken) {
329
+ return null;
330
+ }
331
+ try {
332
+ const result = this.config.getAuthToken();
333
+ if (result instanceof Promise) {
334
+ return await result ?? null;
335
+ }
336
+ return result ?? null;
337
+ } catch (error) {
338
+ console.warn("payments-ui: failed to resolve auth token", error);
339
+ return null;
340
+ }
341
+ };
342
+ this.config = options.config;
343
+ this.fetcher = options.fetcher ?? options.config.fetcher ?? globalThis.fetch?.bind(globalThis);
344
+ if (!this.fetcher) {
345
+ throw new Error("payments-ui: fetch implementation is required");
346
+ }
347
+ this.services = this.createServices();
348
+ }
349
+ getConfig() {
350
+ return this.config;
351
+ }
352
+ getFetcher() {
353
+ return this.fetcher;
354
+ }
355
+ getServices() {
356
+ return this.services;
357
+ }
358
+ createServices() {
359
+ const billingApi = createApiClient(
360
+ this.config,
361
+ this.config.endpoints.billingBaseUrl,
362
+ this.fetcher,
363
+ this.resolveAuthToken
364
+ );
365
+ const accountBaseUrl = this.config.endpoints.accountBaseUrl ?? this.config.endpoints.billingBaseUrl;
366
+ const accountApi = createApiClient(
367
+ this.config,
368
+ accountBaseUrl,
369
+ this.fetcher,
370
+ this.resolveAuthToken
371
+ );
372
+ const solanaPayments = new SolanaPaymentService(billingApi);
373
+ const paymentMethods = new PaymentMethodService(accountApi);
374
+ const cardPayments = new CardPaymentService(this.config);
375
+ const walletGateway = new WalletGateway();
376
+ const tokenCatalog = new TokenCatalog(solanaPayments);
377
+ const subscriptions = new SubscriptionService(billingApi);
378
+ return {
379
+ cardPayments,
380
+ paymentMethods,
381
+ solanaPayments,
382
+ tokenCatalog,
383
+ walletGateway,
384
+ subscriptions,
385
+ billingApi,
386
+ accountApi
387
+ };
388
+ }
389
+ };
390
+ var initialState = {
391
+ selectedMethodId: null,
392
+ solanaModalOpen: false,
393
+ savedPaymentStatus: "idle",
394
+ savedPaymentError: null,
395
+ newCardStatus: "idle",
396
+ newCardError: null,
397
+ solanaTab: "wallet",
398
+ solanaStatus: "selecting",
399
+ solanaError: null,
400
+ solanaTransactionId: null,
401
+ solanaSelectedToken: null,
402
+ solanaTokenAmount: 0
403
+ };
404
+ var createPaymentStore = (options) => createStore((set, get) => {
405
+ const notifyStatus = (status, context) => {
406
+ options?.callbacks?.onStatusChange?.({ status, context });
407
+ };
408
+ const notifySuccess = (payload) => {
409
+ if (!options?.callbacks?.onSuccess) return;
410
+ options.callbacks.onSuccess(payload ?? {});
411
+ };
412
+ const notifyError = (error) => {
413
+ options?.callbacks?.onError?.(new Error(error));
414
+ };
415
+ return {
416
+ ...initialState,
417
+ setSelectedMethod: (methodId) => set({ selectedMethodId: methodId }),
418
+ setSolanaModalOpen: (open) => set({ solanaModalOpen: open }),
419
+ setSolanaTab: (tab) => set({ solanaTab: tab }),
420
+ setSolanaSelectedToken: (symbol) => set({ solanaSelectedToken: symbol }),
421
+ setSolanaTokenAmount: (amount) => set({ solanaTokenAmount: amount }),
422
+ setSolanaTransactionId: (txId) => set({ solanaTransactionId: txId }),
423
+ startSavedPayment: () => {
424
+ notifyStatus("processing", { source: "saved-payment" });
425
+ set({ savedPaymentStatus: "processing", savedPaymentError: null });
426
+ },
427
+ completeSavedPayment: () => {
428
+ notifyStatus("success", { source: "saved-payment" });
429
+ set({ savedPaymentStatus: "success", savedPaymentError: null });
430
+ },
431
+ failSavedPayment: (error) => {
432
+ notifyStatus("error", { source: "saved-payment" });
433
+ notifyError(error);
434
+ set({ savedPaymentStatus: "error", savedPaymentError: error });
435
+ },
436
+ resetSavedPayment: () => set({ savedPaymentStatus: "idle", savedPaymentError: null }),
437
+ startNewCardPayment: () => {
438
+ notifyStatus("processing", { source: "new-card" });
439
+ set({ newCardStatus: "processing", newCardError: null });
440
+ },
441
+ completeNewCardPayment: () => {
442
+ notifyStatus("success", { source: "new-card" });
443
+ set({ newCardStatus: "success", newCardError: null });
444
+ },
445
+ failNewCardPayment: (error) => {
446
+ notifyStatus("error", { source: "new-card" });
447
+ notifyError(error);
448
+ set({ newCardStatus: "error", newCardError: error });
449
+ },
450
+ resetNewCardPayment: () => set({ newCardStatus: "idle", newCardError: null }),
451
+ startSolanaPayment: () => {
452
+ notifyStatus("processing", { source: "solana" });
453
+ set({ solanaStatus: "processing", solanaError: null });
454
+ },
455
+ confirmSolanaPayment: () => set({ solanaStatus: "confirming" }),
456
+ completeSolanaPayment: (payload) => {
457
+ notifyStatus("success", { source: "solana" });
458
+ notifySuccess(payload);
459
+ set({ solanaStatus: "success", solanaError: null });
460
+ },
461
+ failSolanaPayment: (error) => {
462
+ notifyStatus("error", { source: "solana" });
463
+ notifyError(error);
464
+ set({ solanaStatus: "error", solanaError: error });
465
+ },
466
+ resetSolanaPayment: () => set({
467
+ solanaStatus: "selecting",
468
+ solanaError: null,
469
+ solanaTransactionId: null
470
+ }),
471
+ resetAll: () => set(initialState)
472
+ };
473
+ });
474
+ var PaymentContext = createContext(void 0);
475
+ var PaymentProvider = ({
476
+ config,
477
+ children
478
+ }) => {
479
+ const app = useMemo(() => new PaymentApp({ config }), [config]);
480
+ const store = useMemo(
481
+ () => createPaymentStore({ callbacks: config.callbacks }),
482
+ [config.callbacks]
483
+ );
484
+ const value = useMemo(() => {
485
+ return {
486
+ config: app.getConfig(),
487
+ fetcher: app.getFetcher(),
488
+ resolveAuthToken: app.resolveAuthToken,
489
+ app,
490
+ services: app.getServices(),
491
+ store
492
+ };
493
+ }, [app, store]);
494
+ useEffect(() => {
495
+ if (!value.config.collectJsKey) return;
496
+ loadCollectJs(value.config.collectJsKey);
497
+ }, [value.config.collectJsKey]);
498
+ return /* @__PURE__ */ jsx(PaymentContext.Provider, { value, children });
499
+ };
500
+ var usePaymentContext = () => {
501
+ const context = useContext(PaymentContext);
502
+ if (!context) {
503
+ throw new Error("usePaymentContext must be used within a PaymentProvider");
504
+ }
505
+ return context;
506
+ };
507
+ var customCountries = [
508
+ { code: "TW", name: "Taiwan, Province of China" },
509
+ { code: "KR", name: "Korea" },
510
+ { code: "KP", name: "North Korea" },
511
+ { code: "PS", name: "Palestine, State of" },
512
+ { code: "VA", name: "Holy See (Vatican City State)" },
513
+ { code: "BQ", name: "Bonaire, Sint Eustatius and Saba" },
514
+ { code: "SX", name: "Sint Maarten (Dutch part)" },
515
+ { code: "MK", name: "North Macedonia" },
516
+ { code: "SZ", name: "Eswatini" },
517
+ { code: "FM", name: "Micronesia, Federated States Of" },
518
+ { code: "TR", name: "T\xFCrkiye" }
519
+ ];
520
+ countryList.overwrite(customCountries);
521
+ var countries = countryList.getData().sort((a, b) => a.name.localeCompare(b.name));
522
+
523
+ // src/hooks/useCountryDropdown.ts
524
+ function useCountryDropdown() {
525
+ const [country, setCountry] = useState("");
526
+ const [countryOpen, setCountryOpen] = useState(false);
527
+ const [countrySearch, setCountrySearch] = useState("");
528
+ const countryDropdownRef = useRef(null);
529
+ const filteredCountries = countries.filter(
530
+ (country2) => country2.name.toLowerCase().includes(countrySearch.toLowerCase())
531
+ );
532
+ useEffect(() => {
533
+ function handleClickOutside(event) {
534
+ if (countryDropdownRef.current && !countryDropdownRef.current.contains(event.target)) {
535
+ setCountryOpen(false);
536
+ }
537
+ }
538
+ document.addEventListener("mousedown", handleClickOutside);
539
+ return () => {
540
+ document.removeEventListener("mousedown", handleClickOutside);
541
+ };
542
+ }, []);
543
+ return {
544
+ country,
545
+ setCountry,
546
+ countryOpen,
547
+ setCountryOpen,
548
+ countrySearch,
549
+ setCountrySearch,
550
+ countryDropdownRef,
551
+ filteredCountries
552
+ };
553
+ }
554
+ var defaultBilling = {
555
+ firstName: "",
556
+ lastName: "",
557
+ address1: "",
558
+ address2: "",
559
+ city: "",
560
+ stateRegion: "",
561
+ postalCode: "",
562
+ country: "US",
563
+ email: "",
564
+ provider: "mobius"
565
+ };
566
+ var buildSelector = (prefix, field) => `#${prefix}-${field}`;
567
+ var CardDetailsForm = ({
568
+ visible,
569
+ onTokenize,
570
+ submitLabel,
571
+ submitting = false,
572
+ defaultValues,
573
+ externalError,
574
+ collectPrefix = "card-form",
575
+ className,
576
+ onBillingChange,
577
+ submitDisabled = false
578
+ }) => {
579
+ const { config } = usePaymentContext();
580
+ const defaultValuesKey = useMemo(() => JSON.stringify(defaultValues ?? {}), [defaultValues]);
581
+ const mergedDefaults = useMemo(
582
+ () => ({
583
+ ...defaultBilling,
584
+ ...defaultValues,
585
+ email: defaultValues?.email ?? config.defaultUser?.email ?? defaultBilling.email
586
+ }),
587
+ [defaultValuesKey, config.defaultUser?.email]
588
+ );
589
+ const [firstName, setFirstName] = useState(mergedDefaults.firstName);
590
+ const [lastName, setLastName] = useState(mergedDefaults.lastName);
591
+ const [address1, setAddress1] = useState(mergedDefaults.address1);
592
+ const [address2, setAddress2] = useState(mergedDefaults.address2 ?? "");
593
+ const [city, setCity] = useState(mergedDefaults.city);
594
+ const [stateRegion, setStateRegion] = useState(mergedDefaults.stateRegion ?? "");
595
+ const [postalCode, setPostalCode] = useState(mergedDefaults.postalCode);
596
+ const [country, setCountry] = useState(mergedDefaults.country);
597
+ const [email, setEmail] = useState(mergedDefaults.email ?? "");
598
+ const [localError, setLocalError] = useState(null);
599
+ const [isTokenizing, setIsTokenizing] = useState(false);
600
+ const {
601
+ countryDropdownRef,
602
+ countryOpen,
603
+ setCountryOpen,
604
+ countrySearch,
605
+ setCountrySearch,
606
+ filteredCountries
607
+ } = useCountryDropdown();
608
+ useEffect(() => {
609
+ if (!visible) {
610
+ setLocalError(null);
611
+ setIsTokenizing(false);
612
+ }
613
+ }, [visible]);
614
+ useEffect(() => {
615
+ if (!visible) return;
616
+ setFirstName(mergedDefaults.firstName);
617
+ setLastName(mergedDefaults.lastName);
618
+ setAddress1(mergedDefaults.address1);
619
+ setAddress2(mergedDefaults.address2 ?? "");
620
+ setCity(mergedDefaults.city);
621
+ setStateRegion(mergedDefaults.stateRegion ?? "");
622
+ setPostalCode(mergedDefaults.postalCode);
623
+ setCountry(mergedDefaults.country);
624
+ setEmail(mergedDefaults.email ?? "");
625
+ }, [defaultValuesKey, mergedDefaults, visible]);
626
+ useEffect(() => {
627
+ if (!onBillingChange) return;
628
+ onBillingChange({
629
+ firstName,
630
+ lastName,
631
+ address1,
632
+ address2,
633
+ city,
634
+ stateRegion,
635
+ postalCode,
636
+ country,
637
+ email,
638
+ provider: mergedDefaults.provider ?? "mobius"
639
+ });
640
+ }, [
641
+ firstName,
642
+ lastName,
643
+ address1,
644
+ address2,
645
+ city,
646
+ stateRegion,
647
+ postalCode,
648
+ country,
649
+ email,
650
+ mergedDefaults.provider,
651
+ onBillingChange
652
+ ]);
653
+ useEffect(() => {
654
+ if (typeof window === "undefined" || !window.CollectJS || !visible) {
655
+ return;
656
+ }
657
+ const handlers = window.__doujinsCollectHandlers || (window.__doujinsCollectHandlers = {});
658
+ handlers[collectPrefix] = (response) => {
659
+ setIsTokenizing(false);
660
+ if (!response.token) {
661
+ setLocalError("Payment tokenization failed. Please try again.");
662
+ return;
663
+ }
664
+ const billing = {
665
+ firstName,
666
+ lastName,
667
+ address1,
668
+ address2,
669
+ city,
670
+ stateRegion,
671
+ postalCode,
672
+ country,
673
+ email,
674
+ provider: mergedDefaults.provider ?? "mobius"
675
+ };
676
+ onTokenize(response.token, billing);
677
+ };
678
+ const configured = window.__doujinsCollectConfigured || (window.__doujinsCollectConfigured = {});
679
+ if (!configured[collectPrefix]) {
680
+ window.CollectJS.configure({
681
+ variant: "inline",
682
+ fields: {
683
+ ccnumber: { selector: buildSelector(collectPrefix, "ccnumber") },
684
+ ccexp: { selector: buildSelector(collectPrefix, "ccexp") },
685
+ cvv: { selector: buildSelector(collectPrefix, "cvv") }
686
+ },
687
+ callback: (response) => {
688
+ const fn = window.__doujinsCollectHandlers?.[collectPrefix];
689
+ fn?.(response);
690
+ }
691
+ });
692
+ configured[collectPrefix] = true;
693
+ }
694
+ }, [
695
+ collectPrefix,
696
+ firstName,
697
+ lastName,
698
+ address1,
699
+ address2,
700
+ city,
701
+ stateRegion,
702
+ postalCode,
703
+ country,
704
+ email,
705
+ mergedDefaults.provider,
706
+ onTokenize,
707
+ visible
708
+ ]);
709
+ const validate = () => {
710
+ if (!firstName.trim() || !lastName.trim() || !address1.trim() || !city.trim() || !postalCode.trim() || !country.trim() || !email.trim()) {
711
+ setLocalError("Please complete all required billing fields.");
712
+ return false;
713
+ }
714
+ setLocalError(null);
715
+ return true;
716
+ };
717
+ const handleSubmit = (event) => {
718
+ event.preventDefault();
719
+ if (!validate()) return;
720
+ if (!window.CollectJS) {
721
+ setLocalError("Payment form is not ready. Please try again later.");
722
+ return;
723
+ }
724
+ setIsTokenizing(true);
725
+ window.CollectJS.startPaymentRequest();
726
+ };
727
+ const errorMessage = localError ?? externalError;
728
+ return /* @__PURE__ */ jsxs(
729
+ "form",
730
+ {
731
+ className: clsx("payments-ui-card-form", className),
732
+ onSubmit: handleSubmit,
733
+ noValidate: true,
734
+ children: [
735
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-grid", children: [
736
+ /* @__PURE__ */ jsxs("label", { className: "payments-ui-label", children: [
737
+ /* @__PURE__ */ jsxs("span", { children: [
738
+ /* @__PURE__ */ jsx(User, { className: "payments-ui-icon" }),
739
+ " First name"
740
+ ] }),
741
+ /* @__PURE__ */ jsx(
742
+ "input",
743
+ {
744
+ className: "payments-ui-input",
745
+ value: firstName,
746
+ onChange: (e) => setFirstName(e.target.value),
747
+ required: true
748
+ }
749
+ )
750
+ ] }),
751
+ /* @__PURE__ */ jsxs("label", { className: "payments-ui-label", children: [
752
+ /* @__PURE__ */ jsxs("span", { children: [
753
+ /* @__PURE__ */ jsx(User, { className: "payments-ui-icon" }),
754
+ " Last name"
755
+ ] }),
756
+ /* @__PURE__ */ jsx(
757
+ "input",
758
+ {
759
+ className: "payments-ui-input",
760
+ value: lastName,
761
+ onChange: (e) => setLastName(e.target.value),
762
+ required: true
763
+ }
764
+ )
765
+ ] })
766
+ ] }),
767
+ /* @__PURE__ */ jsxs("label", { className: "payments-ui-label", children: [
768
+ /* @__PURE__ */ jsx("span", { children: "Email" }),
769
+ /* @__PURE__ */ jsx(
770
+ "input",
771
+ {
772
+ type: "email",
773
+ className: "payments-ui-input",
774
+ value: email,
775
+ onChange: (e) => setEmail(e.target.value),
776
+ required: true
777
+ }
778
+ )
779
+ ] }),
780
+ /* @__PURE__ */ jsxs("label", { className: "payments-ui-label", children: [
781
+ /* @__PURE__ */ jsx("span", { children: "Address line 1" }),
782
+ /* @__PURE__ */ jsx(
783
+ "input",
784
+ {
785
+ className: "payments-ui-input",
786
+ value: address1,
787
+ onChange: (e) => setAddress1(e.target.value),
788
+ required: true
789
+ }
790
+ )
791
+ ] }),
792
+ /* @__PURE__ */ jsxs("label", { className: "payments-ui-label", children: [
793
+ /* @__PURE__ */ jsx("span", { children: "Address line 2 (optional)" }),
794
+ /* @__PURE__ */ jsx(
795
+ "input",
796
+ {
797
+ className: "payments-ui-input",
798
+ value: address2,
799
+ onChange: (e) => setAddress2(e.target.value)
800
+ }
801
+ )
802
+ ] }),
803
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-grid", children: [
804
+ /* @__PURE__ */ jsxs("label", { className: "payments-ui-label", children: [
805
+ /* @__PURE__ */ jsx("span", { children: "City" }),
806
+ /* @__PURE__ */ jsx(
807
+ "input",
808
+ {
809
+ className: "payments-ui-input",
810
+ value: city,
811
+ onChange: (e) => setCity(e.target.value),
812
+ required: true
813
+ }
814
+ )
815
+ ] }),
816
+ /* @__PURE__ */ jsxs("label", { className: "payments-ui-label", children: [
817
+ /* @__PURE__ */ jsx("span", { children: "State / Region" }),
818
+ /* @__PURE__ */ jsx(
819
+ "input",
820
+ {
821
+ className: "payments-ui-input",
822
+ value: stateRegion,
823
+ onChange: (e) => setStateRegion(e.target.value)
824
+ }
825
+ )
826
+ ] })
827
+ ] }),
828
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-grid", children: [
829
+ /* @__PURE__ */ jsxs("label", { className: "payments-ui-label", children: [
830
+ /* @__PURE__ */ jsxs("span", { children: [
831
+ /* @__PURE__ */ jsx(MapPin, { className: "payments-ui-icon" }),
832
+ " Postal code"
833
+ ] }),
834
+ /* @__PURE__ */ jsx(
835
+ "input",
836
+ {
837
+ className: "payments-ui-input",
838
+ value: postalCode,
839
+ onChange: (e) => setPostalCode(e.target.value),
840
+ required: true
841
+ }
842
+ )
843
+ ] }),
844
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-label", children: [
845
+ /* @__PURE__ */ jsx("span", { children: "Country" }),
846
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-country", ref: countryDropdownRef, children: [
847
+ /* @__PURE__ */ jsxs(
848
+ "button",
849
+ {
850
+ type: "button",
851
+ className: "payments-ui-country-toggle",
852
+ onClick: () => setCountryOpen((prev) => !prev),
853
+ children: [
854
+ /* @__PURE__ */ jsx("span", { children: country }),
855
+ /* @__PURE__ */ jsx(ChevronDown, { className: "payments-ui-icon" })
856
+ ]
857
+ }
858
+ ),
859
+ countryOpen && /* @__PURE__ */ jsxs("div", { className: "payments-ui-country-menu", children: [
860
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-country-search", children: [
861
+ /* @__PURE__ */ jsx(Search, { className: "payments-ui-icon" }),
862
+ /* @__PURE__ */ jsx(
863
+ "input",
864
+ {
865
+ placeholder: "Search country",
866
+ value: countrySearch,
867
+ onChange: (e) => setCountrySearch(e.target.value)
868
+ }
869
+ )
870
+ ] }),
871
+ /* @__PURE__ */ jsx("ul", { children: filteredCountries.map((option) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
872
+ "button",
873
+ {
874
+ type: "button",
875
+ onClick: () => {
876
+ setCountry(option.code);
877
+ setCountryOpen(false);
878
+ },
879
+ children: option.name
880
+ }
881
+ ) }, option.code)) })
882
+ ] })
883
+ ] })
884
+ ] })
885
+ ] }),
886
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-label", children: [
887
+ /* @__PURE__ */ jsx("span", { children: "Card number" }),
888
+ /* @__PURE__ */ jsx(
889
+ "div",
890
+ {
891
+ id: buildSelector(collectPrefix, "ccnumber").slice(1),
892
+ className: "payments-ui-collect-field"
893
+ }
894
+ )
895
+ ] }),
896
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-grid", children: [
897
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-label", children: [
898
+ /* @__PURE__ */ jsx("span", { children: "Expiry" }),
899
+ /* @__PURE__ */ jsx(
900
+ "div",
901
+ {
902
+ id: buildSelector(collectPrefix, "ccexp").slice(1),
903
+ className: "payments-ui-collect-field"
904
+ }
905
+ )
906
+ ] }),
907
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-label", children: [
908
+ /* @__PURE__ */ jsx("span", { children: "CVV" }),
909
+ /* @__PURE__ */ jsx(
910
+ "div",
911
+ {
912
+ id: buildSelector(collectPrefix, "cvv").slice(1),
913
+ className: "payments-ui-collect-field"
914
+ }
915
+ )
916
+ ] })
917
+ ] }),
918
+ errorMessage && /* @__PURE__ */ jsx("p", { className: "payments-ui-error", children: errorMessage }),
919
+ /* @__PURE__ */ jsxs(
920
+ "button",
921
+ {
922
+ type: "submit",
923
+ className: "payments-ui-button",
924
+ disabled: submitting || isTokenizing || submitDisabled,
925
+ children: [
926
+ (submitting || isTokenizing) && /* @__PURE__ */ jsx(Loader2, { className: "payments-ui-spinner" }),
927
+ /* @__PURE__ */ jsx(CreditCard, { className: "payments-ui-icon" }),
928
+ submitting || isTokenizing ? "Processing..." : submitLabel
929
+ ]
930
+ }
931
+ )
932
+ ]
933
+ }
934
+ );
935
+ };
936
+ var usePaymentMethodService = () => {
937
+ const { services } = usePaymentContext();
938
+ return useMemo(() => services.paymentMethods, [services]);
939
+ };
940
+
941
+ // src/hooks/usePaymentMethods.ts
942
+ var PAYMENT_METHODS_KEY = ["payments-ui", "payment-methods"];
943
+ var usePaymentMethods = () => {
944
+ const service = usePaymentMethodService();
945
+ const queryClient = useQueryClient();
946
+ const listQuery = useQuery({
947
+ queryKey: PAYMENT_METHODS_KEY,
948
+ queryFn: () => service.list({ pageSize: 50 })
949
+ });
950
+ const createMutation = useMutation({
951
+ mutationFn: ({ token, billing }) => {
952
+ const payload = {
953
+ payment_token: token,
954
+ first_name: billing.firstName,
955
+ last_name: billing.lastName,
956
+ address1: billing.address1,
957
+ address2: billing.address2,
958
+ city: billing.city,
959
+ state: billing.stateRegion,
960
+ zip: billing.postalCode,
961
+ country: billing.country,
962
+ email: billing.email,
963
+ provider: billing.provider
964
+ };
965
+ return service.create(payload);
966
+ },
967
+ onSuccess: () => {
968
+ void queryClient.invalidateQueries({ queryKey: PAYMENT_METHODS_KEY });
969
+ }
970
+ });
971
+ const deleteMutation = useMutation({
972
+ mutationFn: ({ id }) => service.remove(id),
973
+ onSuccess: () => {
974
+ void queryClient.invalidateQueries({ queryKey: PAYMENT_METHODS_KEY });
975
+ }
976
+ });
977
+ return {
978
+ listQuery,
979
+ createMutation,
980
+ deleteMutation
981
+ };
982
+ };
983
+ var formatCardLabel = (method) => {
984
+ const brand = method.card_type ? method.card_type.toUpperCase() : "CARD";
985
+ const lastFour = method.last_four ? `\u2022\u2022\u2022\u2022 ${method.last_four}` : "";
986
+ return `${brand} ${lastFour}`.trim();
987
+ };
988
+ var StoredPaymentMethods = ({
989
+ selectedMethodId,
990
+ onMethodSelect,
991
+ showAddButton = true,
992
+ heading = "Payment Methods",
993
+ description = "Manage your saved cards"
994
+ }) => {
995
+ const { listQuery, createMutation, deleteMutation } = usePaymentMethods();
996
+ const [isModalOpen, setIsModalOpen] = useState(false);
997
+ const [deletingId, setDeletingId] = useState(null);
998
+ const payments = useMemo(() => listQuery.data?.data ?? [], [listQuery.data]);
999
+ const handleCardTokenize = (token, billing) => {
1000
+ createMutation.mutate({ token, billing });
1001
+ };
1002
+ const handleDelete = (method) => {
1003
+ setDeletingId(method.id);
1004
+ deleteMutation.mutate(
1005
+ { id: method.id },
1006
+ {
1007
+ onSettled: () => setDeletingId(null)
1008
+ }
1009
+ );
1010
+ };
1011
+ return /* @__PURE__ */ jsxs("div", { className: "payments-ui-panel", children: [
1012
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-panel-header", children: [
1013
+ /* @__PURE__ */ jsxs("div", { children: [
1014
+ /* @__PURE__ */ jsxs("p", { className: "payments-ui-panel-title", children: [
1015
+ /* @__PURE__ */ jsx(WalletCards, { className: "payments-ui-icon" }),
1016
+ " ",
1017
+ heading
1018
+ ] }),
1019
+ /* @__PURE__ */ jsx("p", { className: "payments-ui-panel-description", children: description })
1020
+ ] }),
1021
+ showAddButton && /* @__PURE__ */ jsxs(
1022
+ "button",
1023
+ {
1024
+ className: "payments-ui-button",
1025
+ type: "button",
1026
+ onClick: () => setIsModalOpen(true),
1027
+ children: [
1028
+ /* @__PURE__ */ jsx(CreditCard, { className: "payments-ui-icon" }),
1029
+ " Add card"
1030
+ ]
1031
+ }
1032
+ )
1033
+ ] }),
1034
+ /* @__PURE__ */ jsx("div", { className: "payments-ui-panel-body", children: listQuery.isLoading ? /* @__PURE__ */ jsxs("div", { className: "payments-ui-empty", children: [
1035
+ /* @__PURE__ */ jsx(Loader2, { className: "payments-ui-spinner" }),
1036
+ " Loading cards..."
1037
+ ] }) : payments.length === 0 ? /* @__PURE__ */ jsx("div", { className: "payments-ui-empty", children: "No saved payment methods yet." }) : /* @__PURE__ */ jsx("div", { className: "payments-ui-method-list", children: payments.map((method) => {
1038
+ const isSelected = selectedMethodId === method.id;
1039
+ return /* @__PURE__ */ jsxs(
1040
+ "div",
1041
+ {
1042
+ className: clsx("payments-ui-method-item", {
1043
+ "is-selected": isSelected
1044
+ }),
1045
+ children: [
1046
+ /* @__PURE__ */ jsxs("div", { children: [
1047
+ /* @__PURE__ */ jsx("p", { className: "payments-ui-method-label", children: formatCardLabel(method) }),
1048
+ /* @__PURE__ */ jsxs("p", { className: "payments-ui-method-meta", children: [
1049
+ "Added on",
1050
+ " ",
1051
+ method.created_at ? new Date(method.created_at).toLocaleDateString() : "unknown"
1052
+ ] })
1053
+ ] }),
1054
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-method-actions", children: [
1055
+ onMethodSelect && /* @__PURE__ */ jsx(
1056
+ "button",
1057
+ {
1058
+ type: "button",
1059
+ className: "payments-ui-text-button",
1060
+ onClick: () => onMethodSelect(method),
1061
+ children: isSelected ? "Selected" : "Use card"
1062
+ }
1063
+ ),
1064
+ /* @__PURE__ */ jsx(
1065
+ "button",
1066
+ {
1067
+ type: "button",
1068
+ className: "payments-ui-icon-button payments-ui-danger",
1069
+ onClick: () => handleDelete(method),
1070
+ disabled: deletingId === method.id && deleteMutation.isPending,
1071
+ children: deletingId === method.id && deleteMutation.isPending ? /* @__PURE__ */ jsx(Loader2, { className: "payments-ui-spinner" }) : /* @__PURE__ */ jsx(Trash2, { className: "payments-ui-icon" })
1072
+ }
1073
+ )
1074
+ ] })
1075
+ ]
1076
+ },
1077
+ method.id
1078
+ );
1079
+ }) }) }),
1080
+ /* @__PURE__ */ jsx(Dialog2.Root, { open: isModalOpen, onOpenChange: setIsModalOpen, children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
1081
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "payments-ui-modal-overlay" }),
1082
+ /* @__PURE__ */ jsxs(Dialog2.Content, { className: "payments-ui-modal", children: [
1083
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-modal-header", children: [
1084
+ /* @__PURE__ */ jsxs("div", { children: [
1085
+ /* @__PURE__ */ jsx("h3", { children: "Add a new card" }),
1086
+ /* @__PURE__ */ jsx("p", { children: "Your card details are tokenized securely via our payment provider." })
1087
+ ] }),
1088
+ /* @__PURE__ */ jsx(Dialog2.Close, { className: "payments-ui-icon-button", children: /* @__PURE__ */ jsx(X, { className: "payments-ui-icon" }) })
1089
+ ] }),
1090
+ /* @__PURE__ */ jsx(
1091
+ CardDetailsForm,
1092
+ {
1093
+ visible: isModalOpen,
1094
+ collectPrefix: "payments-ui-card",
1095
+ submitting: createMutation.isPending,
1096
+ submitLabel: "Save card",
1097
+ externalError: createMutation.error?.message ?? null,
1098
+ onTokenize: handleCardTokenize
1099
+ }
1100
+ )
1101
+ ] })
1102
+ ] }) })
1103
+ ] });
1104
+ };
1105
+ var useSolanaService = () => {
1106
+ const { services } = usePaymentContext();
1107
+ return useMemo(() => services.solanaPayments, [services]);
1108
+ };
1109
+ var getSolBalance = async (connection, publicKey) => {
1110
+ try {
1111
+ const lamports = await connection.getBalance(publicKey);
1112
+ return lamports / LAMPORTS_PER_SOL;
1113
+ } catch (error) {
1114
+ console.error("Failed to fetch SOL balance:", error);
1115
+ return 0;
1116
+ }
1117
+ };
1118
+ var fetchAllTokenBalances = async (connection, publicKey) => {
1119
+ const balances = /* @__PURE__ */ new Map();
1120
+ try {
1121
+ const tokenAccounts = await connection.getParsedProgramAccounts(
1122
+ TOKEN_PROGRAM_ID,
1123
+ {
1124
+ filters: [
1125
+ {
1126
+ dataSize: 165
1127
+ // Size of token account
1128
+ },
1129
+ {
1130
+ memcmp: {
1131
+ offset: 32,
1132
+ // Owner field offset
1133
+ bytes: publicKey.toString()
1134
+ }
1135
+ }
1136
+ ]
1137
+ }
1138
+ );
1139
+ for (const account of tokenAccounts) {
1140
+ const data = account.account.data;
1141
+ const parsed = data.parsed;
1142
+ const info = parsed?.info;
1143
+ if (info && info.tokenAmount) {
1144
+ const mintAddress = info.mint;
1145
+ const uiAmount = info.tokenAmount.uiAmount || 0;
1146
+ if (uiAmount > 0) {
1147
+ balances.set(mintAddress, uiAmount);
1148
+ }
1149
+ }
1150
+ }
1151
+ } catch (error) {
1152
+ console.error("Failed to fetch token balances:", error);
1153
+ }
1154
+ return balances;
1155
+ };
1156
+ var fetchSupportedTokenBalances = async (connection, publicKey, supportedTokens) => {
1157
+ const results = [];
1158
+ const solBalance = await getSolBalance(connection, publicKey);
1159
+ const solTokenMeta = supportedTokens.find((token) => token.is_native || token.symbol === "SOL") || {
1160
+ symbol: "SOL",
1161
+ name: "Solana",
1162
+ mint: "So11111111111111111111111111111111111111112",
1163
+ decimals: 9};
1164
+ results.push({
1165
+ mint: solTokenMeta.mint,
1166
+ symbol: solTokenMeta.symbol,
1167
+ name: solTokenMeta.name,
1168
+ balance: solBalance,
1169
+ uiBalance: solBalance.toFixed(solTokenMeta.decimals <= 4 ? solTokenMeta.decimals : 4),
1170
+ decimals: solTokenMeta.decimals,
1171
+ isNative: true
1172
+ });
1173
+ const tokenBalances = await fetchAllTokenBalances(connection, publicKey);
1174
+ const tokenMetaByMint = new Map(
1175
+ supportedTokens.filter((token) => !(token.is_native || token.symbol === "SOL")).map((token) => [token.mint, token])
1176
+ );
1177
+ for (const [mint, tokenMeta] of tokenMetaByMint.entries()) {
1178
+ const balance = tokenBalances.get(mint) || 0;
1179
+ results.push({
1180
+ mint,
1181
+ symbol: tokenMeta.symbol,
1182
+ name: tokenMeta.name,
1183
+ balance,
1184
+ uiBalance: balance.toFixed(tokenMeta.decimals <= 6 ? tokenMeta.decimals : 6),
1185
+ decimals: tokenMeta.decimals,
1186
+ isNative: false
1187
+ });
1188
+ }
1189
+ return results;
1190
+ };
1191
+ var hasSufficientBalance = (balance, requiredAmount, buffer = 0.05) => {
1192
+ const requiredWithBuffer = requiredAmount * (1 + buffer);
1193
+ return balance >= requiredWithBuffer;
1194
+ };
1195
+
1196
+ // src/hooks/useSolanaDirectPayment.ts
1197
+ var useSolanaDirectPayment = (options) => {
1198
+ const { priceId, tokenAmount, selectedToken, supportedTokens, onStart, onConfirming, onSuccess, onError } = options;
1199
+ const { connected, publicKey, wallet, signTransaction } = useWallet();
1200
+ const { config } = usePaymentContext();
1201
+ const solanaService = useSolanaService();
1202
+ const [tokenBalance, setTokenBalance] = useState(null);
1203
+ const [isBalanceLoading, setIsBalanceLoading] = useState(false);
1204
+ const [isProcessing, setIsProcessing] = useState(false);
1205
+ const connection = useMemo(() => {
1206
+ const rpc = config.solanaRpcUrl ?? "https://api.mainnet-beta.solana.com";
1207
+ return new Connection(rpc);
1208
+ }, [config.solanaRpcUrl]);
1209
+ const fetchTokenBalance = useCallback(async () => {
1210
+ if (!connected || !publicKey || !selectedToken) {
1211
+ setTokenBalance({ balance: 0, hasBalance: false });
1212
+ return;
1213
+ }
1214
+ try {
1215
+ setIsBalanceLoading(true);
1216
+ const balances = await fetchSupportedTokenBalances(
1217
+ connection,
1218
+ publicKey,
1219
+ supportedTokens
1220
+ );
1221
+ const balanceInfo = balances.find(
1222
+ (tb) => tb.symbol === selectedToken.symbol || tb.mint === selectedToken.mint
1223
+ );
1224
+ const balance = balanceInfo?.balance || 0;
1225
+ const hasBalanceFlag = hasSufficientBalance(balance, tokenAmount);
1226
+ setTokenBalance({ balance, hasBalance: hasBalanceFlag });
1227
+ } catch (error) {
1228
+ console.error("Failed to fetch token balance:", error);
1229
+ setTokenBalance({ balance: 0, hasBalance: false });
1230
+ } finally {
1231
+ setIsBalanceLoading(false);
1232
+ }
1233
+ }, [connected, publicKey, connection, selectedToken, tokenAmount, supportedTokens]);
1234
+ useEffect(() => {
1235
+ if (connected && publicKey && selectedToken) {
1236
+ void fetchTokenBalance();
1237
+ }
1238
+ }, [connected, publicKey, selectedToken, tokenAmount, fetchTokenBalance]);
1239
+ const decodeTransaction = useCallback((serialized) => {
1240
+ const buffer = Buffer.from(serialized, "base64");
1241
+ try {
1242
+ return VersionedTransaction.deserialize(buffer);
1243
+ } catch (err) {
1244
+ try {
1245
+ return Transaction.from(buffer);
1246
+ } catch (legacyErr) {
1247
+ console.error("Failed to deserialize transaction", legacyErr);
1248
+ throw new Error("Invalid transaction payload received from server");
1249
+ }
1250
+ }
1251
+ }, []);
1252
+ const isVersionedTransaction = (tx) => {
1253
+ return !!tx && typeof tx === "object" && "version" in tx;
1254
+ };
1255
+ const signWithWallet = useCallback(
1256
+ async (tx) => {
1257
+ if (!wallet) {
1258
+ throw new Error("Wallet adapter is not available");
1259
+ }
1260
+ const adapter = wallet.adapter;
1261
+ if (isVersionedTransaction(tx)) {
1262
+ if (adapter.supportedTransactionVersions) {
1263
+ const supported = adapter.supportedTransactionVersions;
1264
+ if (!supported.has(tx.version)) {
1265
+ throw new Error("Connected wallet does not support this transaction version");
1266
+ }
1267
+ }
1268
+ if (adapter.signVersionedTransaction) {
1269
+ return adapter.signVersionedTransaction(tx);
1270
+ }
1271
+ }
1272
+ if (adapter.signTransaction) {
1273
+ return adapter.signTransaction(tx);
1274
+ }
1275
+ if (signTransaction) {
1276
+ return signTransaction(tx);
1277
+ }
1278
+ throw new Error("Connected wallet cannot sign transactions");
1279
+ },
1280
+ [wallet, signTransaction]
1281
+ );
1282
+ const pay = useCallback(async () => {
1283
+ if (!connected || !publicKey) {
1284
+ onError("Wallet not connected");
1285
+ return;
1286
+ }
1287
+ if (!selectedToken) {
1288
+ onError("No payment token selected");
1289
+ return;
1290
+ }
1291
+ if (!tokenBalance?.hasBalance) {
1292
+ onError("Insufficient balance for this token");
1293
+ return;
1294
+ }
1295
+ try {
1296
+ setIsProcessing(true);
1297
+ onStart();
1298
+ const paymentData = await solanaService.generatePayment(
1299
+ priceId,
1300
+ selectedToken.symbol,
1301
+ publicKey.toBase58()
1302
+ );
1303
+ const transactionToSign = decodeTransaction(paymentData.transaction);
1304
+ const signedTx = await signWithWallet(transactionToSign);
1305
+ const signedSerialized = Buffer.from(signedTx.serialize()).toString("base64");
1306
+ onConfirming();
1307
+ const result = await solanaService.submitPayment(
1308
+ signedSerialized,
1309
+ priceId,
1310
+ paymentData.intent_id,
1311
+ `Payment for subscription - ${selectedToken.symbol}`
1312
+ );
1313
+ onSuccess(result, result.transaction_id);
1314
+ } catch (err) {
1315
+ console.error("Payment failed:", err);
1316
+ let errorMessage = "Payment failed. Please try again.";
1317
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : "";
1318
+ if (message.includes("User rejected")) {
1319
+ errorMessage = "Payment cancelled by user";
1320
+ } else if (/insufficient\s+funds/i.test(message)) {
1321
+ errorMessage = "Insufficient balance for this token";
1322
+ } else if (message) {
1323
+ errorMessage = message;
1324
+ }
1325
+ onError(errorMessage);
1326
+ } finally {
1327
+ setIsProcessing(false);
1328
+ }
1329
+ }, [
1330
+ connected,
1331
+ publicKey,
1332
+ selectedToken,
1333
+ tokenBalance?.hasBalance,
1334
+ onError,
1335
+ onStart,
1336
+ solanaService,
1337
+ priceId,
1338
+ decodeTransaction,
1339
+ signWithWallet,
1340
+ onConfirming,
1341
+ onSuccess
1342
+ ]);
1343
+ const balanceLabel = tokenBalance ? `${tokenBalance.balance.toFixed(4)} ${selectedToken?.symbol ?? ""}` : "--";
1344
+ return {
1345
+ isBalanceLoading,
1346
+ isProcessing,
1347
+ balanceLabel,
1348
+ canPay: Boolean(
1349
+ connected && tokenBalance?.hasBalance && !isProcessing
1350
+ ),
1351
+ pay
1352
+ };
1353
+ };
1354
+ var DirectPayment = ({
1355
+ priceId,
1356
+ tokenAmount,
1357
+ selectedToken,
1358
+ supportedTokens,
1359
+ onPaymentStart,
1360
+ onPaymentConfirming,
1361
+ onPaymentSuccess,
1362
+ onPaymentError
1363
+ }) => {
1364
+ const { isBalanceLoading, balanceLabel, canPay, isProcessing, pay } = useSolanaDirectPayment({
1365
+ priceId,
1366
+ tokenAmount,
1367
+ selectedToken,
1368
+ supportedTokens,
1369
+ onStart: onPaymentStart,
1370
+ onConfirming: onPaymentConfirming,
1371
+ onSuccess: onPaymentSuccess,
1372
+ onError: onPaymentError
1373
+ });
1374
+ return /* @__PURE__ */ jsxs("div", { className: "payments-ui-panel", children: [
1375
+ /* @__PURE__ */ jsx("div", { className: "payments-ui-panel-header", children: /* @__PURE__ */ jsxs("div", { children: [
1376
+ /* @__PURE__ */ jsxs("p", { className: "payments-ui-panel-title", children: [
1377
+ /* @__PURE__ */ jsx(Wallet, { className: "payments-ui-icon" }),
1378
+ " Pay with connected wallet"
1379
+ ] }),
1380
+ /* @__PURE__ */ jsx("p", { className: "payments-ui-panel-description", children: "Sign the transaction directly in your Solana wallet." })
1381
+ ] }) }),
1382
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-panel-body", children: [
1383
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-balance-row", children: [
1384
+ /* @__PURE__ */ jsx("span", { children: "Available balance" }),
1385
+ isBalanceLoading ? /* @__PURE__ */ jsx(Loader2, { className: "payments-ui-spinner" }) : /* @__PURE__ */ jsx("strong", { children: balanceLabel })
1386
+ ] }),
1387
+ /* @__PURE__ */ jsxs(
1388
+ "button",
1389
+ {
1390
+ type: "button",
1391
+ className: "payments-ui-button",
1392
+ disabled: !canPay,
1393
+ onClick: pay,
1394
+ children: [
1395
+ isProcessing ? /* @__PURE__ */ jsx(Loader2, { className: "payments-ui-spinner" }) : /* @__PURE__ */ jsx(Wallet, { className: "payments-ui-icon" }),
1396
+ isProcessing ? "Processing..." : "Pay with wallet"
1397
+ ]
1398
+ }
1399
+ )
1400
+ ] })
1401
+ ] });
1402
+ };
1403
+ var useSolanaQrPayment = (options) => {
1404
+ const { priceId, selectedToken, onSuccess, onError } = options;
1405
+ const solanaService = useSolanaService();
1406
+ const [intent, setIntent] = useState(null);
1407
+ const [qrDataUri, setQrDataUri] = useState(null);
1408
+ const [isLoading, setIsLoading] = useState(false);
1409
+ const [error, setError] = useState(null);
1410
+ const [timeRemaining, setTimeRemaining] = useState(0);
1411
+ const pollRef = useRef(null);
1412
+ const countdownRef = useRef(null);
1413
+ const clearTimers = useCallback(() => {
1414
+ if (pollRef.current) {
1415
+ clearInterval(pollRef.current);
1416
+ pollRef.current = null;
1417
+ }
1418
+ if (countdownRef.current) {
1419
+ clearInterval(countdownRef.current);
1420
+ countdownRef.current = null;
1421
+ }
1422
+ }, []);
1423
+ useEffect(() => {
1424
+ return () => {
1425
+ clearTimers();
1426
+ };
1427
+ }, [clearTimers]);
1428
+ const handleError = useCallback(
1429
+ (message, notifyParent = false) => {
1430
+ clearTimers();
1431
+ setError(message);
1432
+ setIntent(null);
1433
+ setQrDataUri(null);
1434
+ setTimeRemaining(0);
1435
+ if (notifyParent) {
1436
+ onError(message);
1437
+ }
1438
+ },
1439
+ [clearTimers, onError]
1440
+ );
1441
+ const handleSuccess = useCallback(
1442
+ (status) => {
1443
+ clearTimers();
1444
+ setTimeRemaining(0);
1445
+ setIntent(null);
1446
+ onSuccess(status.payment_id, status.transaction || "");
1447
+ },
1448
+ [clearTimers, onSuccess]
1449
+ );
1450
+ const pollStatus = useCallback(
1451
+ async (reference) => {
1452
+ try {
1453
+ const status = await solanaService.checkPaymentStatus(reference);
1454
+ if (status.status === "confirmed") {
1455
+ handleSuccess(status);
1456
+ }
1457
+ if (status.status === "failed") {
1458
+ handleError(
1459
+ status.error_message || "Payment failed. Please try again.",
1460
+ true
1461
+ );
1462
+ }
1463
+ } catch (err) {
1464
+ console.error("Failed to poll Solana Pay status:", err);
1465
+ }
1466
+ },
1467
+ [handleError, handleSuccess, solanaService]
1468
+ );
1469
+ const startCountdown = useCallback(
1470
+ (expiresAt, reference) => {
1471
+ const updateTime = () => {
1472
+ const remaining = Math.max(0, Math.floor(expiresAt - Date.now() / 1e3));
1473
+ setTimeRemaining(remaining);
1474
+ if (remaining === 0) {
1475
+ handleError("Payment intent expired. Please generate a new QR code.");
1476
+ }
1477
+ };
1478
+ updateTime();
1479
+ countdownRef.current = setInterval(updateTime, 1e3);
1480
+ pollRef.current = setInterval(() => void pollStatus(reference), 4e3);
1481
+ },
1482
+ [handleError, pollStatus]
1483
+ );
1484
+ const renderQr = useCallback(async (qrIntent) => {
1485
+ try {
1486
+ const dataUri = await QRCode.toDataURL(qrIntent.url, {
1487
+ width: 320,
1488
+ margin: 1,
1489
+ color: {
1490
+ dark: "#0f1116",
1491
+ light: "#ffffff"
1492
+ }
1493
+ });
1494
+ setQrDataUri(dataUri);
1495
+ } catch (err) {
1496
+ console.error("Failed to render QR code:", err);
1497
+ handleError("Unable to render QR code");
1498
+ }
1499
+ }, [handleError]);
1500
+ const fetchIntent = useCallback(async () => {
1501
+ if (!selectedToken) {
1502
+ setIntent(null);
1503
+ setQrDataUri(null);
1504
+ setError(null);
1505
+ clearTimers();
1506
+ setTimeRemaining(0);
1507
+ return;
1508
+ }
1509
+ try {
1510
+ setIsLoading(true);
1511
+ setError(null);
1512
+ clearTimers();
1513
+ const nextIntent = await solanaService.generateQRCode(
1514
+ priceId,
1515
+ selectedToken.symbol
1516
+ );
1517
+ setIntent(nextIntent);
1518
+ setTimeRemaining(
1519
+ Math.max(0, Math.floor(nextIntent.expires_at - Date.now() / 1e3))
1520
+ );
1521
+ await renderQr(nextIntent);
1522
+ startCountdown(nextIntent.expires_at, nextIntent.reference);
1523
+ pollStatus(nextIntent.reference);
1524
+ } catch (err) {
1525
+ console.error("Failed to generate Solana Pay QR intent:", err);
1526
+ const message = err instanceof Error ? err.message : "Unable to create Solana Pay QR code";
1527
+ handleError(message);
1528
+ } finally {
1529
+ setIsLoading(false);
1530
+ }
1531
+ }, [
1532
+ clearTimers,
1533
+ handleError,
1534
+ pollStatus,
1535
+ priceId,
1536
+ selectedToken,
1537
+ solanaService,
1538
+ startCountdown,
1539
+ renderQr
1540
+ ]);
1541
+ useEffect(() => {
1542
+ void fetchIntent();
1543
+ }, [fetchIntent]);
1544
+ return {
1545
+ intent,
1546
+ qrDataUri,
1547
+ isLoading,
1548
+ error,
1549
+ timeRemaining,
1550
+ refresh: fetchIntent
1551
+ };
1552
+ };
1553
+ var QRCodePayment = ({
1554
+ priceId,
1555
+ selectedToken,
1556
+ onPaymentError,
1557
+ onPaymentSuccess
1558
+ }) => {
1559
+ const { intent, qrDataUri, isLoading, error, timeRemaining, refresh } = useSolanaQrPayment({
1560
+ priceId,
1561
+ selectedToken,
1562
+ onError: onPaymentError,
1563
+ onSuccess: onPaymentSuccess
1564
+ });
1565
+ if (!selectedToken) {
1566
+ return /* @__PURE__ */ jsx("div", { className: "payments-ui-empty", children: "Select a token to continue." });
1567
+ }
1568
+ return /* @__PURE__ */ jsxs("div", { className: "payments-ui-panel", children: [
1569
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-panel-header", children: [
1570
+ /* @__PURE__ */ jsxs("div", { children: [
1571
+ /* @__PURE__ */ jsx("p", { className: "payments-ui-panel-title", children: "Scan with Solana Pay" }),
1572
+ /* @__PURE__ */ jsx("p", { className: "payments-ui-panel-description", children: "Use any Solana Pay compatible wallet to scan and confirm." })
1573
+ ] }),
1574
+ /* @__PURE__ */ jsx(
1575
+ "button",
1576
+ {
1577
+ type: "button",
1578
+ className: "payments-ui-icon-button",
1579
+ onClick: () => refresh(),
1580
+ disabled: isLoading,
1581
+ children: isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "payments-ui-spinner" }) : /* @__PURE__ */ jsx(RefreshCw, { className: "payments-ui-icon" })
1582
+ }
1583
+ )
1584
+ ] }),
1585
+ error && /* @__PURE__ */ jsx("div", { className: "payments-ui-error", children: error }),
1586
+ /* @__PURE__ */ jsx("div", { className: "payments-ui-qr-wrapper", children: qrDataUri ? /* @__PURE__ */ jsx("img", { src: qrDataUri, alt: "Solana Pay QR" }) : /* @__PURE__ */ jsx("div", { className: "payments-ui-empty", children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
1587
+ /* @__PURE__ */ jsx(Loader2, { className: "payments-ui-spinner" }),
1588
+ "Generating QR code..."
1589
+ ] }) : "QR code unavailable" }) }),
1590
+ intent && /* @__PURE__ */ jsxs("div", { className: "payments-ui-countdown", children: [
1591
+ "Expires in ",
1592
+ timeRemaining,
1593
+ "s \xB7 ",
1594
+ intent.token_amount,
1595
+ " ",
1596
+ intent.token_symbol
1597
+ ] })
1598
+ ] });
1599
+ };
1600
+ var PaymentStatus = ({
1601
+ state,
1602
+ usdAmount,
1603
+ solAmount,
1604
+ errorMessage,
1605
+ transactionId,
1606
+ onRetry,
1607
+ onClose
1608
+ }) => {
1609
+ if (state === "success") {
1610
+ return /* @__PURE__ */ jsxs("div", { className: "payments-ui-status success", children: [
1611
+ /* @__PURE__ */ jsx(CheckCircle, { className: "payments-ui-status-icon" }),
1612
+ /* @__PURE__ */ jsx("h3", { children: "Payment confirmed" }),
1613
+ /* @__PURE__ */ jsxs("p", { children: [
1614
+ usdAmount.toFixed(2),
1615
+ " USD (~",
1616
+ solAmount.toFixed(4),
1617
+ " SOL)."
1618
+ ] }),
1619
+ transactionId && /* @__PURE__ */ jsxs("p", { className: "payments-ui-status-meta", children: [
1620
+ "Txn: ",
1621
+ transactionId
1622
+ ] }),
1623
+ /* @__PURE__ */ jsx("button", { className: "payments-ui-button", type: "button", onClick: onClose, children: "Close" })
1624
+ ] });
1625
+ }
1626
+ if (state === "error") {
1627
+ return /* @__PURE__ */ jsxs("div", { className: "payments-ui-status error", children: [
1628
+ /* @__PURE__ */ jsx(XCircle, { className: "payments-ui-status-icon" }),
1629
+ /* @__PURE__ */ jsx("h3", { children: "Payment failed" }),
1630
+ /* @__PURE__ */ jsx("p", { children: errorMessage ?? "Please try again." }),
1631
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-status-actions", children: [
1632
+ /* @__PURE__ */ jsxs("button", { className: "payments-ui-button", type: "button", onClick: onRetry, children: [
1633
+ /* @__PURE__ */ jsx(RotateCcw, { className: "payments-ui-icon" }),
1634
+ " Retry"
1635
+ ] }),
1636
+ /* @__PURE__ */ jsx("button", { className: "payments-ui-text-button", type: "button", onClick: onClose, children: "Cancel" })
1637
+ ] })
1638
+ ] });
1639
+ }
1640
+ return /* @__PURE__ */ jsxs("div", { className: "payments-ui-status pending", children: [
1641
+ /* @__PURE__ */ jsx(Loader2, { className: "payments-ui-spinner" }),
1642
+ /* @__PURE__ */ jsx("h3", { children: state === "confirming" ? "Waiting for confirmations" : "Processing payment" }),
1643
+ /* @__PURE__ */ jsxs("p", { children: [
1644
+ "Paying ",
1645
+ usdAmount.toFixed(2),
1646
+ " USD (~",
1647
+ solAmount.toFixed(4),
1648
+ " SOL)."
1649
+ ] }),
1650
+ /* @__PURE__ */ jsx("p", { className: "payments-ui-status-meta", children: "This can take up to 60 seconds on Solana mainnet." })
1651
+ ] });
1652
+ };
1653
+ var useSupportedTokens = () => {
1654
+ const solanaService = useSolanaService();
1655
+ const [tokens, setTokens] = useState([]);
1656
+ const [isLoading, setIsLoading] = useState(false);
1657
+ const [error, setError] = useState(null);
1658
+ const [lastFetched, setLastFetched] = useState(null);
1659
+ const CACHE_DURATION = 5 * 60 * 1e3;
1660
+ const tokensRef = useRef(tokens);
1661
+ const lastFetchedRef = useRef(lastFetched);
1662
+ tokensRef.current = tokens;
1663
+ lastFetchedRef.current = lastFetched;
1664
+ const fetchSupportedTokens = useCallback(async () => {
1665
+ if (tokensRef.current.length > 0 && lastFetchedRef.current && Date.now() - lastFetchedRef.current < CACHE_DURATION) {
1666
+ return tokensRef.current;
1667
+ }
1668
+ setIsLoading(true);
1669
+ setError(null);
1670
+ try {
1671
+ const tokens2 = await solanaService.getSupportedTokens();
1672
+ const sortedTokens = [...tokens2].sort(
1673
+ (a, b) => a.symbol.localeCompare(b.symbol)
1674
+ );
1675
+ setTokens(sortedTokens);
1676
+ setLastFetched(Date.now());
1677
+ return sortedTokens;
1678
+ } catch (error2) {
1679
+ const errorMessage = error2 instanceof Error ? error2.message : "Failed to fetch supported tokens";
1680
+ setError(errorMessage);
1681
+ console.error("Failed to fetch supported tokens:", error2);
1682
+ return [];
1683
+ } finally {
1684
+ setIsLoading(false);
1685
+ }
1686
+ }, [solanaService]);
1687
+ useEffect(() => {
1688
+ if (tokens.length === 0) {
1689
+ fetchSupportedTokens();
1690
+ }
1691
+ }, [tokens.length, fetchSupportedTokens]);
1692
+ const getTokenBySymbol = useCallback(
1693
+ (symbol) => {
1694
+ return tokens.find((token) => token.symbol === symbol);
1695
+ },
1696
+ [tokens]
1697
+ );
1698
+ const getTokenByMint = useCallback(
1699
+ (mintAddress) => {
1700
+ return tokens.find((token) => token.mint === mintAddress);
1701
+ },
1702
+ [tokens]
1703
+ );
1704
+ const isTokenSupported = useCallback(
1705
+ (symbol) => {
1706
+ return tokens.some((token) => token.symbol === symbol);
1707
+ },
1708
+ [tokens]
1709
+ );
1710
+ const getUSDCToken = useCallback(() => {
1711
+ return getTokenBySymbol("USDC");
1712
+ }, [getTokenBySymbol]);
1713
+ const getPYUSDToken = useCallback(() => {
1714
+ return getTokenBySymbol("PYUSD");
1715
+ }, [getTokenBySymbol]);
1716
+ const getSOLToken = useCallback(() => {
1717
+ return getTokenBySymbol("SOL");
1718
+ }, [getTokenBySymbol]);
1719
+ const getStablecoins = useCallback(() => {
1720
+ return tokens.filter((token) => ["USDC", "PYUSD"].includes(token.symbol));
1721
+ }, [tokens]);
1722
+ const refreshTokens = useCallback(async () => {
1723
+ setLastFetched(null);
1724
+ return await fetchSupportedTokens();
1725
+ }, [fetchSupportedTokens]);
1726
+ const isCacheStale = useCallback(() => {
1727
+ if (!lastFetched) return true;
1728
+ return Date.now() - lastFetched > CACHE_DURATION;
1729
+ }, [lastFetched]);
1730
+ const getTokenDisplayInfo = useCallback((token) => {
1731
+ return {
1732
+ ...token,
1733
+ displayName: `${token.name} (${token.symbol})`,
1734
+ shortAddress: `${token.mint.slice(0, 4)}...${token.mint.slice(-4)}`,
1735
+ decimalPlaces: token.decimals
1736
+ };
1737
+ }, []);
1738
+ const getTokenPrice = useCallback(
1739
+ (symbol) => {
1740
+ const token = getTokenBySymbol(symbol);
1741
+ if (!token) return 0;
1742
+ const price = token.price ?? 0;
1743
+ return typeof price === "number" ? price : Number(price);
1744
+ },
1745
+ [getTokenBySymbol]
1746
+ );
1747
+ const calculateTokenAmount = useCallback(
1748
+ (usdAmount, tokenSymbol) => {
1749
+ const token = getTokenBySymbol(tokenSymbol);
1750
+ const price = getTokenPrice(tokenSymbol);
1751
+ if (!token || price === 0) return "0";
1752
+ const tokenAmount = usdAmount / price;
1753
+ const tokenAmountInSmallestUnit = tokenAmount * Math.pow(10, token.decimals);
1754
+ return Math.floor(tokenAmountInSmallestUnit).toString();
1755
+ },
1756
+ [getTokenBySymbol, getTokenPrice]
1757
+ );
1758
+ const formatTokenAmount = useCallback(
1759
+ (amount, tokenSymbol) => {
1760
+ const token = getTokenBySymbol(tokenSymbol);
1761
+ if (!token) return "0";
1762
+ const numericAmount = parseFloat(amount);
1763
+ const displayAmount = numericAmount / Math.pow(10, token.decimals);
1764
+ return displayAmount.toFixed(token.decimals <= 6 ? token.decimals : 6);
1765
+ },
1766
+ [getTokenBySymbol]
1767
+ );
1768
+ return {
1769
+ tokens,
1770
+ isLoading,
1771
+ error,
1772
+ lastFetched,
1773
+ fetchSupportedTokens,
1774
+ refreshTokens,
1775
+ getTokenBySymbol,
1776
+ getTokenByMint,
1777
+ isTokenSupported,
1778
+ getUSDCToken,
1779
+ getPYUSDToken,
1780
+ getSOLToken,
1781
+ getStablecoins,
1782
+ getTokenDisplayInfo,
1783
+ getTokenPrice,
1784
+ calculateTokenAmount,
1785
+ formatTokenAmount,
1786
+ isCacheStale,
1787
+ hasTokens: tokens.length > 0,
1788
+ tokenCount: tokens.length
1789
+ };
1790
+ };
1791
+ var usePaymentStore = (selector) => {
1792
+ const { store } = usePaymentContext();
1793
+ return useStore(store, selector);
1794
+ };
1795
+
1796
+ // src/state/selectors.ts
1797
+ var selectCheckoutFlow = (state) => ({
1798
+ selectedMethodId: state.selectedMethodId,
1799
+ savedStatus: state.savedPaymentStatus,
1800
+ savedError: state.savedPaymentError,
1801
+ newCardStatus: state.newCardStatus,
1802
+ newCardError: state.newCardError,
1803
+ solanaModalOpen: state.solanaModalOpen,
1804
+ setSelectedMethod: state.setSelectedMethod,
1805
+ setSolanaModalOpen: state.setSolanaModalOpen,
1806
+ startSavedPayment: state.startSavedPayment,
1807
+ completeSavedPayment: state.completeSavedPayment,
1808
+ failSavedPayment: state.failSavedPayment,
1809
+ startNewCardPayment: state.startNewCardPayment,
1810
+ completeNewCardPayment: state.completeNewCardPayment,
1811
+ failNewCardPayment: state.failNewCardPayment,
1812
+ resetSavedPayment: state.resetSavedPayment
1813
+ });
1814
+ var selectSolanaFlow = (state) => ({
1815
+ tab: state.solanaTab,
1816
+ status: state.solanaStatus,
1817
+ error: state.solanaError,
1818
+ transactionId: state.solanaTransactionId,
1819
+ tokenAmount: state.solanaTokenAmount,
1820
+ selectedTokenSymbol: state.solanaSelectedToken,
1821
+ setTab: state.setSolanaTab,
1822
+ setTokenAmount: state.setSolanaTokenAmount,
1823
+ setTransactionId: state.setSolanaTransactionId,
1824
+ setSelectedTokenSymbol: state.setSolanaSelectedToken,
1825
+ startSolanaPayment: state.startSolanaPayment,
1826
+ confirmSolanaPayment: state.confirmSolanaPayment,
1827
+ completeSolanaPayment: state.completeSolanaPayment,
1828
+ failSolanaPayment: state.failSolanaPayment,
1829
+ resetSolanaPayment: state.resetSolanaPayment
1830
+ });
1831
+ var SolanaPaymentSelector = ({
1832
+ isOpen,
1833
+ onClose,
1834
+ priceId,
1835
+ usdAmount,
1836
+ onSuccess,
1837
+ onError
1838
+ }) => {
1839
+ const { connected } = useWallet();
1840
+ const {
1841
+ tab: activeTab,
1842
+ status: paymentState,
1843
+ error: errorMessage,
1844
+ transactionId,
1845
+ tokenAmount,
1846
+ selectedTokenSymbol,
1847
+ setTab,
1848
+ setTokenAmount,
1849
+ setTransactionId,
1850
+ setSelectedTokenSymbol,
1851
+ startSolanaPayment,
1852
+ confirmSolanaPayment,
1853
+ completeSolanaPayment,
1854
+ failSolanaPayment,
1855
+ resetSolanaPayment
1856
+ } = usePaymentStore(selectSolanaFlow);
1857
+ const {
1858
+ tokens,
1859
+ isLoading: tokensLoading,
1860
+ error: tokensError
1861
+ } = useSupportedTokens();
1862
+ const selectedToken = useMemo(() => {
1863
+ if (!tokens.length) return null;
1864
+ const explicit = tokens.find((token) => token.symbol === selectedTokenSymbol);
1865
+ return explicit || tokens[0];
1866
+ }, [tokens, selectedTokenSymbol]);
1867
+ useEffect(() => {
1868
+ if (!selectedTokenSymbol && tokens.length) {
1869
+ const defaultToken = tokens.find((token) => token.symbol === "SOL") || tokens[0];
1870
+ setSelectedTokenSymbol(defaultToken.symbol);
1871
+ }
1872
+ }, [tokens, selectedTokenSymbol, setSelectedTokenSymbol]);
1873
+ const handlePaymentStart = useCallback(() => {
1874
+ startSolanaPayment();
1875
+ }, [startSolanaPayment]);
1876
+ const handlePaymentConfirming = useCallback(() => {
1877
+ confirmSolanaPayment();
1878
+ }, [confirmSolanaPayment]);
1879
+ const handlePaymentSuccess = useCallback(
1880
+ (result, txId) => {
1881
+ const resolvedTx = txId || (typeof result === "string" ? result : result.transaction_id);
1882
+ setTransactionId(resolvedTx);
1883
+ completeSolanaPayment(
1884
+ typeof result === "string" ? {
1885
+ transactionId: resolvedTx,
1886
+ processor: "solana",
1887
+ metadata: { source: "solana-pay" }
1888
+ } : {
1889
+ transactionId: result.transaction_id,
1890
+ intentId: result.intent_id,
1891
+ processor: "solana",
1892
+ metadata: {
1893
+ purchaseId: result.purchase_id,
1894
+ amount: result.amount,
1895
+ currency: result.currency
1896
+ }
1897
+ }
1898
+ );
1899
+ setTimeout(() => {
1900
+ onSuccess(result);
1901
+ }, 1500);
1902
+ },
1903
+ [completeSolanaPayment, onSuccess, setTransactionId]
1904
+ );
1905
+ const handlePaymentError = useCallback(
1906
+ (error) => {
1907
+ failSolanaPayment(error);
1908
+ onError?.(error);
1909
+ },
1910
+ [failSolanaPayment, onError]
1911
+ );
1912
+ const handleRetry = useCallback(() => {
1913
+ resetSolanaPayment();
1914
+ setTransactionId(null);
1915
+ }, [resetSolanaPayment, setTransactionId]);
1916
+ const handleClose = useCallback(() => {
1917
+ if (paymentState === "processing" || paymentState === "confirming") {
1918
+ return;
1919
+ }
1920
+ resetSolanaPayment();
1921
+ setTransactionId(null);
1922
+ onClose();
1923
+ }, [paymentState, resetSolanaPayment, setTransactionId, onClose]);
1924
+ useEffect(() => {
1925
+ if (!isOpen || !selectedToken || usdAmount === 0) {
1926
+ setTokenAmount(0);
1927
+ return;
1928
+ }
1929
+ const price = selectedToken.price ?? 0;
1930
+ if (!price || price <= 0) {
1931
+ setTokenAmount(0);
1932
+ return;
1933
+ }
1934
+ setTokenAmount(usdAmount / price);
1935
+ }, [isOpen, usdAmount, selectedToken, setTokenAmount]);
1936
+ const handleTokenChange = useCallback(
1937
+ (event) => {
1938
+ setSelectedTokenSymbol(event.target.value);
1939
+ },
1940
+ [setSelectedTokenSymbol]
1941
+ );
1942
+ const wasConnectedRef = useRef(connected);
1943
+ useEffect(() => {
1944
+ if (connected && !wasConnectedRef.current) {
1945
+ setTab("wallet");
1946
+ }
1947
+ if (!connected && wasConnectedRef.current) {
1948
+ setTab("qr");
1949
+ }
1950
+ wasConnectedRef.current = connected;
1951
+ }, [connected, setTab]);
1952
+ const renderSelector = () => {
1953
+ if (tokensLoading) {
1954
+ return /* @__PURE__ */ jsxs("div", { className: "payments-ui-empty", children: [
1955
+ /* @__PURE__ */ jsx(Loader2, { className: "payments-ui-spinner" }),
1956
+ " Loading tokens\u2026"
1957
+ ] });
1958
+ }
1959
+ if (tokensError) {
1960
+ return /* @__PURE__ */ jsx("div", { className: "payments-ui-error", children: tokensError });
1961
+ }
1962
+ if (!tokens.length) {
1963
+ return /* @__PURE__ */ jsx("div", { className: "payments-ui-empty", children: "No payment tokens available." });
1964
+ }
1965
+ return /* @__PURE__ */ jsxs("div", { className: "payments-ui-token-select", children: [
1966
+ /* @__PURE__ */ jsxs("label", { children: [
1967
+ "Payment token",
1968
+ /* @__PURE__ */ jsx("select", { value: selectedTokenSymbol ?? "", onChange: handleTokenChange, children: tokens.map((token) => /* @__PURE__ */ jsxs("option", { value: token.symbol, children: [
1969
+ token.name,
1970
+ " (",
1971
+ token.symbol,
1972
+ ")"
1973
+ ] }, token.symbol)) })
1974
+ ] }),
1975
+ /* @__PURE__ */ jsxs("p", { className: "payments-ui-token-meta", children: [
1976
+ "\u2248 ",
1977
+ tokenAmount.toFixed(4),
1978
+ " ",
1979
+ selectedToken?.symbol
1980
+ ] })
1981
+ ] });
1982
+ };
1983
+ if (paymentState !== "selecting") {
1984
+ return /* @__PURE__ */ jsx(Dialog2.Root, { open: isOpen, onOpenChange: handleClose, children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
1985
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "payments-ui-modal-overlay" }),
1986
+ /* @__PURE__ */ jsxs(Dialog2.Content, { className: "payments-ui-modal", children: [
1987
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-modal-header", children: [
1988
+ /* @__PURE__ */ jsxs("div", { children: [
1989
+ /* @__PURE__ */ jsx("h3", { children: "Complete your payment" }),
1990
+ /* @__PURE__ */ jsx("p", { children: "Follow the prompts below to finish." })
1991
+ ] }),
1992
+ /* @__PURE__ */ jsx(
1993
+ Dialog2.Close,
1994
+ {
1995
+ className: "payments-ui-icon-button",
1996
+ disabled: paymentState === "processing" || paymentState === "confirming",
1997
+ children: /* @__PURE__ */ jsx(X, { className: "payments-ui-icon" })
1998
+ }
1999
+ )
2000
+ ] }),
2001
+ /* @__PURE__ */ jsx(
2002
+ PaymentStatus,
2003
+ {
2004
+ state: paymentState,
2005
+ usdAmount,
2006
+ solAmount: tokenAmount,
2007
+ onRetry: handleRetry,
2008
+ onClose: handleClose,
2009
+ errorMessage,
2010
+ transactionId
2011
+ }
2012
+ )
2013
+ ] })
2014
+ ] }) });
2015
+ }
2016
+ return /* @__PURE__ */ jsx(Dialog2.Root, { open: isOpen, onOpenChange: handleClose, children: /* @__PURE__ */ jsxs(Dialog2.Portal, { children: [
2017
+ /* @__PURE__ */ jsx(Dialog2.Overlay, { className: "payments-ui-modal-overlay" }),
2018
+ /* @__PURE__ */ jsxs(Dialog2.Content, { className: "payments-ui-modal", children: [
2019
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-modal-header", children: [
2020
+ /* @__PURE__ */ jsxs("div", { children: [
2021
+ /* @__PURE__ */ jsx("h3", { children: "Complete your payment" }),
2022
+ /* @__PURE__ */ jsx("p", { children: "Select a token and preferred method." })
2023
+ ] }),
2024
+ /* @__PURE__ */ jsx(Dialog2.Close, { className: "payments-ui-icon-button", children: /* @__PURE__ */ jsx(X, { className: "payments-ui-icon" }) })
2025
+ ] }),
2026
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-tab-header", children: [
2027
+ /* @__PURE__ */ jsx(
2028
+ "button",
2029
+ {
2030
+ type: "button",
2031
+ className: activeTab === "wallet" ? "active" : "",
2032
+ onClick: () => setTab("wallet"),
2033
+ disabled: !connected,
2034
+ children: "Pay with wallet"
2035
+ }
2036
+ ),
2037
+ /* @__PURE__ */ jsx(
2038
+ "button",
2039
+ {
2040
+ type: "button",
2041
+ className: activeTab === "qr" ? "active" : "",
2042
+ onClick: () => setTab("qr"),
2043
+ children: "Scan QR"
2044
+ }
2045
+ )
2046
+ ] }),
2047
+ renderSelector(),
2048
+ activeTab === "wallet" ? /* @__PURE__ */ jsx(
2049
+ DirectPayment,
2050
+ {
2051
+ priceId,
2052
+ tokenAmount,
2053
+ selectedToken,
2054
+ supportedTokens: tokens,
2055
+ onPaymentStart: handlePaymentStart,
2056
+ onPaymentConfirming: handlePaymentConfirming,
2057
+ onPaymentSuccess: handlePaymentSuccess,
2058
+ onPaymentError: handlePaymentError
2059
+ }
2060
+ ) : /* @__PURE__ */ jsx(
2061
+ QRCodePayment,
2062
+ {
2063
+ priceId,
2064
+ selectedToken,
2065
+ onPaymentError: handlePaymentError,
2066
+ onPaymentSuccess: handlePaymentSuccess
2067
+ }
2068
+ )
2069
+ ] })
2070
+ ] }) });
2071
+ };
2072
+ var PaymentExperience = ({
2073
+ priceId,
2074
+ usdAmount,
2075
+ onNewCardPayment,
2076
+ onSavedMethodPayment,
2077
+ enableNewCard = true,
2078
+ enableStoredMethods = true,
2079
+ enableSolanaPay = true,
2080
+ checkoutSummary,
2081
+ onSolanaSuccess,
2082
+ onSolanaError
2083
+ }) => {
2084
+ const showNewCard = enableNewCard && Boolean(onNewCardPayment);
2085
+ const showStored = enableStoredMethods;
2086
+ const {
2087
+ selectedMethodId,
2088
+ savedStatus,
2089
+ savedError,
2090
+ newCardStatus,
2091
+ newCardError,
2092
+ solanaModalOpen,
2093
+ setSelectedMethod,
2094
+ setSolanaModalOpen,
2095
+ startSavedPayment,
2096
+ completeSavedPayment,
2097
+ failSavedPayment,
2098
+ startNewCardPayment,
2099
+ completeNewCardPayment,
2100
+ failNewCardPayment,
2101
+ resetSavedPayment
2102
+ } = usePaymentStore(selectCheckoutFlow);
2103
+ const handleMethodSelect = (method) => {
2104
+ setSelectedMethod(method.id);
2105
+ resetSavedPayment();
2106
+ };
2107
+ const handleNewCardTokenize = async (token, billing) => {
2108
+ if (!onNewCardPayment) return;
2109
+ try {
2110
+ startNewCardPayment();
2111
+ await onNewCardPayment({ token, billing });
2112
+ completeNewCardPayment();
2113
+ } catch (error) {
2114
+ const message = error instanceof Error ? error.message : "Unable to complete payment";
2115
+ failNewCardPayment(message);
2116
+ }
2117
+ };
2118
+ const handleSavedPayment = async () => {
2119
+ if (!onSavedMethodPayment || !selectedMethodId) return;
2120
+ try {
2121
+ startSavedPayment();
2122
+ await onSavedMethodPayment({
2123
+ paymentMethodId: selectedMethodId,
2124
+ amount: usdAmount
2125
+ });
2126
+ completeSavedPayment();
2127
+ } catch (error) {
2128
+ const message = error instanceof Error ? error.message : "Unable to complete payment with saved card";
2129
+ failSavedPayment(message);
2130
+ }
2131
+ };
2132
+ return /* @__PURE__ */ jsxs("div", { className: "payments-ui-experience", children: [
2133
+ /* @__PURE__ */ jsxs("header", { className: "payments-ui-experience-header", children: [
2134
+ /* @__PURE__ */ jsxs("div", { children: [
2135
+ /* @__PURE__ */ jsxs("h2", { children: [
2136
+ /* @__PURE__ */ jsx(CreditCard, { className: "payments-ui-icon" }),
2137
+ " Secure checkout"
2138
+ ] }),
2139
+ /* @__PURE__ */ jsxs("p", { children: [
2140
+ "Amount due: $",
2141
+ usdAmount.toFixed(2)
2142
+ ] })
2143
+ ] }),
2144
+ checkoutSummary && /* @__PURE__ */ jsx("div", { className: "payments-ui-summary", children: checkoutSummary })
2145
+ ] }),
2146
+ /* @__PURE__ */ jsxs("div", { className: "payments-ui-experience-grid", children: [
2147
+ showStored && /* @__PURE__ */ jsxs("div", { className: "payments-ui-column", children: [
2148
+ /* @__PURE__ */ jsx(
2149
+ StoredPaymentMethods,
2150
+ {
2151
+ selectedMethodId,
2152
+ onMethodSelect: handleMethodSelect,
2153
+ heading: "Saved cards",
2154
+ description: "Use or manage your saved payment methods."
2155
+ }
2156
+ ),
2157
+ onSavedMethodPayment && /* @__PURE__ */ jsx(
2158
+ "button",
2159
+ {
2160
+ type: "button",
2161
+ className: "payments-ui-button",
2162
+ disabled: !selectedMethodId || savedStatus === "processing",
2163
+ onClick: handleSavedPayment,
2164
+ children: savedStatus === "processing" ? "Processing\u2026" : "Pay with selected card"
2165
+ }
2166
+ ),
2167
+ savedError && /* @__PURE__ */ jsx("p", { className: "payments-ui-error", children: savedError })
2168
+ ] }),
2169
+ showNewCard && /* @__PURE__ */ jsx("div", { className: "payments-ui-column", children: /* @__PURE__ */ jsxs("div", { className: "payments-ui-panel", children: [
2170
+ /* @__PURE__ */ jsx("div", { className: "payments-ui-panel-header", children: /* @__PURE__ */ jsxs("div", { children: [
2171
+ /* @__PURE__ */ jsxs("p", { className: "payments-ui-panel-title", children: [
2172
+ /* @__PURE__ */ jsx(CreditCard, { className: "payments-ui-icon" }),
2173
+ " Pay with a new card"
2174
+ ] }),
2175
+ /* @__PURE__ */ jsx("p", { className: "payments-ui-panel-description", children: "Card details are tokenized via Collect.js and never hit your server." })
2176
+ ] }) }),
2177
+ /* @__PURE__ */ jsx(
2178
+ CardDetailsForm,
2179
+ {
2180
+ visible: true,
2181
+ submitLabel: "Pay now",
2182
+ submitting: newCardStatus === "processing",
2183
+ externalError: newCardError,
2184
+ onTokenize: handleNewCardTokenize
2185
+ }
2186
+ )
2187
+ ] }) })
2188
+ ] }),
2189
+ enableSolanaPay && /* @__PURE__ */ jsxs("div", { className: "payments-ui-solana-banner", children: [
2190
+ /* @__PURE__ */ jsxs("div", { children: [
2191
+ /* @__PURE__ */ jsxs("h3", { children: [
2192
+ /* @__PURE__ */ jsx(Sparkles, { className: "payments-ui-icon" }),
2193
+ " Prefer Solana Pay?"
2194
+ ] }),
2195
+ /* @__PURE__ */ jsx("p", { children: "Use a Solana wallet or QR code for instant settlement." })
2196
+ ] }),
2197
+ /* @__PURE__ */ jsx(
2198
+ "button",
2199
+ {
2200
+ type: "button",
2201
+ className: "payments-ui-button",
2202
+ onClick: () => setSolanaModalOpen(true),
2203
+ children: "Open Solana Pay"
2204
+ }
2205
+ )
2206
+ ] }),
2207
+ enableSolanaPay && /* @__PURE__ */ jsx(
2208
+ SolanaPaymentSelector,
2209
+ {
2210
+ isOpen: solanaModalOpen,
2211
+ onClose: () => setSolanaModalOpen(false),
2212
+ priceId,
2213
+ usdAmount,
2214
+ onSuccess: (result) => {
2215
+ setSolanaModalOpen(false);
2216
+ onSolanaSuccess?.(result);
2217
+ },
2218
+ onError: onSolanaError
2219
+ }
2220
+ )
2221
+ ] });
2222
+ };
2223
+ var useTokenBalance = (tokens) => {
2224
+ const { publicKey } = useWallet();
2225
+ const { connection } = useConnection();
2226
+ const [balances, setBalances] = useState([]);
2227
+ const [isLoading, setIsLoading] = useState(false);
2228
+ const [error, setError] = useState(null);
2229
+ const fetchTokenBalance = useCallback(
2230
+ async (token, walletAddress) => {
2231
+ try {
2232
+ const mintPublicKey = new PublicKey(token.mint);
2233
+ const associatedTokenAddress = await getAssociatedTokenAddress(
2234
+ mintPublicKey,
2235
+ walletAddress
2236
+ );
2237
+ try {
2238
+ const tokenAccount = await getAccount(
2239
+ connection,
2240
+ associatedTokenAddress
2241
+ );
2242
+ const balance = Number(tokenAccount.amount);
2243
+ const uiAmount = balance / Math.pow(10, token.decimals);
2244
+ return {
2245
+ token,
2246
+ balance,
2247
+ uiAmount,
2248
+ hasBalance: balance > 0
2249
+ };
2250
+ } catch (accountError) {
2251
+ return {
2252
+ token,
2253
+ balance: 0,
2254
+ uiAmount: 0,
2255
+ hasBalance: false
2256
+ };
2257
+ }
2258
+ } catch (error2) {
2259
+ console.error(`Failed to fetch balance for ${token.symbol}:`, error2);
2260
+ return {
2261
+ token,
2262
+ balance: 0,
2263
+ uiAmount: 0,
2264
+ hasBalance: false
2265
+ };
2266
+ }
2267
+ },
2268
+ [connection]
2269
+ );
2270
+ const tokensKey = useMemo(() => tokens.map((t) => t.mint).join(","), [tokens]);
2271
+ useEffect(() => {
2272
+ if (!publicKey || tokens.length === 0) {
2273
+ setBalances([]);
2274
+ return;
2275
+ }
2276
+ setIsLoading(true);
2277
+ setError(null);
2278
+ const fetchAllBalances = async () => {
2279
+ try {
2280
+ const balancePromises = tokens.map(
2281
+ (token) => fetchTokenBalance(token, publicKey)
2282
+ );
2283
+ const tokenBalances = await Promise.all(balancePromises);
2284
+ setBalances(tokenBalances);
2285
+ } catch (error2) {
2286
+ const errorMessage = error2 instanceof Error ? error2.message : "Failed to fetch token balances";
2287
+ setError(errorMessage);
2288
+ console.error("Failed to fetch token balances:", error2);
2289
+ } finally {
2290
+ setIsLoading(false);
2291
+ }
2292
+ };
2293
+ fetchAllBalances();
2294
+ }, [publicKey, tokensKey, fetchTokenBalance]);
2295
+ const getTokenBalance = useCallback(
2296
+ (tokenSymbol) => {
2297
+ return balances.find((balance) => balance.token.symbol === tokenSymbol);
2298
+ },
2299
+ [balances]
2300
+ );
2301
+ const hasSufficientBalance2 = useCallback(
2302
+ (tokenSymbol, requiredAmount) => {
2303
+ const balance = getTokenBalance(tokenSymbol);
2304
+ return balance ? balance.uiAmount >= requiredAmount : false;
2305
+ },
2306
+ [getTokenBalance]
2307
+ );
2308
+ const getFormattedBalance = useCallback(
2309
+ (tokenSymbol) => {
2310
+ const balance = getTokenBalance(tokenSymbol);
2311
+ if (!balance) return "0.00";
2312
+ if (balance.uiAmount < 0.01) {
2313
+ return balance.uiAmount.toFixed(6);
2314
+ } else if (balance.uiAmount < 1) {
2315
+ return balance.uiAmount.toFixed(4);
2316
+ } else {
2317
+ return balance.uiAmount.toFixed(2);
2318
+ }
2319
+ },
2320
+ [getTokenBalance]
2321
+ );
2322
+ const refreshBalances = useCallback(async () => {
2323
+ if (!publicKey || tokens.length === 0) {
2324
+ setBalances([]);
2325
+ return;
2326
+ }
2327
+ setIsLoading(true);
2328
+ setError(null);
2329
+ try {
2330
+ const balancePromises = tokens.map(
2331
+ (token) => fetchTokenBalance(token, publicKey)
2332
+ );
2333
+ const tokenBalances = await Promise.all(balancePromises);
2334
+ setBalances(tokenBalances);
2335
+ } catch (error2) {
2336
+ const errorMessage = error2 instanceof Error ? error2.message : "Failed to fetch token balances";
2337
+ setError(errorMessage);
2338
+ console.error("Failed to fetch token balances:", error2);
2339
+ } finally {
2340
+ setIsLoading(false);
2341
+ }
2342
+ }, [publicKey, tokens, fetchTokenBalance]);
2343
+ const getTotalValue = useCallback(
2344
+ (priceData) => {
2345
+ if (!priceData) return 0;
2346
+ return balances.reduce((total, balance) => {
2347
+ const price = priceData[balance.token.symbol] || 0;
2348
+ return total + balance.uiAmount * price;
2349
+ }, 0);
2350
+ },
2351
+ [balances]
2352
+ );
2353
+ const sortedBalances = [...balances].sort((a, b) => b.uiAmount - a.uiAmount);
2354
+ const positiveBalances = balances.filter((balance) => balance.hasBalance);
2355
+ return {
2356
+ balances: sortedBalances,
2357
+ positiveBalances,
2358
+ isLoading,
2359
+ error,
2360
+ refreshBalances,
2361
+ getTokenBalance,
2362
+ hasSufficientBalance: hasSufficientBalance2,
2363
+ getFormattedBalance,
2364
+ getTotalValue,
2365
+ hasAnyBalance: positiveBalances.length > 0,
2366
+ isConnected: !!publicKey
2367
+ };
2368
+ };
2369
+ var useDirectWalletPayment = () => {
2370
+ const { publicKey, signTransaction, connected } = useWallet();
2371
+ const solanaService = useSolanaService();
2372
+ const [paymentState, setPaymentState] = useState({
2373
+ loading: false,
2374
+ error: null,
2375
+ success: false,
2376
+ transactionId: null
2377
+ });
2378
+ const resetPayment = useCallback(() => {
2379
+ setPaymentState({
2380
+ loading: false,
2381
+ error: null,
2382
+ success: false,
2383
+ transactionId: null
2384
+ });
2385
+ }, []);
2386
+ const payWithWallet = useCallback(
2387
+ async (token, priceId) => {
2388
+ if (!connected || !publicKey || !signTransaction) {
2389
+ setPaymentState((prev) => ({
2390
+ ...prev,
2391
+ error: "Wallet not connected. Please connect your wallet first."
2392
+ }));
2393
+ return;
2394
+ }
2395
+ setPaymentState({
2396
+ loading: true,
2397
+ error: null,
2398
+ success: false,
2399
+ transactionId: null
2400
+ });
2401
+ try {
2402
+ console.log("Generating payment transaction...", {
2403
+ token: token.symbol,
2404
+ priceId
2405
+ });
2406
+ const paymentData = await solanaService.generatePayment(
2407
+ priceId,
2408
+ token.symbol,
2409
+ publicKey.toBase58()
2410
+ );
2411
+ const transactionBuffer = Buffer.from(paymentData.transaction, "base64");
2412
+ const transaction = Transaction.from(transactionBuffer);
2413
+ console.log("Requesting wallet signature...");
2414
+ const signedTransaction = await signTransaction(transaction);
2415
+ const signedTransactionBase64 = signedTransaction.serialize().toString("base64");
2416
+ console.log("Submitting signed transaction...");
2417
+ const submitResult = await solanaService.submitPayment(
2418
+ signedTransactionBase64,
2419
+ priceId,
2420
+ paymentData.intent_id
2421
+ );
2422
+ setPaymentState({
2423
+ loading: false,
2424
+ error: null,
2425
+ success: true,
2426
+ transactionId: submitResult.transaction_id
2427
+ });
2428
+ console.log("Payment successful!", submitResult);
2429
+ } catch (err) {
2430
+ console.error("Payment failed:", err);
2431
+ let errorMessage = "Payment failed. Please try again.";
2432
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : "";
2433
+ if (message.includes("User rejected")) {
2434
+ errorMessage = "Payment cancelled by user.";
2435
+ } else if (/insufficient\s+funds/i.test(message)) {
2436
+ errorMessage = `Insufficient ${token.symbol} balance.`;
2437
+ } else if (/network/i.test(message)) {
2438
+ errorMessage = "Network error. Please check your connection.";
2439
+ } else if (message) {
2440
+ errorMessage = message;
2441
+ }
2442
+ setPaymentState({
2443
+ loading: false,
2444
+ error: errorMessage,
2445
+ success: false,
2446
+ transactionId: null
2447
+ });
2448
+ }
2449
+ },
2450
+ [connected, publicKey, signTransaction, solanaService]
2451
+ );
2452
+ return {
2453
+ paymentState,
2454
+ payWithWallet,
2455
+ resetPayment
2456
+ };
2457
+ };
2458
+ var usePaymentStatus = (options = {}) => {
2459
+ const { connection } = useConnection();
2460
+ const { services } = usePaymentContext();
2461
+ const billingApi = services.billingApi;
2462
+ const {
2463
+ transactionId,
2464
+ purchaseId,
2465
+ onConfirmed,
2466
+ onFailed,
2467
+ maxRetries = 30,
2468
+ // 5 minutes with 10s intervals
2469
+ retryInterval = 1e4
2470
+ // 10 seconds
2471
+ } = options;
2472
+ const [status, setStatus] = useState(null);
2473
+ const [paymentStatus, setPaymentStatus] = useState(null);
2474
+ const [isLoading, setIsLoading] = useState(false);
2475
+ const [error, setError] = useState(null);
2476
+ const [retryCount, setRetryCount] = useState(0);
2477
+ const intervalRef = useRef(null);
2478
+ const isMonitoringRef = useRef(false);
2479
+ useEffect(() => {
2480
+ return () => {
2481
+ if (intervalRef.current) {
2482
+ clearInterval(intervalRef.current);
2483
+ }
2484
+ };
2485
+ }, []);
2486
+ const checkTransactionStatus = useCallback(
2487
+ async (signature) => {
2488
+ try {
2489
+ const statusResponse = await connection.getSignatureStatus(signature, {
2490
+ searchTransactionHistory: true
2491
+ });
2492
+ if (statusResponse.value === null) {
2493
+ return {
2494
+ signature,
2495
+ confirmationStatus: "processed",
2496
+ confirmations: 0,
2497
+ error: "Transaction not found"
2498
+ };
2499
+ }
2500
+ const { confirmationStatus, confirmations, slot, err } = statusResponse.value;
2501
+ let blockTime;
2502
+ if (slot) {
2503
+ try {
2504
+ blockTime = await connection.getBlockTime(slot) || void 0;
2505
+ } catch (error2) {
2506
+ }
2507
+ }
2508
+ return {
2509
+ signature,
2510
+ confirmationStatus: err ? "failed" : confirmationStatus || "processed",
2511
+ confirmations: confirmations || 0,
2512
+ slot,
2513
+ blockTime,
2514
+ error: err ? `Transaction failed: ${err}` : void 0
2515
+ };
2516
+ } catch (error2) {
2517
+ console.error("Failed to check transaction status:", error2);
2518
+ return {
2519
+ signature,
2520
+ confirmationStatus: "failed",
2521
+ confirmations: 0,
2522
+ error: error2 instanceof Error ? error2.message : "Failed to check transaction status"
2523
+ };
2524
+ }
2525
+ },
2526
+ [connection]
2527
+ );
2528
+ const checkPaymentStatus = useCallback(
2529
+ async (id) => {
2530
+ try {
2531
+ const data = await billingApi.get(
2532
+ `/payment/status/${id}`
2533
+ );
2534
+ return data;
2535
+ } catch (error2) {
2536
+ if (error2?.status === 404) {
2537
+ return null;
2538
+ }
2539
+ console.error("Failed to check payment status:", error2);
2540
+ return null;
2541
+ }
2542
+ },
2543
+ [billingApi]
2544
+ );
2545
+ const startMonitoring = useCallback(async () => {
2546
+ if (isMonitoringRef.current || !transactionId && !purchaseId) {
2547
+ return;
2548
+ }
2549
+ isMonitoringRef.current = true;
2550
+ setIsLoading(true);
2551
+ setError(null);
2552
+ setRetryCount(0);
2553
+ const monitor = async () => {
2554
+ try {
2555
+ let currentStatus = null;
2556
+ let currentPaymentStatus = null;
2557
+ if (transactionId) {
2558
+ currentStatus = await checkTransactionStatus(transactionId);
2559
+ setStatus(currentStatus);
2560
+ }
2561
+ if (purchaseId) {
2562
+ currentPaymentStatus = await checkPaymentStatus(purchaseId);
2563
+ setPaymentStatus(currentPaymentStatus);
2564
+ }
2565
+ const isBlockchainConfirmed = currentStatus?.confirmationStatus === "finalized" || currentStatus?.confirmationStatus === "confirmed";
2566
+ const isBackendConfirmed = currentPaymentStatus?.status === "confirmed";
2567
+ const isBlockchainFailed = currentStatus?.confirmationStatus === "failed" || currentStatus?.error;
2568
+ const isBackendFailed = currentPaymentStatus?.status === "failed";
2569
+ if (isBlockchainConfirmed || isBackendConfirmed) {
2570
+ if (intervalRef.current) {
2571
+ clearInterval(intervalRef.current);
2572
+ }
2573
+ isMonitoringRef.current = false;
2574
+ setIsLoading(false);
2575
+ onConfirmed?.();
2576
+ return;
2577
+ }
2578
+ if (isBlockchainFailed || isBackendFailed) {
2579
+ if (intervalRef.current) {
2580
+ clearInterval(intervalRef.current);
2581
+ }
2582
+ isMonitoringRef.current = false;
2583
+ setIsLoading(false);
2584
+ const errorMessage = currentStatus?.error || "Payment failed";
2585
+ setError(errorMessage);
2586
+ onFailed?.(errorMessage);
2587
+ return;
2588
+ }
2589
+ setRetryCount((prev) => prev + 1);
2590
+ if (retryCount >= maxRetries) {
2591
+ if (intervalRef.current) {
2592
+ clearInterval(intervalRef.current);
2593
+ }
2594
+ isMonitoringRef.current = false;
2595
+ setIsLoading(false);
2596
+ const timeoutError = "Payment confirmation timeout - please check your transaction manually";
2597
+ setError(timeoutError);
2598
+ onFailed?.(timeoutError);
2599
+ return;
2600
+ }
2601
+ } catch (error2) {
2602
+ console.error("Error monitoring payment:", error2);
2603
+ setRetryCount((prev) => prev + 1);
2604
+ if (retryCount >= maxRetries) {
2605
+ if (intervalRef.current) {
2606
+ clearInterval(intervalRef.current);
2607
+ }
2608
+ isMonitoringRef.current = false;
2609
+ setIsLoading(false);
2610
+ const monitorError = "Failed to monitor payment status";
2611
+ setError(monitorError);
2612
+ onFailed?.(monitorError);
2613
+ }
2614
+ }
2615
+ };
2616
+ await monitor();
2617
+ intervalRef.current = setInterval(monitor, retryInterval);
2618
+ }, [
2619
+ transactionId,
2620
+ purchaseId,
2621
+ checkTransactionStatus,
2622
+ checkPaymentStatus,
2623
+ onConfirmed,
2624
+ onFailed,
2625
+ maxRetries,
2626
+ retryInterval,
2627
+ retryCount
2628
+ ]);
2629
+ const stopMonitoring = useCallback(() => {
2630
+ if (intervalRef.current) {
2631
+ clearInterval(intervalRef.current);
2632
+ }
2633
+ isMonitoringRef.current = false;
2634
+ setIsLoading(false);
2635
+ }, []);
2636
+ const checkStatus = useCallback(async () => {
2637
+ if (!transactionId && !purchaseId) return;
2638
+ setIsLoading(true);
2639
+ setError(null);
2640
+ try {
2641
+ if (transactionId) {
2642
+ const txStatus = await checkTransactionStatus(transactionId);
2643
+ setStatus(txStatus);
2644
+ }
2645
+ if (purchaseId) {
2646
+ const payStatus = await checkPaymentStatus(purchaseId);
2647
+ setPaymentStatus(payStatus);
2648
+ }
2649
+ } catch (error2) {
2650
+ const errorMessage = error2 instanceof Error ? error2.message : "Failed to check status";
2651
+ setError(errorMessage);
2652
+ } finally {
2653
+ setIsLoading(false);
2654
+ }
2655
+ }, [transactionId, purchaseId, checkTransactionStatus, checkPaymentStatus]);
2656
+ useEffect(() => {
2657
+ if ((transactionId || purchaseId) && !isMonitoringRef.current) {
2658
+ startMonitoring();
2659
+ }
2660
+ return () => {
2661
+ stopMonitoring();
2662
+ };
2663
+ }, [transactionId, purchaseId, startMonitoring, stopMonitoring]);
2664
+ const getConfirmationStatus = useCallback(() => {
2665
+ if (paymentStatus?.status === "confirmed") return "confirmed";
2666
+ if (paymentStatus?.status === "failed") return "failed";
2667
+ if (status?.confirmationStatus === "finalized") return "confirmed";
2668
+ if (status?.confirmationStatus === "confirmed") return "confirmed";
2669
+ if (status?.confirmationStatus === "failed" || status?.error)
2670
+ return "failed";
2671
+ return "pending";
2672
+ }, [status, paymentStatus]);
2673
+ const getSolscanUrl = useCallback(
2674
+ (signature) => {
2675
+ const txId = signature || transactionId;
2676
+ if (!txId) return null;
2677
+ return `https://solscan.io/tx/${txId}`;
2678
+ },
2679
+ [transactionId]
2680
+ );
2681
+ return {
2682
+ status,
2683
+ paymentStatus,
2684
+ isLoading,
2685
+ error,
2686
+ retryCount,
2687
+ maxRetries,
2688
+ isMonitoring: isMonitoringRef.current,
2689
+ confirmationStatus: getConfirmationStatus(),
2690
+ startMonitoring,
2691
+ stopMonitoring,
2692
+ checkStatus,
2693
+ getSolscanUrl,
2694
+ isConfirmed: getConfirmationStatus() === "confirmed",
2695
+ isFailed: getConfirmationStatus() === "failed",
2696
+ isPending: getConfirmationStatus() === "pending"
2697
+ };
2698
+ };
2699
+ var useSubscriptionActions = () => {
2700
+ const { services } = usePaymentContext();
2701
+ const ensurePrice = (priceId) => {
2702
+ if (!priceId) {
2703
+ throw new Error("payments-ui: priceId is required for subscription actions");
2704
+ }
2705
+ return priceId;
2706
+ };
2707
+ const subscribeWithCard = useCallback(
2708
+ async ({
2709
+ priceId,
2710
+ processor = "nmi",
2711
+ provider,
2712
+ paymentToken,
2713
+ billing
2714
+ }) => {
2715
+ const payload = {
2716
+ priceId: ensurePrice(priceId),
2717
+ paymentToken,
2718
+ processor,
2719
+ provider,
2720
+ email: billing.email,
2721
+ firstName: billing.firstName,
2722
+ lastName: billing.lastName,
2723
+ address1: billing.address1,
2724
+ city: billing.city,
2725
+ state: billing.stateRegion,
2726
+ zipCode: billing.postalCode,
2727
+ country: billing.country
2728
+ };
2729
+ return services.subscriptions.subscribe("nmi", payload);
2730
+ },
2731
+ [services]
2732
+ );
2733
+ const subscribeWithSavedMethod = useCallback(
2734
+ async ({
2735
+ priceId,
2736
+ processor = "nmi",
2737
+ provider,
2738
+ paymentMethodId,
2739
+ email
2740
+ }) => {
2741
+ const payload = {
2742
+ priceId: ensurePrice(priceId),
2743
+ paymentMethodId,
2744
+ processor,
2745
+ provider,
2746
+ email
2747
+ };
2748
+ return services.subscriptions.subscribe("nmi", payload);
2749
+ },
2750
+ [services]
2751
+ );
2752
+ const subscribeWithCCBill = useCallback(
2753
+ async ({
2754
+ priceId,
2755
+ email,
2756
+ firstName,
2757
+ lastName,
2758
+ zipCode,
2759
+ country,
2760
+ processor = "ccbill"
2761
+ }) => {
2762
+ const payload = {
2763
+ priceId: ensurePrice(priceId),
2764
+ email,
2765
+ firstName,
2766
+ lastName,
2767
+ zipCode,
2768
+ country,
2769
+ processor
2770
+ };
2771
+ return services.subscriptions.subscribe("ccbill", payload);
2772
+ },
2773
+ [services]
2774
+ );
2775
+ const generateFlexFormUrl = useCallback(
2776
+ async ({
2777
+ priceId,
2778
+ firstName,
2779
+ lastName,
2780
+ address1,
2781
+ city,
2782
+ state,
2783
+ zipCode,
2784
+ country
2785
+ }) => {
2786
+ const payload = {
2787
+ price_id: ensurePrice(priceId),
2788
+ first_name: firstName,
2789
+ last_name: lastName,
2790
+ address1,
2791
+ city,
2792
+ state,
2793
+ zip_code: zipCode,
2794
+ country
2795
+ };
2796
+ return services.subscriptions.generateFlexFormUrl(payload);
2797
+ },
2798
+ [services]
2799
+ );
2800
+ return {
2801
+ subscribeWithCard,
2802
+ subscribeWithSavedMethod,
2803
+ subscribeWithCCBill,
2804
+ generateFlexFormUrl
2805
+ };
2806
+ };
2807
+
2808
+ export { CardDetailsForm, CardPaymentService, PaymentApp, PaymentExperience, PaymentMethodService, PaymentProvider, SolanaPaymentSelector, SolanaPaymentService, StoredPaymentMethods, SubscriptionService, TokenCatalog, WalletGateway, createPaymentStore, useDirectWalletPayment, usePaymentContext, usePaymentMethodService, usePaymentMethods, usePaymentStatus, usePaymentStore, useSolanaDirectPayment, useSolanaQrPayment, useSolanaService, useSubscriptionActions, useSupportedTokens, useTokenBalance };
2809
+ //# sourceMappingURL=index.js.map
2810
+ //# sourceMappingURL=index.js.map