@flopay/react 0.3.11 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/index.cjs +526 -195
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -5
- package/dist/index.d.ts +27 -5
- package/dist/index.mjs +517 -186
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -69,7 +69,7 @@ function FloPayProvider({
|
|
|
69
69
|
// src/flopay-checkout.tsx
|
|
70
70
|
import React5, { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
|
|
71
71
|
import { loadFloPay, PaymentAPI } from "@flopay/js";
|
|
72
|
-
import { FloPayError as
|
|
72
|
+
import { FloPayError as FloPayError3, resolveBillingApiUrl as resolveBillingApiUrl3, buildCheckoutDisplayData, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme2 } from "@flopay/shared";
|
|
73
73
|
|
|
74
74
|
// src/card-button-content.tsx
|
|
75
75
|
import "react";
|
|
@@ -227,8 +227,116 @@ function useBillingApiUrl() {
|
|
|
227
227
|
return ctx.billingApiUrl || resolveBillingApiUrl2();
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
// src/
|
|
230
|
+
// src/checkout-utils.ts
|
|
231
231
|
import { FloPayError } from "@flopay/shared";
|
|
232
|
+
function mergeInlineSessionPatches(base, patch) {
|
|
233
|
+
if (!patch) return base;
|
|
234
|
+
if (!base) return patch;
|
|
235
|
+
return {
|
|
236
|
+
account: { ...base.account ?? {}, ...patch.account ?? {} },
|
|
237
|
+
couponCodes: patch.couponCodes ?? base.couponCodes,
|
|
238
|
+
tagsData: {
|
|
239
|
+
...base.tagsData ?? {},
|
|
240
|
+
...patch.tagsData ?? {}
|
|
241
|
+
},
|
|
242
|
+
utmMetadata: patch.utmMetadata ?? base.utmMetadata
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function mergeInlineSessionPatch(params, patch) {
|
|
246
|
+
if (!patch) return params;
|
|
247
|
+
return {
|
|
248
|
+
...params,
|
|
249
|
+
account: {
|
|
250
|
+
...params.account,
|
|
251
|
+
...patch.account ?? {}
|
|
252
|
+
},
|
|
253
|
+
couponCodes: patch.couponCodes ?? params.couponCodes,
|
|
254
|
+
tagsData: {
|
|
255
|
+
...params.tagsData ?? {},
|
|
256
|
+
...patch.tagsData ?? {}
|
|
257
|
+
},
|
|
258
|
+
utmMetadata: patch.utmMetadata ?? params.utmMetadata
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function buildSyntheticSession(params, checkoutModeOverride) {
|
|
262
|
+
const totalAmount = [
|
|
263
|
+
...(params.items ?? []).map((item) => item.overrideAmount ?? item.totalAmount ?? 0),
|
|
264
|
+
...(params.subscriptions ?? []).map((subscription) => subscription.overrideAmount ?? subscription.totalAmount ?? 0)
|
|
265
|
+
].reduce((sum, value) => sum + value, 0);
|
|
266
|
+
const currency = params.items?.[0]?.currency ?? params.subscriptions?.[0]?.currency ?? "USD";
|
|
267
|
+
return {
|
|
268
|
+
id: "",
|
|
269
|
+
clientSecret: "",
|
|
270
|
+
mode: "payment",
|
|
271
|
+
amount: Math.round(totalAmount * 100),
|
|
272
|
+
currency,
|
|
273
|
+
status: "open",
|
|
274
|
+
customer: {
|
|
275
|
+
id: params.account.userId,
|
|
276
|
+
email: params.account.email ?? "",
|
|
277
|
+
firstName: params.account.firstName,
|
|
278
|
+
lastName: params.account.lastName,
|
|
279
|
+
gender: params.account.gender ?? void 0,
|
|
280
|
+
city: params.account.city ?? void 0,
|
|
281
|
+
state: params.account.state ?? void 0,
|
|
282
|
+
country: params.account.country ?? void 0,
|
|
283
|
+
zip: params.account.zip ?? void 0
|
|
284
|
+
},
|
|
285
|
+
successUrl: params.successUrl,
|
|
286
|
+
cancelUrl: params.cancelUrl,
|
|
287
|
+
checkoutMode: params.checkoutMode ?? checkoutModeOverride ?? "full",
|
|
288
|
+
items: (params.items ?? []).map((item, idx) => ({
|
|
289
|
+
uuid: `synthetic-item-${idx}`,
|
|
290
|
+
checkoutSessionId: "",
|
|
291
|
+
providerItemId: item.providerItemId,
|
|
292
|
+
providerItemName: item.providerItemName ?? item.providerItemId,
|
|
293
|
+
quantity: item.quantity ?? 1,
|
|
294
|
+
totalAmount: item.totalAmount,
|
|
295
|
+
overrideAmount: item.overrideAmount ?? null,
|
|
296
|
+
currency: item.currency ?? currency
|
|
297
|
+
})),
|
|
298
|
+
subscriptions: (params.subscriptions ?? []).map((subscription, idx) => ({
|
|
299
|
+
uuid: `synthetic-sub-${idx}`,
|
|
300
|
+
checkoutSessionId: "",
|
|
301
|
+
providerPlanId: subscription.providerPlanId,
|
|
302
|
+
providerPlanName: subscription.providerPlanName ?? subscription.providerPlanId,
|
|
303
|
+
quantity: subscription.quantity ?? 1,
|
|
304
|
+
totalAmount: subscription.totalAmount,
|
|
305
|
+
overrideAmount: subscription.overrideAmount ?? null,
|
|
306
|
+
currency: subscription.currency ?? currency
|
|
307
|
+
}))
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function ensureInlineSessionReady(params) {
|
|
311
|
+
if (!params.account.email?.trim()) {
|
|
312
|
+
throw new FloPayError(
|
|
313
|
+
"Email is required before continuing with card checkout.",
|
|
314
|
+
"validation_error",
|
|
315
|
+
{ param: "createSession.account.email" }
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function mergeAccountPatch(base, patch) {
|
|
320
|
+
if (!patch) return base;
|
|
321
|
+
return {
|
|
322
|
+
...base,
|
|
323
|
+
...patch
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function buildDeclineEvent(method, input, overrides) {
|
|
327
|
+
const message = typeof input === "string" ? input : input.message;
|
|
328
|
+
const code = typeof input === "string" ? overrides?.code : overrides?.code ?? input.code;
|
|
329
|
+
const declineCode = typeof input === "string" ? overrides?.declineCode : overrides?.declineCode ?? input.declineCode;
|
|
330
|
+
return {
|
|
331
|
+
method,
|
|
332
|
+
message,
|
|
333
|
+
...code ? { code } : {},
|
|
334
|
+
...declineCode ? { declineCode } : {}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/split-card-form.tsx
|
|
339
|
+
import { FloPayError as FloPayError2 } from "@flopay/shared";
|
|
232
340
|
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
233
341
|
var WALLET_RESUME_KEY = "flopay_wallet_resume";
|
|
234
342
|
var FLOPAY_KEYFRAMES = `
|
|
@@ -366,7 +474,8 @@ function PayPalButtonInner({
|
|
|
366
474
|
onTokenizedBody,
|
|
367
475
|
onErrorChange,
|
|
368
476
|
isProcessing = false,
|
|
369
|
-
onButtonClick
|
|
477
|
+
onButtonClick,
|
|
478
|
+
onDecline
|
|
370
479
|
}) {
|
|
371
480
|
const stripe = useStripeRaw();
|
|
372
481
|
const elements = useStripeElements();
|
|
@@ -386,7 +495,9 @@ function PayPalButtonInner({
|
|
|
386
495
|
try {
|
|
387
496
|
setSubmitting(true);
|
|
388
497
|
if (redirectStatus === "failed") {
|
|
389
|
-
|
|
498
|
+
const message = "PayPal payment was declined. Please try again.";
|
|
499
|
+
onErrorChange?.(message);
|
|
500
|
+
onDecline?.(buildDeclineEvent("paypal", message));
|
|
390
501
|
return;
|
|
391
502
|
}
|
|
392
503
|
const { paymentIntent, error } = await stripe.retrievePaymentIntent(clientSecret);
|
|
@@ -408,7 +519,9 @@ function PayPalButtonInner({
|
|
|
408
519
|
url.searchParams.delete("redirect_status");
|
|
409
520
|
window.history.replaceState({}, "", url.toString());
|
|
410
521
|
} else {
|
|
411
|
-
|
|
522
|
+
const message = "PayPal payment was not completed. Please try again.";
|
|
523
|
+
onErrorChange?.(message);
|
|
524
|
+
onDecline?.(buildDeclineEvent("paypal", message));
|
|
412
525
|
}
|
|
413
526
|
} catch (err) {
|
|
414
527
|
onErrorChange?.(err instanceof Error ? err.message : "Failed to complete PayPal payment.");
|
|
@@ -416,7 +529,7 @@ function PayPalButtonInner({
|
|
|
416
529
|
setSubmitting(false);
|
|
417
530
|
}
|
|
418
531
|
})();
|
|
419
|
-
}, [stripe, onTokenizedBody, onErrorChange]);
|
|
532
|
+
}, [stripe, onTokenizedBody, onErrorChange, onDecline]);
|
|
420
533
|
const handlePayPalConfirm = useCallback(async (_event) => {
|
|
421
534
|
if (!stripe || !elements) return;
|
|
422
535
|
onButtonClick?.("paypal");
|
|
@@ -457,7 +570,11 @@ function PayPalButtonInner({
|
|
|
457
570
|
redirect: "if_required"
|
|
458
571
|
});
|
|
459
572
|
if (confirmError) {
|
|
460
|
-
|
|
573
|
+
const message = confirmError.message ?? "PayPal payment failed.";
|
|
574
|
+
onErrorChange?.(message);
|
|
575
|
+
onDecline?.(buildDeclineEvent("paypal", message, {
|
|
576
|
+
code: confirmError.code
|
|
577
|
+
}));
|
|
461
578
|
return;
|
|
462
579
|
}
|
|
463
580
|
const confirmedPmId = typeof paymentIntent?.payment_method === "string" ? paymentIntent.payment_method : paymentIntent?.payment_method?.id;
|
|
@@ -472,7 +589,7 @@ function PayPalButtonInner({
|
|
|
472
589
|
} finally {
|
|
473
590
|
setSubmitting(false);
|
|
474
591
|
}
|
|
475
|
-
}, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]);
|
|
592
|
+
}, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline]);
|
|
476
593
|
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
477
594
|
/* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(
|
|
478
595
|
ExpressCheckoutElement,
|
|
@@ -481,6 +598,9 @@ function PayPalButtonInner({
|
|
|
481
598
|
onLoadError: () => {
|
|
482
599
|
},
|
|
483
600
|
onConfirm: handlePayPalConfirm,
|
|
601
|
+
onCancel: () => {
|
|
602
|
+
onDecline?.(buildDeclineEvent("paypal", "PayPal checkout was cancelled."));
|
|
603
|
+
},
|
|
484
604
|
options: {
|
|
485
605
|
buttonType: { paypal: "paypal" },
|
|
486
606
|
paymentMethods: {
|
|
@@ -503,13 +623,15 @@ function WalletButtonInner({
|
|
|
503
623
|
showGooglePay = true,
|
|
504
624
|
onTokenizedBody,
|
|
505
625
|
onErrorChange,
|
|
506
|
-
onButtonClick
|
|
626
|
+
onButtonClick,
|
|
627
|
+
onDecline
|
|
507
628
|
}) {
|
|
508
629
|
const stripe = useStripeRaw();
|
|
509
630
|
const elements = useStripeElements();
|
|
510
631
|
const [ready, setReady] = useState2(false);
|
|
511
632
|
const [submitting, setSubmitting] = useState2(false);
|
|
512
633
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
634
|
+
const lastWalletMethodRef = useRef2("card");
|
|
513
635
|
const handleWalletConfirm = useCallback(
|
|
514
636
|
async (_event) => {
|
|
515
637
|
if (!stripe || !elements) return;
|
|
@@ -550,7 +672,12 @@ function WalletButtonInner({
|
|
|
550
672
|
{ payment_method: paymentMethod.id }
|
|
551
673
|
);
|
|
552
674
|
if (confirmError) {
|
|
553
|
-
|
|
675
|
+
const method = walletType === "apple_pay" ? "apple_pay" : "google_pay";
|
|
676
|
+
const message = confirmError.message ?? "Wallet payment failed.";
|
|
677
|
+
onErrorChange?.(message);
|
|
678
|
+
onDecline?.(buildDeclineEvent(method, message, {
|
|
679
|
+
code: confirmError.code
|
|
680
|
+
}));
|
|
554
681
|
return;
|
|
555
682
|
}
|
|
556
683
|
onTokenizedBody({
|
|
@@ -564,7 +691,7 @@ function WalletButtonInner({
|
|
|
564
691
|
setSubmitting(false);
|
|
565
692
|
}
|
|
566
693
|
},
|
|
567
|
-
[stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]
|
|
694
|
+
[stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline]
|
|
568
695
|
);
|
|
569
696
|
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
570
697
|
/* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(
|
|
@@ -573,7 +700,14 @@ function WalletButtonInner({
|
|
|
573
700
|
onReady: () => setReady(true),
|
|
574
701
|
onLoadError: () => {
|
|
575
702
|
},
|
|
703
|
+
onClick: (event) => {
|
|
704
|
+
lastWalletMethodRef.current = event.expressPaymentType === "apple_pay" ? "apple_pay" : "google_pay";
|
|
705
|
+
event.resolve();
|
|
706
|
+
},
|
|
576
707
|
onConfirm: handleWalletConfirm,
|
|
708
|
+
onCancel: () => {
|
|
709
|
+
onDecline?.(buildDeclineEvent(lastWalletMethodRef.current, "Wallet checkout was cancelled."));
|
|
710
|
+
},
|
|
577
711
|
options: {
|
|
578
712
|
buttonType: { applePay: "plain", googlePay: "plain" },
|
|
579
713
|
paymentMethods: {
|
|
@@ -598,6 +732,7 @@ function SplitCardFormInner({
|
|
|
598
732
|
userId,
|
|
599
733
|
onComplete,
|
|
600
734
|
onError,
|
|
735
|
+
onDecline,
|
|
601
736
|
onTokenizedBody,
|
|
602
737
|
firstName,
|
|
603
738
|
lastName,
|
|
@@ -620,6 +755,7 @@ function SplitCardFormInner({
|
|
|
620
755
|
cardBackButtonContent,
|
|
621
756
|
cardTitleContent,
|
|
622
757
|
onButtonClick,
|
|
758
|
+
onBeforeButtonClick,
|
|
623
759
|
enableAVS = false,
|
|
624
760
|
avsLayout: avsLayoutProp = "row",
|
|
625
761
|
country: countryProp,
|
|
@@ -628,6 +764,7 @@ function SplitCardFormInner({
|
|
|
628
764
|
onZipChange,
|
|
629
765
|
totalAmount = 0,
|
|
630
766
|
currency = "usd",
|
|
767
|
+
initialCardOpen = false,
|
|
631
768
|
innerRef
|
|
632
769
|
}) {
|
|
633
770
|
const flopay = useFloPay();
|
|
@@ -638,9 +775,10 @@ function SplitCardFormInner({
|
|
|
638
775
|
const [is3DSActive, setIs3DSActive] = useState2(false);
|
|
639
776
|
const [selectedCountry, setSelectedCountry] = useState2(countryProp ?? "US");
|
|
640
777
|
const [zipCode, setZipCode] = useState2(zipProp ?? "");
|
|
778
|
+
const [accountPatch, setAccountPatch] = useState2({});
|
|
641
779
|
const zipCodeRef = useRef2(zipProp ?? "");
|
|
642
780
|
const selectedCountryRef = useRef2(countryProp ?? "US");
|
|
643
|
-
const [viewState, setViewState] = useState2("buttons");
|
|
781
|
+
const [viewState, setViewState] = useState2(initialCardOpen ? "card" : "buttons");
|
|
644
782
|
const showCardForm = viewState === "expanding" || viewState === "card";
|
|
645
783
|
const TRANSITION_MS = 280;
|
|
646
784
|
const expandToCard = useCallback(() => {
|
|
@@ -651,6 +789,11 @@ function SplitCardFormInner({
|
|
|
651
789
|
setViewState("collapsing");
|
|
652
790
|
setTimeout(() => setViewState("buttons"), TRANSITION_MS);
|
|
653
791
|
}, []);
|
|
792
|
+
useEffect3(() => {
|
|
793
|
+
if (layout === "buttons" && initialCardOpen) {
|
|
794
|
+
setViewState("card");
|
|
795
|
+
}
|
|
796
|
+
}, [layout, initialCardOpen]);
|
|
654
797
|
const [fullName, setFullName] = useState2("");
|
|
655
798
|
const [formReady, setFormReady] = useState2(false);
|
|
656
799
|
const [overlayStatus, setOverlayStatus] = useState2(null);
|
|
@@ -674,6 +817,14 @@ function SplitCardFormInner({
|
|
|
674
817
|
const isSubmitting = externalProcessing ?? processing;
|
|
675
818
|
const isSelfContained = !onTokenizedBody;
|
|
676
819
|
const baseUrl = resolvedBillingApiUrl.replace(/\/+$/, "");
|
|
820
|
+
const resolvedAccount = useMemo2(() => mergeAccountPatch({
|
|
821
|
+
userId,
|
|
822
|
+
email,
|
|
823
|
+
firstName,
|
|
824
|
+
lastName,
|
|
825
|
+
country: countryProp,
|
|
826
|
+
zip: zipProp
|
|
827
|
+
}, accountPatch), [userId, email, firstName, lastName, countryProp, zipProp, accountPatch]);
|
|
677
828
|
const stripeInstance = useMemo2(() => {
|
|
678
829
|
if (!flopay) return null;
|
|
679
830
|
return flopay.getRawProvider();
|
|
@@ -699,6 +850,12 @@ function SplitCardFormInner({
|
|
|
699
850
|
},
|
|
700
851
|
[onErrorChange]
|
|
701
852
|
);
|
|
853
|
+
const emitDecline = useCallback(
|
|
854
|
+
(method, input, overrides) => {
|
|
855
|
+
onDecline?.(buildDeclineEvent(method, input, overrides));
|
|
856
|
+
},
|
|
857
|
+
[onDecline]
|
|
858
|
+
);
|
|
702
859
|
const showWallets = showApplePay || showGooglePay;
|
|
703
860
|
const handleNameChange = useCallback((value) => {
|
|
704
861
|
setFullName(value);
|
|
@@ -706,6 +863,30 @@ function SplitCardFormInner({
|
|
|
706
863
|
onFirstNameChange?.(parts[0] ?? "");
|
|
707
864
|
onLastNameChange?.(parts.length > 1 ? parts.slice(1).join(" ") : "");
|
|
708
865
|
}, [onFirstNameChange, onLastNameChange]);
|
|
866
|
+
const runBeforeCardButtonClick = useCallback(async () => {
|
|
867
|
+
if (!onBeforeButtonClick) return true;
|
|
868
|
+
try {
|
|
869
|
+
const result = await onBeforeButtonClick({
|
|
870
|
+
method: "card",
|
|
871
|
+
sessionId: sessionId || void 0
|
|
872
|
+
});
|
|
873
|
+
if (result === false) {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
if (result?.account) {
|
|
877
|
+
setAccountPatch((prev) => ({ ...prev ?? {}, ...result.account }));
|
|
878
|
+
}
|
|
879
|
+
return true;
|
|
880
|
+
} catch (err) {
|
|
881
|
+
const floPayErr = err instanceof FloPayError2 ? err : new FloPayError2(
|
|
882
|
+
err instanceof Error ? err.message : "Card before-click hook failed.",
|
|
883
|
+
"validation_error"
|
|
884
|
+
);
|
|
885
|
+
updateError(floPayErr.message);
|
|
886
|
+
onError?.(floPayErr);
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
}, [onBeforeButtonClick, sessionId, updateError, onError]);
|
|
709
890
|
const processPaymentInternal = useCallback(
|
|
710
891
|
async (tokenizedBody) => {
|
|
711
892
|
if (processingRef.current) return;
|
|
@@ -718,16 +899,16 @@ function SplitCardFormInner({
|
|
|
718
899
|
method: "POST",
|
|
719
900
|
headers: {
|
|
720
901
|
"Content-Type": "application/json",
|
|
721
|
-
"x-user-id": userId ?? ""
|
|
902
|
+
"x-user-id": resolvedAccount.userId ?? ""
|
|
722
903
|
},
|
|
723
904
|
body: JSON.stringify({
|
|
724
905
|
sessionId,
|
|
725
906
|
tokenizedData: tokenizedBody,
|
|
726
907
|
accountData: {
|
|
727
|
-
userId: userId ?? "",
|
|
728
|
-
email: email ?? "",
|
|
729
|
-
firstName: firstName ?? fullName.trim().split(/\s+/)[0] ?? "",
|
|
730
|
-
lastName: lastName ?? fullName.trim().split(/\s+/).slice(1).join(" ") ?? "",
|
|
908
|
+
userId: resolvedAccount.userId ?? "",
|
|
909
|
+
email: resolvedAccount.email ?? "",
|
|
910
|
+
firstName: resolvedAccount.firstName ?? fullName.trim().split(/\s+/)[0] ?? "",
|
|
911
|
+
lastName: resolvedAccount.lastName ?? fullName.trim().split(/\s+/).slice(1).join(" ") ?? "",
|
|
731
912
|
...enableAVS ? { zip: zipCodeRef.current, country: selectedCountryRef.current } : {}
|
|
732
913
|
},
|
|
733
914
|
chv
|
|
@@ -760,6 +941,7 @@ function SplitCardFormInner({
|
|
|
760
941
|
setOverlayStatus("error");
|
|
761
942
|
updateError(result.error.message);
|
|
762
943
|
onError?.(result.error);
|
|
944
|
+
emitDecline("card", result.error);
|
|
763
945
|
return;
|
|
764
946
|
}
|
|
765
947
|
if (result.status === "succeeded" || result.status === "processing") {
|
|
@@ -802,7 +984,11 @@ function SplitCardFormInner({
|
|
|
802
984
|
});
|
|
803
985
|
if (confirmError) {
|
|
804
986
|
setOverlayStatus("error");
|
|
805
|
-
|
|
987
|
+
const message2 = confirmError.message ?? "PayPal payment failed.";
|
|
988
|
+
updateError(message2);
|
|
989
|
+
emitDecline("paypal", message2, {
|
|
990
|
+
code: confirmError.code
|
|
991
|
+
});
|
|
806
992
|
}
|
|
807
993
|
} catch (err) {
|
|
808
994
|
setOverlayStatus("error");
|
|
@@ -811,7 +997,12 @@ function SplitCardFormInner({
|
|
|
811
997
|
return;
|
|
812
998
|
}
|
|
813
999
|
setOverlayStatus("error");
|
|
814
|
-
|
|
1000
|
+
const message = json?.message ?? "Payment failed. Please try again.";
|
|
1001
|
+
updateError(message);
|
|
1002
|
+
emitDecline(tokenizedBody.isPaypal ? "paypal" : "card", message, {
|
|
1003
|
+
code: json?.code ?? json?.gatewayErrorCode,
|
|
1004
|
+
declineCode: json?.declineCode ?? json?.gatewayDeclineReason
|
|
1005
|
+
});
|
|
815
1006
|
await new Promise((r) => setTimeout(r, 1500));
|
|
816
1007
|
} catch (err) {
|
|
817
1008
|
setOverlayStatus("error");
|
|
@@ -823,7 +1014,7 @@ function SplitCardFormInner({
|
|
|
823
1014
|
processingRef.current = false;
|
|
824
1015
|
}
|
|
825
1016
|
},
|
|
826
|
-
[baseUrl, sessionId,
|
|
1017
|
+
[baseUrl, sessionId, resolvedAccount, fullName, chv, flopay, onComplete, onError, updateError, emitDecline]
|
|
827
1018
|
);
|
|
828
1019
|
const dispatchTokenizedBody = useCallback(
|
|
829
1020
|
(tokenizedBody) => {
|
|
@@ -847,6 +1038,7 @@ function SplitCardFormInner({
|
|
|
847
1038
|
if (result.error) {
|
|
848
1039
|
updateError(result.error.message);
|
|
849
1040
|
onError?.(result.error);
|
|
1041
|
+
emitDecline("card", result.error);
|
|
850
1042
|
} else if (result.status === "succeeded" || result.status === "processing") {
|
|
851
1043
|
dispatchTokenizedBody({
|
|
852
1044
|
id: result.paymentIntentId,
|
|
@@ -860,7 +1052,7 @@ function SplitCardFormInner({
|
|
|
860
1052
|
setIs3DSActive(false);
|
|
861
1053
|
}
|
|
862
1054
|
}
|
|
863
|
-
}), [flopay, dispatchTokenizedBody, onError, updateError]);
|
|
1055
|
+
}), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
|
|
864
1056
|
useEffect3(() => {
|
|
865
1057
|
if (typeof window === "undefined") return;
|
|
866
1058
|
const stored = localStorage.getItem(WALLET_RESUME_KEY);
|
|
@@ -883,7 +1075,9 @@ function SplitCardFormInner({
|
|
|
883
1075
|
async (e) => {
|
|
884
1076
|
e.preventDefault();
|
|
885
1077
|
if (!flopay || !elements || isSubmitting || processingRef.current) return;
|
|
886
|
-
|
|
1078
|
+
if (layout !== "buttons") {
|
|
1079
|
+
onButtonClick?.("card");
|
|
1080
|
+
}
|
|
887
1081
|
setProcessing(true);
|
|
888
1082
|
setOverlayStatus("processing");
|
|
889
1083
|
updateError(null);
|
|
@@ -911,23 +1105,23 @@ function SplitCardFormInner({
|
|
|
911
1105
|
updateError(pmResult.error?.message ?? "Failed to create payment method.");
|
|
912
1106
|
return;
|
|
913
1107
|
}
|
|
914
|
-
if (!sessionId || !email) {
|
|
915
|
-
throw new
|
|
1108
|
+
if (!sessionId || !resolvedAccount.email) {
|
|
1109
|
+
throw new FloPayError2("Missing sessionId or email", "validation_error");
|
|
916
1110
|
}
|
|
917
1111
|
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
918
1112
|
method: "POST",
|
|
919
1113
|
headers: { "Content-Type": "application/json" },
|
|
920
1114
|
body: JSON.stringify({
|
|
921
1115
|
sessionId,
|
|
922
|
-
email,
|
|
1116
|
+
email: resolvedAccount.email,
|
|
923
1117
|
paymentMethodType: pmResult.paymentMethodId,
|
|
924
1118
|
isPaypal: false
|
|
925
1119
|
})
|
|
926
1120
|
});
|
|
927
|
-
if (!intentResponse.ok) throw new
|
|
1121
|
+
if (!intentResponse.ok) throw new FloPayError2("Failed to create payment intent", "api_error");
|
|
928
1122
|
const intentJson = await intentResponse.json();
|
|
929
1123
|
const intentClientSecret = intentJson.data?.id;
|
|
930
|
-
if (!intentClientSecret) throw new
|
|
1124
|
+
if (!intentClientSecret) throw new FloPayError2("No client_secret in payment intent response", "api_error");
|
|
931
1125
|
const confirmResult = await flopay.confirmCardPayment({
|
|
932
1126
|
clientSecret: intentClientSecret,
|
|
933
1127
|
paymentMethodId: pmResult.paymentMethodId
|
|
@@ -935,6 +1129,8 @@ function SplitCardFormInner({
|
|
|
935
1129
|
if (confirmResult.error) {
|
|
936
1130
|
setOverlayStatus("error");
|
|
937
1131
|
updateError(confirmResult.error.message);
|
|
1132
|
+
onError?.(confirmResult.error);
|
|
1133
|
+
emitDecline("card", confirmResult.error);
|
|
938
1134
|
await new Promise((r) => setTimeout(r, 1500));
|
|
939
1135
|
return;
|
|
940
1136
|
}
|
|
@@ -955,7 +1151,7 @@ function SplitCardFormInner({
|
|
|
955
1151
|
}
|
|
956
1152
|
}
|
|
957
1153
|
},
|
|
958
|
-
[flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError]
|
|
1154
|
+
[flopay, elements, isSubmitting, sessionId, resolvedAccount.email, baseUrl, isSelfContained, dispatchTokenizedBody, onButtonClick, onError, updateError, emitDecline, layout]
|
|
959
1155
|
);
|
|
960
1156
|
const isReady = flopay !== null && elements !== null;
|
|
961
1157
|
if (!isReady) {
|
|
@@ -1240,32 +1436,36 @@ function SplitCardFormInner({
|
|
|
1240
1436
|
PayPalButtonInner,
|
|
1241
1437
|
{
|
|
1242
1438
|
sessionId,
|
|
1243
|
-
email,
|
|
1439
|
+
email: resolvedAccount.email,
|
|
1244
1440
|
billingApiUrl: resolvedBillingApiUrl,
|
|
1245
1441
|
onTokenizedBody: dispatchTokenizedBody,
|
|
1246
1442
|
onErrorChange: updateError,
|
|
1247
1443
|
isProcessing: isSubmitting,
|
|
1248
|
-
onButtonClick
|
|
1444
|
+
onButtonClick,
|
|
1445
|
+
onDecline
|
|
1249
1446
|
}
|
|
1250
1447
|
) }) : showPayPal ? /* @__PURE__ */ jsx4("div", { style: { height: 44, borderRadius: 8, background: "#e5e7eb", animation: "flopay-pulse 1.5s ease-in-out infinite" } }) : null,
|
|
1251
1448
|
showWallets && stripeInstance ? /* @__PURE__ */ jsx4(StripeElements, { stripe: stripeInstance, options: walletOptions, children: /* @__PURE__ */ jsx4(
|
|
1252
1449
|
WalletButtonInner,
|
|
1253
1450
|
{
|
|
1254
1451
|
sessionId,
|
|
1255
|
-
email,
|
|
1452
|
+
email: resolvedAccount.email,
|
|
1256
1453
|
billingApiUrl: resolvedBillingApiUrl,
|
|
1257
1454
|
showApplePay,
|
|
1258
1455
|
showGooglePay,
|
|
1259
1456
|
onTokenizedBody: dispatchTokenizedBody,
|
|
1260
1457
|
onErrorChange: updateError,
|
|
1261
|
-
onButtonClick
|
|
1458
|
+
onButtonClick,
|
|
1459
|
+
onDecline
|
|
1262
1460
|
}
|
|
1263
1461
|
) }) : showWallets ? /* @__PURE__ */ jsx4("div", { style: { height: 44, borderRadius: 8, background: "#e5e7eb", animation: "flopay-pulse 1.5s ease-in-out infinite" } }) : null,
|
|
1264
1462
|
/* @__PURE__ */ jsx4(
|
|
1265
1463
|
"button",
|
|
1266
1464
|
{
|
|
1267
1465
|
type: "button",
|
|
1268
|
-
onClick: () => {
|
|
1466
|
+
onClick: async () => {
|
|
1467
|
+
const shouldContinue = await runBeforeCardButtonClick();
|
|
1468
|
+
if (!shouldContinue) return;
|
|
1269
1469
|
onButtonClick?.("card");
|
|
1270
1470
|
expandToCard();
|
|
1271
1471
|
},
|
|
@@ -1330,23 +1530,25 @@ function SplitCardFormInner({
|
|
|
1330
1530
|
WalletButtonInner,
|
|
1331
1531
|
{
|
|
1332
1532
|
sessionId,
|
|
1333
|
-
email,
|
|
1533
|
+
email: resolvedAccount.email,
|
|
1334
1534
|
billingApiUrl: resolvedBillingApiUrl,
|
|
1335
1535
|
showApplePay,
|
|
1336
1536
|
showGooglePay,
|
|
1337
1537
|
onTokenizedBody: dispatchTokenizedBody,
|
|
1338
|
-
onErrorChange: updateError
|
|
1538
|
+
onErrorChange: updateError,
|
|
1539
|
+
onDecline
|
|
1339
1540
|
}
|
|
1340
1541
|
) }),
|
|
1341
1542
|
showPayPal && stripeInstance && /* @__PURE__ */ jsx4(StripeElements, { stripe: stripeInstance, options: paypalOptions, children: /* @__PURE__ */ jsx4(
|
|
1342
1543
|
PayPalButtonInner,
|
|
1343
1544
|
{
|
|
1344
1545
|
sessionId,
|
|
1345
|
-
email,
|
|
1546
|
+
email: resolvedAccount.email,
|
|
1346
1547
|
billingApiUrl: resolvedBillingApiUrl,
|
|
1347
1548
|
onTokenizedBody: dispatchTokenizedBody,
|
|
1348
1549
|
onErrorChange: updateError,
|
|
1349
|
-
isProcessing: isSubmitting
|
|
1550
|
+
isProcessing: isSubmitting,
|
|
1551
|
+
onDecline
|
|
1350
1552
|
}
|
|
1351
1553
|
) }),
|
|
1352
1554
|
(showWallets && stripeInstance || showPayPal && stripeInstance) && /* @__PURE__ */ jsxs2("div", { style: {
|
|
@@ -1378,6 +1580,7 @@ function FloPayCheckout({
|
|
|
1378
1580
|
error: errorNode,
|
|
1379
1581
|
onComplete,
|
|
1380
1582
|
onError,
|
|
1583
|
+
onDecline,
|
|
1381
1584
|
showPayPal = true,
|
|
1382
1585
|
showApplePay = true,
|
|
1383
1586
|
showGooglePay = true,
|
|
@@ -1388,6 +1591,7 @@ function FloPayCheckout({
|
|
|
1388
1591
|
cardBackButtonContent,
|
|
1389
1592
|
cardTitleContent,
|
|
1390
1593
|
onButtonClick,
|
|
1594
|
+
onBeforeButtonClick,
|
|
1391
1595
|
enableAVS,
|
|
1392
1596
|
avsLayout,
|
|
1393
1597
|
submitLabel,
|
|
@@ -1409,13 +1613,28 @@ function FloPayCheckout({
|
|
|
1409
1613
|
const [currentMode, setCurrentMode] = useState3("full");
|
|
1410
1614
|
const [confirmProcessing, setConfirmProcessing] = useState3(false);
|
|
1411
1615
|
const [modeError, setModeError] = useState3(null);
|
|
1616
|
+
const [createSessionPatch, setCreateSessionPatch] = useState3(void 0);
|
|
1617
|
+
const [cardBootstrapPending, setCardBootstrapPending] = useState3(false);
|
|
1618
|
+
const [deferredCardOpen, setDeferredCardOpen] = useState3(false);
|
|
1412
1619
|
const autoCheckoutAttempted = useRef3(false);
|
|
1413
1620
|
const onCompleteRef = useRef3(onComplete);
|
|
1414
1621
|
onCompleteRef.current = onComplete;
|
|
1415
1622
|
const onErrorRef = useRef3(onError);
|
|
1416
1623
|
onErrorRef.current = onError;
|
|
1624
|
+
const onDeclineRef = useRef3(onDecline);
|
|
1625
|
+
onDeclineRef.current = onDecline;
|
|
1417
1626
|
const onSessionCompletedRef = useRef3(onSessionCompleted);
|
|
1418
1627
|
onSessionCompletedRef.current = onSessionCompleted;
|
|
1628
|
+
const effectiveCreateSession = useMemo3(
|
|
1629
|
+
() => createSessionParams ? mergeInlineSessionPatch(createSessionParams, createSessionPatch) : void 0,
|
|
1630
|
+
[createSessionParams, createSessionPatch]
|
|
1631
|
+
);
|
|
1632
|
+
const emitDecline = useCallback2(
|
|
1633
|
+
(method, input, overrides) => {
|
|
1634
|
+
onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
|
|
1635
|
+
},
|
|
1636
|
+
[]
|
|
1637
|
+
);
|
|
1419
1638
|
const processPaymentForMode = useCallback2(
|
|
1420
1639
|
async (sess) => {
|
|
1421
1640
|
const baseUrl = resolvedBillingUrl.replace(/\/+$/, "");
|
|
@@ -1455,9 +1674,13 @@ function FloPayCheckout({
|
|
|
1455
1674
|
// No client secret available
|
|
1456
1675
|
};
|
|
1457
1676
|
}
|
|
1458
|
-
throw new
|
|
1677
|
+
throw new FloPayError3(
|
|
1459
1678
|
json?.message ?? "Payment failed. Please try again.",
|
|
1460
|
-
"api_error"
|
|
1679
|
+
"api_error",
|
|
1680
|
+
{
|
|
1681
|
+
code: json?.code ?? json?.gatewayErrorCode,
|
|
1682
|
+
declineCode: json?.declineCode ?? json?.gatewayDeclineReason
|
|
1683
|
+
}
|
|
1461
1684
|
);
|
|
1462
1685
|
},
|
|
1463
1686
|
[resolvedBillingUrl, resolvedSessionId]
|
|
@@ -1476,6 +1699,9 @@ function FloPayCheckout({
|
|
|
1476
1699
|
});
|
|
1477
1700
|
if (nextActionError) {
|
|
1478
1701
|
setModeError(nextActionError.message ?? "3DS authentication failed.");
|
|
1702
|
+
emitDecline("card", nextActionError.message ?? "3DS authentication failed.", {
|
|
1703
|
+
code: nextActionError.code
|
|
1704
|
+
});
|
|
1479
1705
|
return false;
|
|
1480
1706
|
}
|
|
1481
1707
|
if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
|
|
@@ -1498,6 +1724,9 @@ function FloPayCheckout({
|
|
|
1498
1724
|
});
|
|
1499
1725
|
if (error) {
|
|
1500
1726
|
setModeError(error.message ?? "PayPal authorization failed.");
|
|
1727
|
+
emitDecline("paypal", error.message ?? "PayPal authorization failed.", {
|
|
1728
|
+
code: error.code
|
|
1729
|
+
});
|
|
1501
1730
|
return false;
|
|
1502
1731
|
}
|
|
1503
1732
|
onCompleteRef.current?.({ status: "succeeded" });
|
|
@@ -1505,16 +1734,24 @@ function FloPayCheckout({
|
|
|
1505
1734
|
}
|
|
1506
1735
|
return false;
|
|
1507
1736
|
},
|
|
1508
|
-
[resolvedBillingUrl, resolvedSessionId]
|
|
1737
|
+
[resolvedBillingUrl, resolvedSessionId, emitDecline]
|
|
1509
1738
|
);
|
|
1510
1739
|
const inflightRef = useRef3(/* @__PURE__ */ new Map());
|
|
1511
1740
|
const initializedHashRef = useRef3(null);
|
|
1741
|
+
const deferInlineSessionUntilCardClick = Boolean(
|
|
1742
|
+
effectiveCreateSession && !children && layout === "buttons" && onBeforeButtonClick && (effectiveCreateSession.checkoutMode ?? checkoutModeProp ?? "full") === "full"
|
|
1743
|
+
);
|
|
1512
1744
|
function hashCreateParams(params) {
|
|
1513
1745
|
const key = JSON.stringify({
|
|
1514
1746
|
c: params?.clientId,
|
|
1747
|
+
successUrl: params?.successUrl,
|
|
1748
|
+
cancelUrl: params?.cancelUrl,
|
|
1515
1749
|
i: params?.items?.map((x) => `${x.providerItemId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
|
|
1516
1750
|
s: params?.subscriptions?.map((x) => `${x.providerPlanId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
|
|
1517
|
-
|
|
1751
|
+
account: params?.account,
|
|
1752
|
+
couponCodes: params?.couponCodes,
|
|
1753
|
+
tagsData: params?.tagsData,
|
|
1754
|
+
utmMetadata: params?.utmMetadata,
|
|
1518
1755
|
m: params?.checkoutMode ?? "full"
|
|
1519
1756
|
});
|
|
1520
1757
|
let h = 0;
|
|
@@ -1524,138 +1761,167 @@ function FloPayCheckout({
|
|
|
1524
1761
|
return `flopay_session_${Math.abs(h).toString(36)}`;
|
|
1525
1762
|
}
|
|
1526
1763
|
const createSessionHash = useMemo3(
|
|
1527
|
-
() =>
|
|
1528
|
-
|
|
1529
|
-
[
|
|
1530
|
-
createSessionParams?.clientId,
|
|
1531
|
-
createSessionParams?.account?.email,
|
|
1532
|
-
createSessionParams?.checkoutMode,
|
|
1533
|
-
JSON.stringify(createSessionParams?.items),
|
|
1534
|
-
JSON.stringify(createSessionParams?.subscriptions)
|
|
1535
|
-
]
|
|
1764
|
+
() => effectiveCreateSession ? hashCreateParams(effectiveCreateSession) : "",
|
|
1765
|
+
[effectiveCreateSession]
|
|
1536
1766
|
);
|
|
1537
|
-
const createSessionParamsRef = useRef3(
|
|
1538
|
-
createSessionParamsRef.current =
|
|
1767
|
+
const createSessionParamsRef = useRef3(effectiveCreateSession);
|
|
1768
|
+
createSessionParamsRef.current = effectiveCreateSession;
|
|
1769
|
+
useEffect4(() => {
|
|
1770
|
+
setResolvedSessionId(sessionIdProp ?? "");
|
|
1771
|
+
}, [sessionIdProp]);
|
|
1772
|
+
async function resolveInlineSession(params, cacheKey) {
|
|
1773
|
+
ensureInlineSessionReady(params);
|
|
1774
|
+
const api = new PaymentAPI(resolvedBillingUrl);
|
|
1775
|
+
let sid = typeof window !== "undefined" ? window.sessionStorage.getItem(cacheKey) : null;
|
|
1776
|
+
let realResult = null;
|
|
1777
|
+
if (sid) {
|
|
1778
|
+
try {
|
|
1779
|
+
realResult = await api.getUnifiedCheckoutSession(sid);
|
|
1780
|
+
if (realResult.data.session?.status === "complete") {
|
|
1781
|
+
if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
|
|
1782
|
+
sid = null;
|
|
1783
|
+
realResult = null;
|
|
1784
|
+
}
|
|
1785
|
+
} catch {
|
|
1786
|
+
if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
|
|
1787
|
+
sid = null;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (!sid) {
|
|
1791
|
+
realResult = await api.createAndFetchSession(params);
|
|
1792
|
+
sid = realResult.data.session?.id ?? "";
|
|
1793
|
+
if (sid && typeof window !== "undefined") {
|
|
1794
|
+
window.sessionStorage.setItem(cacheKey, sid);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return { sid: sid ?? "", result: realResult };
|
|
1798
|
+
}
|
|
1799
|
+
const bootstrapInlineSession = useCallback2(
|
|
1800
|
+
async (patch) => {
|
|
1801
|
+
const baseParams = createSessionParamsRef.current;
|
|
1802
|
+
if (!baseParams) {
|
|
1803
|
+
throw new FloPayError3("createSession is required to bootstrap checkout.", "validation_error");
|
|
1804
|
+
}
|
|
1805
|
+
const mergedParams = mergeInlineSessionPatch(baseParams, patch);
|
|
1806
|
+
const cacheKey = hashCreateParams(mergedParams);
|
|
1807
|
+
let promise = inflightRef.current.get(cacheKey);
|
|
1808
|
+
if (!promise) {
|
|
1809
|
+
promise = resolveInlineSession(mergedParams, cacheKey);
|
|
1810
|
+
inflightRef.current.set(cacheKey, promise);
|
|
1811
|
+
}
|
|
1812
|
+
let resolved;
|
|
1813
|
+
try {
|
|
1814
|
+
resolved = await promise;
|
|
1815
|
+
} finally {
|
|
1816
|
+
inflightRef.current.delete(cacheKey);
|
|
1817
|
+
}
|
|
1818
|
+
if (patch) {
|
|
1819
|
+
setCreateSessionPatch((prev) => mergeInlineSessionPatches(prev, patch));
|
|
1820
|
+
}
|
|
1821
|
+
const { sid, result: realResult } = resolved;
|
|
1822
|
+
setUnified(realResult);
|
|
1823
|
+
if (realResult.data.session) {
|
|
1824
|
+
setSession(realResult.data.session);
|
|
1825
|
+
}
|
|
1826
|
+
if (sid) {
|
|
1827
|
+
setResolvedSessionId(sid);
|
|
1828
|
+
}
|
|
1829
|
+
let publishableKey;
|
|
1830
|
+
if (realResult.provider === "stripe") {
|
|
1831
|
+
publishableKey = realResult.data.stripe?.publishableKey;
|
|
1832
|
+
}
|
|
1833
|
+
if (!publishableKey) publishableKey = fallbackPublishableKey;
|
|
1834
|
+
if (!publishableKey) {
|
|
1835
|
+
throw new FloPayError3(
|
|
1836
|
+
"No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
|
|
1837
|
+
"validation_error"
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
const instance = await loadFloPay(publishableKey, {
|
|
1841
|
+
billingApiUrl: resolvedBillingUrl,
|
|
1842
|
+
locale
|
|
1843
|
+
});
|
|
1844
|
+
flopayRef.current = instance;
|
|
1845
|
+
setFloPay(instance);
|
|
1846
|
+
initializedHashRef.current = cacheKey;
|
|
1847
|
+
setIsLoading(false);
|
|
1848
|
+
return resolved;
|
|
1849
|
+
},
|
|
1850
|
+
[fallbackPublishableKey, locale, resolvedBillingUrl]
|
|
1851
|
+
);
|
|
1852
|
+
const handleDeferredCardButtonClick = useCallback2(async () => {
|
|
1853
|
+
if (cardBootstrapPending) return;
|
|
1854
|
+
setLoadError(null);
|
|
1855
|
+
setCardBootstrapPending(true);
|
|
1856
|
+
try {
|
|
1857
|
+
const beforeClickResult = await onBeforeButtonClick?.({
|
|
1858
|
+
method: "card",
|
|
1859
|
+
createSession: effectiveCreateSession
|
|
1860
|
+
});
|
|
1861
|
+
if (beforeClickResult === false) {
|
|
1862
|
+
setDeferredCardOpen(false);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const patch = beforeClickResult && typeof beforeClickResult === "object" ? beforeClickResult : void 0;
|
|
1866
|
+
const baseParams = createSessionParamsRef.current;
|
|
1867
|
+
if (!baseParams) {
|
|
1868
|
+
throw new FloPayError3(
|
|
1869
|
+
"createSession is required to bootstrap checkout.",
|
|
1870
|
+
"validation_error"
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
ensureInlineSessionReady(mergeInlineSessionPatch(baseParams, patch));
|
|
1874
|
+
setDeferredCardOpen(true);
|
|
1875
|
+
onButtonClick?.("card");
|
|
1876
|
+
await bootstrapInlineSession(patch);
|
|
1877
|
+
} catch (err) {
|
|
1878
|
+
setDeferredCardOpen(false);
|
|
1879
|
+
const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(
|
|
1880
|
+
err instanceof Error ? err.message : "Failed to start card checkout.",
|
|
1881
|
+
"api_error"
|
|
1882
|
+
);
|
|
1883
|
+
setLoadError(floPayErr);
|
|
1884
|
+
onErrorRef.current?.(floPayErr);
|
|
1885
|
+
} finally {
|
|
1886
|
+
setCardBootstrapPending(false);
|
|
1887
|
+
}
|
|
1888
|
+
}, [
|
|
1889
|
+
bootstrapInlineSession,
|
|
1890
|
+
cardBootstrapPending,
|
|
1891
|
+
effectiveCreateSession,
|
|
1892
|
+
onButtonClick,
|
|
1893
|
+
onBeforeButtonClick
|
|
1894
|
+
]);
|
|
1539
1895
|
useEffect4(() => {
|
|
1540
1896
|
let cancelled = false;
|
|
1541
1897
|
setLoadError(null);
|
|
1542
1898
|
if (createSessionHash) {
|
|
1899
|
+
if (deferInlineSessionUntilCardClick) {
|
|
1900
|
+
if (initializedHashRef.current !== createSessionHash) {
|
|
1901
|
+
setSession(null);
|
|
1902
|
+
setUnified(null);
|
|
1903
|
+
setFloPay(null);
|
|
1904
|
+
setResolvedSessionId("");
|
|
1905
|
+
setDeferredCardOpen(false);
|
|
1906
|
+
flopayRef.current = null;
|
|
1907
|
+
}
|
|
1908
|
+
setCurrentMode(createSessionParamsRef.current?.checkoutMode ?? checkoutModeProp ?? "full");
|
|
1909
|
+
setIsLoading(false);
|
|
1910
|
+
return () => {
|
|
1911
|
+
cancelled = true;
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1543
1914
|
if (initializedHashRef.current === createSessionHash) return;
|
|
1544
1915
|
const params = createSessionParamsRef.current;
|
|
1545
|
-
|
|
1546
|
-
...(params.items ?? []).map((i) => i.overrideAmount ?? i.totalAmount ?? 0),
|
|
1547
|
-
...(params.subscriptions ?? []).map((s) => s.overrideAmount ?? s.totalAmount ?? 0)
|
|
1548
|
-
].reduce((sum, v) => sum + v, 0);
|
|
1549
|
-
const currency = params.items?.[0]?.currency ?? params.subscriptions?.[0]?.currency ?? "USD";
|
|
1550
|
-
const syntheticSession = {
|
|
1551
|
-
id: "",
|
|
1552
|
-
clientSecret: "",
|
|
1553
|
-
mode: "payment",
|
|
1554
|
-
amount: Math.round(totalAmount * 100),
|
|
1555
|
-
currency,
|
|
1556
|
-
status: "open",
|
|
1557
|
-
customer: {
|
|
1558
|
-
id: params.account.userId,
|
|
1559
|
-
email: params.account.email,
|
|
1560
|
-
firstName: params.account.firstName,
|
|
1561
|
-
lastName: params.account.lastName
|
|
1562
|
-
},
|
|
1563
|
-
successUrl: params.successUrl,
|
|
1564
|
-
cancelUrl: params.cancelUrl,
|
|
1565
|
-
checkoutMode: params.checkoutMode ?? "full",
|
|
1566
|
-
items: (params.items ?? []).map((i, idx) => ({
|
|
1567
|
-
uuid: `synthetic-item-${idx}`,
|
|
1568
|
-
checkoutSessionId: "",
|
|
1569
|
-
providerItemId: i.providerItemId,
|
|
1570
|
-
providerItemName: i.providerItemName ?? i.providerItemId,
|
|
1571
|
-
quantity: i.quantity ?? 1,
|
|
1572
|
-
totalAmount: i.totalAmount,
|
|
1573
|
-
overrideAmount: i.overrideAmount ?? null,
|
|
1574
|
-
currency: i.currency ?? currency
|
|
1575
|
-
})),
|
|
1576
|
-
subscriptions: (params.subscriptions ?? []).map((s, idx) => ({
|
|
1577
|
-
uuid: `synthetic-sub-${idx}`,
|
|
1578
|
-
checkoutSessionId: "",
|
|
1579
|
-
providerPlanId: s.providerPlanId,
|
|
1580
|
-
providerPlanName: s.providerPlanName ?? s.providerPlanId,
|
|
1581
|
-
quantity: s.quantity ?? 1,
|
|
1582
|
-
totalAmount: s.totalAmount,
|
|
1583
|
-
overrideAmount: s.overrideAmount ?? null,
|
|
1584
|
-
currency: s.currency ?? currency
|
|
1585
|
-
}))
|
|
1586
|
-
};
|
|
1587
|
-
setSession(syntheticSession);
|
|
1916
|
+
setSession(buildSyntheticSession(params, checkoutModeProp));
|
|
1588
1917
|
setCurrentMode(params.checkoutMode ?? checkoutModeProp ?? "full");
|
|
1589
1918
|
setIsLoading(false);
|
|
1590
|
-
const cacheKey = createSessionHash;
|
|
1591
|
-
async function resolveSession() {
|
|
1592
|
-
const api = new PaymentAPI(resolvedBillingUrl);
|
|
1593
|
-
let sid = typeof window !== "undefined" ? window.sessionStorage.getItem(cacheKey) : null;
|
|
1594
|
-
let realResult = null;
|
|
1595
|
-
if (sid) {
|
|
1596
|
-
try {
|
|
1597
|
-
realResult = await api.getUnifiedCheckoutSession(sid);
|
|
1598
|
-
if (realResult.data.session?.status === "complete") {
|
|
1599
|
-
if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
|
|
1600
|
-
sid = null;
|
|
1601
|
-
realResult = null;
|
|
1602
|
-
}
|
|
1603
|
-
} catch {
|
|
1604
|
-
if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
|
|
1605
|
-
sid = null;
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
if (!sid) {
|
|
1609
|
-
realResult = await api.createAndFetchSession(createSessionParamsRef.current);
|
|
1610
|
-
sid = realResult.data.session?.id ?? "";
|
|
1611
|
-
if (sid && typeof window !== "undefined") {
|
|
1612
|
-
window.sessionStorage.setItem(cacheKey, sid);
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
return { sid: sid ?? "", result: realResult };
|
|
1616
|
-
}
|
|
1617
1919
|
(async () => {
|
|
1618
1920
|
try {
|
|
1619
|
-
|
|
1620
|
-
if (!promise) {
|
|
1621
|
-
promise = resolveSession();
|
|
1622
|
-
inflightRef.current.set(cacheKey, promise);
|
|
1623
|
-
}
|
|
1624
|
-
let resolved;
|
|
1625
|
-
try {
|
|
1626
|
-
resolved = await promise;
|
|
1627
|
-
} finally {
|
|
1628
|
-
inflightRef.current.delete(cacheKey);
|
|
1629
|
-
}
|
|
1630
|
-
if (cancelled) return;
|
|
1631
|
-
const { sid, result: realResult } = resolved;
|
|
1632
|
-
if (realResult) {
|
|
1633
|
-
setUnified(realResult);
|
|
1634
|
-
if (realResult.data.session) setSession(realResult.data.session);
|
|
1635
|
-
}
|
|
1636
|
-
if (sid) {
|
|
1637
|
-
setResolvedSessionId(sid);
|
|
1638
|
-
}
|
|
1639
|
-
let pk;
|
|
1640
|
-
if (realResult?.provider === "stripe") {
|
|
1641
|
-
pk = realResult.data.stripe?.publishableKey;
|
|
1642
|
-
}
|
|
1643
|
-
if (!pk) pk = fallbackPublishableKey;
|
|
1644
|
-
if (!pk) {
|
|
1645
|
-
throw new FloPayError2(
|
|
1646
|
-
"No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
|
|
1647
|
-
"validation_error"
|
|
1648
|
-
);
|
|
1649
|
-
}
|
|
1650
|
-
const instance = await loadFloPay(pk, { billingApiUrl: resolvedBillingUrl, locale });
|
|
1651
|
-
if (!cancelled) {
|
|
1652
|
-
flopayRef.current = instance;
|
|
1653
|
-
setFloPay(instance);
|
|
1654
|
-
initializedHashRef.current = cacheKey;
|
|
1655
|
-
}
|
|
1921
|
+
await bootstrapInlineSession();
|
|
1656
1922
|
} catch (err) {
|
|
1657
1923
|
if (cancelled) return;
|
|
1658
|
-
const floPayErr = err instanceof
|
|
1924
|
+
const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(err instanceof Error ? err.message : "Failed to create session", "api_error");
|
|
1659
1925
|
setLoadError(floPayErr);
|
|
1660
1926
|
}
|
|
1661
1927
|
})();
|
|
@@ -1673,7 +1939,7 @@ function FloPayCheckout({
|
|
|
1673
1939
|
const sess = result.data.session ?? null;
|
|
1674
1940
|
setSession(sess);
|
|
1675
1941
|
if (!sess) {
|
|
1676
|
-
throw new
|
|
1942
|
+
throw new FloPayError3("No session data returned", "api_error");
|
|
1677
1943
|
}
|
|
1678
1944
|
if (sess.status === "complete") {
|
|
1679
1945
|
setIsLoading(false);
|
|
@@ -1712,7 +1978,7 @@ function FloPayCheckout({
|
|
|
1712
1978
|
if (!cancelled) setIsLoading(false);
|
|
1713
1979
|
} catch (err) {
|
|
1714
1980
|
if (cancelled) return;
|
|
1715
|
-
const floPayErr = err instanceof
|
|
1981
|
+
const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
|
|
1716
1982
|
setLoadError(floPayErr);
|
|
1717
1983
|
setIsLoading(false);
|
|
1718
1984
|
}
|
|
@@ -1724,7 +1990,7 @@ function FloPayCheckout({
|
|
|
1724
1990
|
}
|
|
1725
1991
|
if (!publishableKey) publishableKey = fallbackPublishableKey;
|
|
1726
1992
|
if (!publishableKey) {
|
|
1727
|
-
throw new
|
|
1993
|
+
throw new FloPayError3(
|
|
1728
1994
|
"No publishable key found in session response. Provide a fallbackPublishableKey prop or ensure the session includes gatewayData.publishableKey.",
|
|
1729
1995
|
"validation_error"
|
|
1730
1996
|
);
|
|
@@ -1740,7 +2006,7 @@ function FloPayCheckout({
|
|
|
1740
2006
|
return () => {
|
|
1741
2007
|
cancelled = true;
|
|
1742
2008
|
};
|
|
1743
|
-
}, [resolvedSessionId, createSessionHash,
|
|
2009
|
+
}, [resolvedSessionId, createSessionHash, checkoutModeProp, deferInlineSessionUntilCardClick, bootstrapInlineSession]);
|
|
1744
2010
|
const handleConfirmCheckout = useCallback2(async () => {
|
|
1745
2011
|
if (confirmProcessing || !session) return;
|
|
1746
2012
|
setConfirmProcessing(true);
|
|
@@ -1755,17 +2021,18 @@ function FloPayCheckout({
|
|
|
1755
2021
|
setCurrentMode("full");
|
|
1756
2022
|
}
|
|
1757
2023
|
} catch (err) {
|
|
1758
|
-
const floPayErr = err instanceof
|
|
2024
|
+
const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(
|
|
1759
2025
|
err instanceof Error ? err.message : "Payment failed",
|
|
1760
2026
|
"api_error"
|
|
1761
2027
|
);
|
|
1762
2028
|
setModeError(floPayErr.message);
|
|
1763
2029
|
onError?.(floPayErr);
|
|
2030
|
+
emitDecline("card", floPayErr);
|
|
1764
2031
|
setCurrentMode("full");
|
|
1765
2032
|
} finally {
|
|
1766
2033
|
setConfirmProcessing(false);
|
|
1767
2034
|
}
|
|
1768
|
-
}, [confirmProcessing, session, processPaymentForMode, handleRedirectResult, onError]);
|
|
2035
|
+
}, [confirmProcessing, session, processPaymentForMode, handleRedirectResult, onError, emitDecline]);
|
|
1769
2036
|
const providerOptions = useMemo3(() => {
|
|
1770
2037
|
if (!unified || !session) return void 0;
|
|
1771
2038
|
const opts = {
|
|
@@ -1791,6 +2058,8 @@ function FloPayCheckout({
|
|
|
1791
2058
|
}),
|
|
1792
2059
|
[session, isLoading, loadError, currentMode]
|
|
1793
2060
|
);
|
|
2061
|
+
const shouldShowInterimButtons = Boolean(createSessionParams) && layout === "buttons" && (!flopay || !providerOptions);
|
|
2062
|
+
const shouldKeepDeferredInterimVisible = shouldShowInterimButtons && deferInlineSessionUntilCardClick;
|
|
1794
2063
|
if (isLoading) {
|
|
1795
2064
|
if (loadingNode) return /* @__PURE__ */ jsx5(Fragment3, { children: loadingNode });
|
|
1796
2065
|
if (layout === "buttons") {
|
|
@@ -1819,7 +2088,7 @@ function FloPayCheckout({
|
|
|
1819
2088
|
/* @__PURE__ */ jsx5("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
|
|
1820
2089
|
] });
|
|
1821
2090
|
}
|
|
1822
|
-
if (loadError) {
|
|
2091
|
+
if (loadError && !shouldKeepDeferredInterimVisible) {
|
|
1823
2092
|
if (errorNode) return /* @__PURE__ */ jsx5(Fragment3, { children: errorNode(loadError) });
|
|
1824
2093
|
return /* @__PURE__ */ jsx5(
|
|
1825
2094
|
"div",
|
|
@@ -1834,14 +2103,18 @@ function FloPayCheckout({
|
|
|
1834
2103
|
}
|
|
1835
2104
|
);
|
|
1836
2105
|
}
|
|
1837
|
-
if (
|
|
2106
|
+
if (shouldShowInterimButtons) {
|
|
1838
2107
|
return /* @__PURE__ */ jsx5(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ jsx5(
|
|
1839
2108
|
InterimButtonsView,
|
|
1840
2109
|
{
|
|
1841
2110
|
onButtonClick,
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2111
|
+
onCardButtonClick: deferInlineSessionUntilCardClick ? handleDeferredCardButtonClick : void 0,
|
|
2112
|
+
cardLoading: cardBootstrapPending,
|
|
2113
|
+
cardOpen: deferInlineSessionUntilCardClick ? deferredCardOpen : void 0,
|
|
2114
|
+
errorMessage: shouldKeepDeferredInterimVisible ? loadError?.message ?? null : null,
|
|
2115
|
+
showPayPal: deferInlineSessionUntilCardClick ? false : showPayPal,
|
|
2116
|
+
showApplePay: deferInlineSessionUntilCardClick ? false : showApplePay,
|
|
2117
|
+
showGooglePay: deferInlineSessionUntilCardClick ? false : showGooglePay,
|
|
1845
2118
|
buttonsTheme,
|
|
1846
2119
|
buttonsStyles,
|
|
1847
2120
|
cardButtonContent,
|
|
@@ -1927,6 +2200,7 @@ function FloPayCheckout({
|
|
|
1927
2200
|
currency: session?.currency?.toLowerCase() ?? "usd",
|
|
1928
2201
|
onComplete,
|
|
1929
2202
|
onError,
|
|
2203
|
+
onDecline,
|
|
1930
2204
|
showPayPal,
|
|
1931
2205
|
showApplePay,
|
|
1932
2206
|
showGooglePay,
|
|
@@ -1937,10 +2211,12 @@ function FloPayCheckout({
|
|
|
1937
2211
|
cardBackButtonContent,
|
|
1938
2212
|
cardTitleContent,
|
|
1939
2213
|
onButtonClick,
|
|
2214
|
+
onBeforeButtonClick,
|
|
1940
2215
|
enableAVS,
|
|
1941
2216
|
avsLayout,
|
|
1942
2217
|
country: session?.customer?.country,
|
|
1943
2218
|
zip: session?.customer?.zip,
|
|
2219
|
+
initialCardOpen: deferredCardOpen,
|
|
1944
2220
|
submitLabel,
|
|
1945
2221
|
className
|
|
1946
2222
|
}
|
|
@@ -1973,6 +2249,10 @@ function SessionInjector({
|
|
|
1973
2249
|
}
|
|
1974
2250
|
function InterimButtonsView({
|
|
1975
2251
|
onButtonClick,
|
|
2252
|
+
onCardButtonClick,
|
|
2253
|
+
cardLoading = false,
|
|
2254
|
+
cardOpen,
|
|
2255
|
+
errorMessage,
|
|
1976
2256
|
showPayPal,
|
|
1977
2257
|
showApplePay,
|
|
1978
2258
|
showGooglePay,
|
|
@@ -1983,6 +2263,12 @@ function InterimButtonsView({
|
|
|
1983
2263
|
cardTitleContent
|
|
1984
2264
|
}) {
|
|
1985
2265
|
const [showCardForm, setShowCardForm] = useState3(false);
|
|
2266
|
+
const isCardOpenControlled = typeof cardOpen === "boolean";
|
|
2267
|
+
useEffect4(() => {
|
|
2268
|
+
if (isCardOpenControlled) {
|
|
2269
|
+
setShowCardForm(cardOpen);
|
|
2270
|
+
}
|
|
2271
|
+
}, [cardOpen, isCardOpenControlled]);
|
|
1986
2272
|
const bStyles = useMemo3(() => {
|
|
1987
2273
|
const base = resolveButtonsLayoutTheme2(buttonsTheme);
|
|
1988
2274
|
if (!stylesOverride) return base;
|
|
@@ -2020,20 +2306,26 @@ function InterimButtonsView({
|
|
|
2020
2306
|
"button",
|
|
2021
2307
|
{
|
|
2022
2308
|
type: "button",
|
|
2023
|
-
onClick: () =>
|
|
2309
|
+
onClick: () => {
|
|
2310
|
+
if (!isCardOpenControlled) {
|
|
2311
|
+
setShowCardForm(false);
|
|
2312
|
+
}
|
|
2313
|
+
},
|
|
2024
2314
|
"aria-label": "Back to payment methods",
|
|
2315
|
+
disabled: isCardOpenControlled || cardLoading,
|
|
2025
2316
|
style: {
|
|
2026
2317
|
display: "inline-flex",
|
|
2027
2318
|
alignItems: "center",
|
|
2028
2319
|
gap: hideBackButtonLabel ? 0 : "0.5rem",
|
|
2029
2320
|
background: "none",
|
|
2030
2321
|
border: "none",
|
|
2031
|
-
cursor: "pointer",
|
|
2032
2322
|
color: "#4b5563",
|
|
2033
2323
|
fontSize: "0.85rem",
|
|
2034
2324
|
fontWeight: 500,
|
|
2035
2325
|
padding: 0,
|
|
2036
2326
|
flexShrink: 0,
|
|
2327
|
+
opacity: isCardOpenControlled || cardLoading ? 0.6 : 1,
|
|
2328
|
+
cursor: isCardOpenControlled || cardLoading ? "not-allowed" : "pointer",
|
|
2037
2329
|
...bStyles.backButton
|
|
2038
2330
|
},
|
|
2039
2331
|
children: [
|
|
@@ -2093,10 +2385,16 @@ function InterimButtonsView({
|
|
|
2093
2385
|
"button",
|
|
2094
2386
|
{
|
|
2095
2387
|
type: "button",
|
|
2096
|
-
onClick: () => {
|
|
2388
|
+
onClick: async () => {
|
|
2389
|
+
if (cardLoading) return;
|
|
2390
|
+
if (onCardButtonClick) {
|
|
2391
|
+
await onCardButtonClick();
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2097
2394
|
onButtonClick?.("card");
|
|
2098
2395
|
setShowCardForm(true);
|
|
2099
2396
|
},
|
|
2397
|
+
disabled: cardLoading,
|
|
2100
2398
|
style: {
|
|
2101
2399
|
width: "100%",
|
|
2102
2400
|
padding: "0.9rem 1rem",
|
|
@@ -2106,7 +2404,7 @@ function InterimButtonsView({
|
|
|
2106
2404
|
borderRadius: "8px",
|
|
2107
2405
|
fontSize: bStyles.cardButtonFontSize ?? "0.95rem",
|
|
2108
2406
|
fontWeight: 600,
|
|
2109
|
-
cursor: "pointer",
|
|
2407
|
+
cursor: cardLoading ? "not-allowed" : "pointer",
|
|
2110
2408
|
display: "flex",
|
|
2111
2409
|
alignItems: "center",
|
|
2112
2410
|
justifyContent: "center",
|
|
@@ -2114,6 +2412,7 @@ function InterimButtonsView({
|
|
|
2114
2412
|
boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
|
|
2115
2413
|
transition: "transform 0.1s",
|
|
2116
2414
|
position: "relative",
|
|
2415
|
+
opacity: cardLoading ? 0.6 : 1,
|
|
2117
2416
|
...bStyles.cardButton
|
|
2118
2417
|
},
|
|
2119
2418
|
onMouseDown: (e) => {
|
|
@@ -2125,12 +2424,29 @@ function InterimButtonsView({
|
|
|
2125
2424
|
children: /* @__PURE__ */ jsx5(CardButtonContentSlot, { content: cardButtonContent })
|
|
2126
2425
|
}
|
|
2127
2426
|
),
|
|
2427
|
+
errorMessage && /* @__PURE__ */ jsxs3("div", { style: {
|
|
2428
|
+
margin: "0.25rem 0",
|
|
2429
|
+
padding: "0.625rem 0.875rem",
|
|
2430
|
+
background: "#FEF2F2",
|
|
2431
|
+
border: "1px solid #FECACA",
|
|
2432
|
+
borderRadius: "8px",
|
|
2433
|
+
color: "#991B1B",
|
|
2434
|
+
fontSize: "0.85rem",
|
|
2435
|
+
fontWeight: 600,
|
|
2436
|
+
display: "flex",
|
|
2437
|
+
alignItems: "center",
|
|
2438
|
+
gap: "0.5rem",
|
|
2439
|
+
...bStyles.errorBanner
|
|
2440
|
+
}, children: [
|
|
2441
|
+
/* @__PURE__ */ jsx5("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx5("path", { d: "M12 9v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z", stroke: "#DC2626", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }),
|
|
2442
|
+
errorMessage
|
|
2443
|
+
] }),
|
|
2128
2444
|
/* @__PURE__ */ jsx5("style", { children: `@keyframes flopay-interim-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }` })
|
|
2129
2445
|
] });
|
|
2130
2446
|
}
|
|
2131
2447
|
|
|
2132
2448
|
// src/checkout-form.tsx
|
|
2133
|
-
import { FloPayError as
|
|
2449
|
+
import { FloPayError as FloPayError4 } from "@flopay/shared";
|
|
2134
2450
|
import { forwardRef as forwardRef2, useCallback as useCallback3, useEffect as useEffect5, useImperativeHandle as useImperativeHandle2, useState as useState4 } from "react";
|
|
2135
2451
|
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2136
2452
|
var WALLET_RESUME_KEY2 = "flopay_wallet_resume";
|
|
@@ -2146,6 +2462,7 @@ function CheckoutFormInner({
|
|
|
2146
2462
|
userId,
|
|
2147
2463
|
onComplete,
|
|
2148
2464
|
onError,
|
|
2465
|
+
onDecline,
|
|
2149
2466
|
onTokenizedBody,
|
|
2150
2467
|
layout = "auto",
|
|
2151
2468
|
submitLabel = "Pay",
|
|
@@ -2177,6 +2494,12 @@ function CheckoutFormInner({
|
|
|
2177
2494
|
},
|
|
2178
2495
|
[onErrorChange]
|
|
2179
2496
|
);
|
|
2497
|
+
const emitDecline = useCallback3(
|
|
2498
|
+
(input, overrides) => {
|
|
2499
|
+
onDecline?.(buildDeclineEvent("card", input, overrides));
|
|
2500
|
+
},
|
|
2501
|
+
[onDecline]
|
|
2502
|
+
);
|
|
2180
2503
|
const processPaymentInternal = useCallback3(
|
|
2181
2504
|
async (tokenizedBody) => {
|
|
2182
2505
|
setProcessing(true);
|
|
@@ -2223,6 +2546,7 @@ function CheckoutFormInner({
|
|
|
2223
2546
|
if (result.error) {
|
|
2224
2547
|
updateError(result.error.message);
|
|
2225
2548
|
onError?.(result.error);
|
|
2549
|
+
emitDecline(result.error);
|
|
2226
2550
|
return;
|
|
2227
2551
|
}
|
|
2228
2552
|
if (result.status === "succeeded" || result.status === "processing") {
|
|
@@ -2257,13 +2581,17 @@ function CheckoutFormInner({
|
|
|
2257
2581
|
}
|
|
2258
2582
|
const errorMessage = json?.message ?? "Payment failed. Please try again.";
|
|
2259
2583
|
updateError(errorMessage);
|
|
2584
|
+
emitDecline(errorMessage, {
|
|
2585
|
+
code: json?.code,
|
|
2586
|
+
declineCode: json?.declineCode ?? json?.gatewayDeclineReason
|
|
2587
|
+
});
|
|
2260
2588
|
} catch (err) {
|
|
2261
2589
|
updateError(err instanceof Error ? err.message : "An unexpected error occurred");
|
|
2262
2590
|
} finally {
|
|
2263
2591
|
setProcessing(false);
|
|
2264
2592
|
}
|
|
2265
2593
|
},
|
|
2266
|
-
[baseUrl, sessionId, userId, email, firstName, lastName, chv, flopay, onComplete, onError, updateError]
|
|
2594
|
+
[baseUrl, sessionId, userId, email, firstName, lastName, chv, flopay, onComplete, onError, updateError, emitDecline]
|
|
2267
2595
|
);
|
|
2268
2596
|
const dispatchTokenizedBody = useCallback3(
|
|
2269
2597
|
(tokenizedBody) => {
|
|
@@ -2287,6 +2615,7 @@ function CheckoutFormInner({
|
|
|
2287
2615
|
if (result.error) {
|
|
2288
2616
|
updateError(result.error.message);
|
|
2289
2617
|
onError?.(result.error);
|
|
2618
|
+
emitDecline(result.error);
|
|
2290
2619
|
} else if (result.status === "succeeded" || result.status === "processing") {
|
|
2291
2620
|
dispatchTokenizedBody({
|
|
2292
2621
|
id: result.paymentIntentId,
|
|
@@ -2300,7 +2629,7 @@ function CheckoutFormInner({
|
|
|
2300
2629
|
setIs3DSActive(false);
|
|
2301
2630
|
}
|
|
2302
2631
|
}
|
|
2303
|
-
}), [flopay, dispatchTokenizedBody, onError, updateError]);
|
|
2632
|
+
}), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
|
|
2304
2633
|
useEffect5(() => {
|
|
2305
2634
|
if (typeof window === "undefined") return;
|
|
2306
2635
|
const stored = localStorage.getItem(WALLET_RESUME_KEY2);
|
|
@@ -2338,7 +2667,7 @@ function CheckoutFormInner({
|
|
|
2338
2667
|
return;
|
|
2339
2668
|
}
|
|
2340
2669
|
if (!sessionId || !email) {
|
|
2341
|
-
throw new
|
|
2670
|
+
throw new FloPayError4("Missing sessionId or email", "validation_error");
|
|
2342
2671
|
}
|
|
2343
2672
|
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
2344
2673
|
method: "POST",
|
|
@@ -2350,16 +2679,18 @@ function CheckoutFormInner({
|
|
|
2350
2679
|
isPaypal: false
|
|
2351
2680
|
})
|
|
2352
2681
|
});
|
|
2353
|
-
if (!intentResponse.ok) throw new
|
|
2682
|
+
if (!intentResponse.ok) throw new FloPayError4("Failed to create payment intent", "api_error");
|
|
2354
2683
|
const intentJson = await intentResponse.json();
|
|
2355
2684
|
const intentClientSecret = intentJson.data?.id;
|
|
2356
|
-
if (!intentClientSecret) throw new
|
|
2685
|
+
if (!intentClientSecret) throw new FloPayError4("No client_secret in payment intent response", "api_error");
|
|
2357
2686
|
const confirmResult = await flopay.confirmCardPayment({
|
|
2358
2687
|
clientSecret: intentClientSecret,
|
|
2359
2688
|
paymentMethodId: pmResult.paymentMethodId
|
|
2360
2689
|
});
|
|
2361
2690
|
if (confirmResult.error) {
|
|
2362
2691
|
updateError(confirmResult.error.message);
|
|
2692
|
+
onError?.(confirmResult.error);
|
|
2693
|
+
emitDecline(confirmResult.error);
|
|
2363
2694
|
return;
|
|
2364
2695
|
}
|
|
2365
2696
|
dispatchTokenizedBody({
|
|
@@ -2376,7 +2707,7 @@ function CheckoutFormInner({
|
|
|
2376
2707
|
}
|
|
2377
2708
|
}
|
|
2378
2709
|
},
|
|
2379
|
-
[flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError]
|
|
2710
|
+
[flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError, emitDecline]
|
|
2380
2711
|
);
|
|
2381
2712
|
const isReady = flopay !== null && elements !== null;
|
|
2382
2713
|
return /* @__PURE__ */ jsxs4("form", { onSubmit: handleSubmit, className, style: { position: "relative" }, children: [
|
|
@@ -2408,7 +2739,7 @@ function CheckoutFormInner({
|
|
|
2408
2739
|
}
|
|
2409
2740
|
|
|
2410
2741
|
// src/paypal-button.tsx
|
|
2411
|
-
import { FloPayError as
|
|
2742
|
+
import { FloPayError as FloPayError5 } from "@flopay/shared";
|
|
2412
2743
|
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef4, useState as useState5 } from "react";
|
|
2413
2744
|
import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2414
2745
|
function PayPalButton({
|
|
@@ -2528,7 +2859,7 @@ function PayPalButton({
|
|
|
2528
2859
|
setSubmitting(true);
|
|
2529
2860
|
onErrorChange?.(null);
|
|
2530
2861
|
if (!sessionId || !email) {
|
|
2531
|
-
throw new
|
|
2862
|
+
throw new FloPayError5("Missing sessionId or email for PayPal payment", "validation_error");
|
|
2532
2863
|
}
|
|
2533
2864
|
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
2534
2865
|
method: "POST",
|
|
@@ -2540,10 +2871,10 @@ function PayPalButton({
|
|
|
2540
2871
|
isPaypal: "true"
|
|
2541
2872
|
})
|
|
2542
2873
|
});
|
|
2543
|
-
if (!intentResponse.ok) throw new
|
|
2874
|
+
if (!intentResponse.ok) throw new FloPayError5("Failed to create payment intent", "api_error");
|
|
2544
2875
|
const intentJson = await intentResponse.json();
|
|
2545
2876
|
const intentClientSecret = intentJson.data?.id;
|
|
2546
|
-
if (!intentClientSecret) throw new
|
|
2877
|
+
if (!intentClientSecret) throw new FloPayError5("No client_secret in response", "api_error");
|
|
2547
2878
|
const result = await flopay.confirmPayment({
|
|
2548
2879
|
clientSecret: intentClientSecret,
|
|
2549
2880
|
returnUrl: window.location.href
|