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