@flopay/react 0.3.11 → 0.3.13
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 +542 -197
- 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 +533 -188
- 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: {
|
|
@@ -584,7 +718,7 @@ function WalletButtonInner({
|
|
|
584
718
|
amazonPay: "never",
|
|
585
719
|
klarna: "never"
|
|
586
720
|
},
|
|
587
|
-
layout: { overflow: "never" }
|
|
721
|
+
layout: { maxColumns: 1, overflow: "never" }
|
|
588
722
|
}
|
|
589
723
|
}
|
|
590
724
|
) }),
|
|
@@ -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,41 @@ 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 [createSessionPatchBaseHash, setCreateSessionPatchBaseHash] = useState3("");
|
|
1618
|
+
const [cardBootstrapPending, setCardBootstrapPending] = useState3(false);
|
|
1619
|
+
const [deferredCardOpen, setDeferredCardOpen] = useState3(false);
|
|
1412
1620
|
const autoCheckoutAttempted = useRef3(false);
|
|
1413
1621
|
const onCompleteRef = useRef3(onComplete);
|
|
1414
1622
|
onCompleteRef.current = onComplete;
|
|
1415
1623
|
const onErrorRef = useRef3(onError);
|
|
1416
1624
|
onErrorRef.current = onError;
|
|
1625
|
+
const onDeclineRef = useRef3(onDecline);
|
|
1626
|
+
onDeclineRef.current = onDecline;
|
|
1417
1627
|
const onSessionCompletedRef = useRef3(onSessionCompleted);
|
|
1418
1628
|
onSessionCompletedRef.current = onSessionCompleted;
|
|
1629
|
+
const baseCreateSessionHash = useMemo3(
|
|
1630
|
+
() => createSessionParams ? hashCreateParams(createSessionParams) : "",
|
|
1631
|
+
[createSessionParams]
|
|
1632
|
+
);
|
|
1633
|
+
const activeCreateSessionPatch = useMemo3(
|
|
1634
|
+
() => createSessionPatchBaseHash === baseCreateSessionHash ? createSessionPatch : void 0,
|
|
1635
|
+
[createSessionPatch, createSessionPatchBaseHash, baseCreateSessionHash]
|
|
1636
|
+
);
|
|
1637
|
+
const effectiveCreateSession = useMemo3(
|
|
1638
|
+
() => createSessionParams ? mergeInlineSessionPatch(createSessionParams, activeCreateSessionPatch) : void 0,
|
|
1639
|
+
[createSessionParams, activeCreateSessionPatch]
|
|
1640
|
+
);
|
|
1641
|
+
useEffect4(() => {
|
|
1642
|
+
setCreateSessionPatch(void 0);
|
|
1643
|
+
setCreateSessionPatchBaseHash(baseCreateSessionHash);
|
|
1644
|
+
}, [baseCreateSessionHash]);
|
|
1645
|
+
const emitDecline = useCallback2(
|
|
1646
|
+
(method, input, overrides) => {
|
|
1647
|
+
onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
|
|
1648
|
+
},
|
|
1649
|
+
[]
|
|
1650
|
+
);
|
|
1419
1651
|
const processPaymentForMode = useCallback2(
|
|
1420
1652
|
async (sess) => {
|
|
1421
1653
|
const baseUrl = resolvedBillingUrl.replace(/\/+$/, "");
|
|
@@ -1455,9 +1687,13 @@ function FloPayCheckout({
|
|
|
1455
1687
|
// No client secret available
|
|
1456
1688
|
};
|
|
1457
1689
|
}
|
|
1458
|
-
throw new
|
|
1690
|
+
throw new FloPayError3(
|
|
1459
1691
|
json?.message ?? "Payment failed. Please try again.",
|
|
1460
|
-
"api_error"
|
|
1692
|
+
"api_error",
|
|
1693
|
+
{
|
|
1694
|
+
code: json?.code ?? json?.gatewayErrorCode,
|
|
1695
|
+
declineCode: json?.declineCode ?? json?.gatewayDeclineReason
|
|
1696
|
+
}
|
|
1461
1697
|
);
|
|
1462
1698
|
},
|
|
1463
1699
|
[resolvedBillingUrl, resolvedSessionId]
|
|
@@ -1476,6 +1712,9 @@ function FloPayCheckout({
|
|
|
1476
1712
|
});
|
|
1477
1713
|
if (nextActionError) {
|
|
1478
1714
|
setModeError(nextActionError.message ?? "3DS authentication failed.");
|
|
1715
|
+
emitDecline("card", nextActionError.message ?? "3DS authentication failed.", {
|
|
1716
|
+
code: nextActionError.code
|
|
1717
|
+
});
|
|
1479
1718
|
return false;
|
|
1480
1719
|
}
|
|
1481
1720
|
if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
|
|
@@ -1498,6 +1737,9 @@ function FloPayCheckout({
|
|
|
1498
1737
|
});
|
|
1499
1738
|
if (error) {
|
|
1500
1739
|
setModeError(error.message ?? "PayPal authorization failed.");
|
|
1740
|
+
emitDecline("paypal", error.message ?? "PayPal authorization failed.", {
|
|
1741
|
+
code: error.code
|
|
1742
|
+
});
|
|
1501
1743
|
return false;
|
|
1502
1744
|
}
|
|
1503
1745
|
onCompleteRef.current?.({ status: "succeeded" });
|
|
@@ -1505,16 +1747,24 @@ function FloPayCheckout({
|
|
|
1505
1747
|
}
|
|
1506
1748
|
return false;
|
|
1507
1749
|
},
|
|
1508
|
-
[resolvedBillingUrl, resolvedSessionId]
|
|
1750
|
+
[resolvedBillingUrl, resolvedSessionId, emitDecline]
|
|
1509
1751
|
);
|
|
1510
1752
|
const inflightRef = useRef3(/* @__PURE__ */ new Map());
|
|
1511
1753
|
const initializedHashRef = useRef3(null);
|
|
1754
|
+
const deferInlineSessionUntilCardClick = Boolean(
|
|
1755
|
+
effectiveCreateSession && !children && layout === "buttons" && onBeforeButtonClick && (effectiveCreateSession.checkoutMode ?? checkoutModeProp ?? "full") === "full"
|
|
1756
|
+
);
|
|
1512
1757
|
function hashCreateParams(params) {
|
|
1513
1758
|
const key = JSON.stringify({
|
|
1514
1759
|
c: params?.clientId,
|
|
1760
|
+
successUrl: params?.successUrl,
|
|
1761
|
+
cancelUrl: params?.cancelUrl,
|
|
1515
1762
|
i: params?.items?.map((x) => `${x.providerItemId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
|
|
1516
1763
|
s: params?.subscriptions?.map((x) => `${x.providerPlanId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
|
|
1517
|
-
|
|
1764
|
+
account: params?.account,
|
|
1765
|
+
couponCodes: params?.couponCodes,
|
|
1766
|
+
tagsData: params?.tagsData,
|
|
1767
|
+
utmMetadata: params?.utmMetadata,
|
|
1518
1768
|
m: params?.checkoutMode ?? "full"
|
|
1519
1769
|
});
|
|
1520
1770
|
let h = 0;
|
|
@@ -1524,138 +1774,169 @@ function FloPayCheckout({
|
|
|
1524
1774
|
return `flopay_session_${Math.abs(h).toString(36)}`;
|
|
1525
1775
|
}
|
|
1526
1776
|
const createSessionHash = useMemo3(
|
|
1527
|
-
() =>
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1777
|
+
() => effectiveCreateSession ? hashCreateParams(effectiveCreateSession) : "",
|
|
1778
|
+
[effectiveCreateSession]
|
|
1779
|
+
);
|
|
1780
|
+
const createSessionParamsRef = useRef3(effectiveCreateSession);
|
|
1781
|
+
createSessionParamsRef.current = effectiveCreateSession;
|
|
1782
|
+
useEffect4(() => {
|
|
1783
|
+
setResolvedSessionId(sessionIdProp ?? "");
|
|
1784
|
+
}, [sessionIdProp]);
|
|
1785
|
+
async function resolveInlineSession(params, cacheKey) {
|
|
1786
|
+
ensureInlineSessionReady(params);
|
|
1787
|
+
const api = new PaymentAPI(resolvedBillingUrl);
|
|
1788
|
+
let sid = typeof window !== "undefined" ? window.sessionStorage.getItem(cacheKey) : null;
|
|
1789
|
+
let realResult = null;
|
|
1790
|
+
if (sid) {
|
|
1791
|
+
try {
|
|
1792
|
+
realResult = await api.getUnifiedCheckoutSession(sid);
|
|
1793
|
+
if (realResult.data.session?.status === "complete") {
|
|
1794
|
+
if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
|
|
1795
|
+
sid = null;
|
|
1796
|
+
realResult = null;
|
|
1797
|
+
}
|
|
1798
|
+
} catch {
|
|
1799
|
+
if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
|
|
1800
|
+
sid = null;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (!sid) {
|
|
1804
|
+
realResult = await api.createAndFetchSession(params);
|
|
1805
|
+
sid = realResult.data.session?.id ?? "";
|
|
1806
|
+
if (sid && typeof window !== "undefined") {
|
|
1807
|
+
window.sessionStorage.setItem(cacheKey, sid);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
return { sid: sid ?? "", result: realResult };
|
|
1811
|
+
}
|
|
1812
|
+
const bootstrapInlineSession = useCallback2(
|
|
1813
|
+
async (patch) => {
|
|
1814
|
+
const baseParams = createSessionParamsRef.current;
|
|
1815
|
+
if (!baseParams) {
|
|
1816
|
+
throw new FloPayError3("createSession is required to bootstrap checkout.", "validation_error");
|
|
1817
|
+
}
|
|
1818
|
+
const mergedParams = mergeInlineSessionPatch(baseParams, patch);
|
|
1819
|
+
const cacheKey = hashCreateParams(mergedParams);
|
|
1820
|
+
let promise = inflightRef.current.get(cacheKey);
|
|
1821
|
+
if (!promise) {
|
|
1822
|
+
promise = resolveInlineSession(mergedParams, cacheKey);
|
|
1823
|
+
inflightRef.current.set(cacheKey, promise);
|
|
1824
|
+
}
|
|
1825
|
+
let resolved;
|
|
1826
|
+
try {
|
|
1827
|
+
resolved = await promise;
|
|
1828
|
+
} finally {
|
|
1829
|
+
inflightRef.current.delete(cacheKey);
|
|
1830
|
+
}
|
|
1831
|
+
if (patch) {
|
|
1832
|
+
setCreateSessionPatchBaseHash(baseCreateSessionHash);
|
|
1833
|
+
setCreateSessionPatch((prev) => mergeInlineSessionPatches(prev, patch));
|
|
1834
|
+
}
|
|
1835
|
+
const { sid, result: realResult } = resolved;
|
|
1836
|
+
setUnified(realResult);
|
|
1837
|
+
if (realResult.data.session) {
|
|
1838
|
+
setSession(realResult.data.session);
|
|
1839
|
+
}
|
|
1840
|
+
if (sid) {
|
|
1841
|
+
setResolvedSessionId(sid);
|
|
1842
|
+
}
|
|
1843
|
+
let publishableKey;
|
|
1844
|
+
if (realResult.provider === "stripe") {
|
|
1845
|
+
publishableKey = realResult.data.stripe?.publishableKey;
|
|
1846
|
+
}
|
|
1847
|
+
if (!publishableKey) publishableKey = fallbackPublishableKey;
|
|
1848
|
+
if (!publishableKey) {
|
|
1849
|
+
throw new FloPayError3(
|
|
1850
|
+
"No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
|
|
1851
|
+
"validation_error"
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
const instance = await loadFloPay(publishableKey, {
|
|
1855
|
+
billingApiUrl: resolvedBillingUrl,
|
|
1856
|
+
locale
|
|
1857
|
+
});
|
|
1858
|
+
flopayRef.current = instance;
|
|
1859
|
+
setFloPay(instance);
|
|
1860
|
+
initializedHashRef.current = cacheKey;
|
|
1861
|
+
setIsLoading(false);
|
|
1862
|
+
return resolved;
|
|
1863
|
+
},
|
|
1864
|
+
[fallbackPublishableKey, locale, resolvedBillingUrl]
|
|
1536
1865
|
);
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1866
|
+
const handleDeferredCardButtonClick = useCallback2(async () => {
|
|
1867
|
+
if (cardBootstrapPending) return;
|
|
1868
|
+
setLoadError(null);
|
|
1869
|
+
setCardBootstrapPending(true);
|
|
1870
|
+
try {
|
|
1871
|
+
const beforeClickResult = await onBeforeButtonClick?.({
|
|
1872
|
+
method: "card",
|
|
1873
|
+
createSession: effectiveCreateSession
|
|
1874
|
+
});
|
|
1875
|
+
if (beforeClickResult === false) {
|
|
1876
|
+
setDeferredCardOpen(false);
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const patch = beforeClickResult && typeof beforeClickResult === "object" ? beforeClickResult : void 0;
|
|
1880
|
+
const baseParams = createSessionParamsRef.current;
|
|
1881
|
+
if (!baseParams) {
|
|
1882
|
+
throw new FloPayError3(
|
|
1883
|
+
"createSession is required to bootstrap checkout.",
|
|
1884
|
+
"validation_error"
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
ensureInlineSessionReady(mergeInlineSessionPatch(baseParams, patch));
|
|
1888
|
+
setDeferredCardOpen(true);
|
|
1889
|
+
onButtonClick?.("card");
|
|
1890
|
+
await bootstrapInlineSession(patch);
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
setDeferredCardOpen(false);
|
|
1893
|
+
const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(
|
|
1894
|
+
err instanceof Error ? err.message : "Failed to start card checkout.",
|
|
1895
|
+
"api_error"
|
|
1896
|
+
);
|
|
1897
|
+
setLoadError(floPayErr);
|
|
1898
|
+
onErrorRef.current?.(floPayErr);
|
|
1899
|
+
} finally {
|
|
1900
|
+
setCardBootstrapPending(false);
|
|
1901
|
+
}
|
|
1902
|
+
}, [
|
|
1903
|
+
bootstrapInlineSession,
|
|
1904
|
+
baseCreateSessionHash,
|
|
1905
|
+
cardBootstrapPending,
|
|
1906
|
+
effectiveCreateSession,
|
|
1907
|
+
onButtonClick,
|
|
1908
|
+
onBeforeButtonClick
|
|
1909
|
+
]);
|
|
1539
1910
|
useEffect4(() => {
|
|
1540
1911
|
let cancelled = false;
|
|
1541
1912
|
setLoadError(null);
|
|
1542
1913
|
if (createSessionHash) {
|
|
1914
|
+
if (deferInlineSessionUntilCardClick) {
|
|
1915
|
+
if (initializedHashRef.current !== createSessionHash) {
|
|
1916
|
+
setSession(null);
|
|
1917
|
+
setUnified(null);
|
|
1918
|
+
setFloPay(null);
|
|
1919
|
+
setResolvedSessionId("");
|
|
1920
|
+
setDeferredCardOpen(false);
|
|
1921
|
+
flopayRef.current = null;
|
|
1922
|
+
}
|
|
1923
|
+
setCurrentMode(createSessionParamsRef.current?.checkoutMode ?? checkoutModeProp ?? "full");
|
|
1924
|
+
setIsLoading(false);
|
|
1925
|
+
return () => {
|
|
1926
|
+
cancelled = true;
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1543
1929
|
if (initializedHashRef.current === createSessionHash) return;
|
|
1544
1930
|
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);
|
|
1931
|
+
setSession(buildSyntheticSession(params, checkoutModeProp));
|
|
1588
1932
|
setCurrentMode(params.checkoutMode ?? checkoutModeProp ?? "full");
|
|
1589
1933
|
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
1934
|
(async () => {
|
|
1618
1935
|
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
|
-
}
|
|
1936
|
+
await bootstrapInlineSession();
|
|
1656
1937
|
} catch (err) {
|
|
1657
1938
|
if (cancelled) return;
|
|
1658
|
-
const floPayErr = err instanceof
|
|
1939
|
+
const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(err instanceof Error ? err.message : "Failed to create session", "api_error");
|
|
1659
1940
|
setLoadError(floPayErr);
|
|
1660
1941
|
}
|
|
1661
1942
|
})();
|
|
@@ -1673,7 +1954,7 @@ function FloPayCheckout({
|
|
|
1673
1954
|
const sess = result.data.session ?? null;
|
|
1674
1955
|
setSession(sess);
|
|
1675
1956
|
if (!sess) {
|
|
1676
|
-
throw new
|
|
1957
|
+
throw new FloPayError3("No session data returned", "api_error");
|
|
1677
1958
|
}
|
|
1678
1959
|
if (sess.status === "complete") {
|
|
1679
1960
|
setIsLoading(false);
|
|
@@ -1712,7 +1993,7 @@ function FloPayCheckout({
|
|
|
1712
1993
|
if (!cancelled) setIsLoading(false);
|
|
1713
1994
|
} catch (err) {
|
|
1714
1995
|
if (cancelled) return;
|
|
1715
|
-
const floPayErr = err instanceof
|
|
1996
|
+
const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
|
|
1716
1997
|
setLoadError(floPayErr);
|
|
1717
1998
|
setIsLoading(false);
|
|
1718
1999
|
}
|
|
@@ -1724,7 +2005,7 @@ function FloPayCheckout({
|
|
|
1724
2005
|
}
|
|
1725
2006
|
if (!publishableKey) publishableKey = fallbackPublishableKey;
|
|
1726
2007
|
if (!publishableKey) {
|
|
1727
|
-
throw new
|
|
2008
|
+
throw new FloPayError3(
|
|
1728
2009
|
"No publishable key found in session response. Provide a fallbackPublishableKey prop or ensure the session includes gatewayData.publishableKey.",
|
|
1729
2010
|
"validation_error"
|
|
1730
2011
|
);
|
|
@@ -1740,7 +2021,7 @@ function FloPayCheckout({
|
|
|
1740
2021
|
return () => {
|
|
1741
2022
|
cancelled = true;
|
|
1742
2023
|
};
|
|
1743
|
-
}, [resolvedSessionId, createSessionHash,
|
|
2024
|
+
}, [resolvedSessionId, createSessionHash, checkoutModeProp, deferInlineSessionUntilCardClick, bootstrapInlineSession]);
|
|
1744
2025
|
const handleConfirmCheckout = useCallback2(async () => {
|
|
1745
2026
|
if (confirmProcessing || !session) return;
|
|
1746
2027
|
setConfirmProcessing(true);
|
|
@@ -1755,17 +2036,18 @@ function FloPayCheckout({
|
|
|
1755
2036
|
setCurrentMode("full");
|
|
1756
2037
|
}
|
|
1757
2038
|
} catch (err) {
|
|
1758
|
-
const floPayErr = err instanceof
|
|
2039
|
+
const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(
|
|
1759
2040
|
err instanceof Error ? err.message : "Payment failed",
|
|
1760
2041
|
"api_error"
|
|
1761
2042
|
);
|
|
1762
2043
|
setModeError(floPayErr.message);
|
|
1763
2044
|
onError?.(floPayErr);
|
|
2045
|
+
emitDecline("card", floPayErr);
|
|
1764
2046
|
setCurrentMode("full");
|
|
1765
2047
|
} finally {
|
|
1766
2048
|
setConfirmProcessing(false);
|
|
1767
2049
|
}
|
|
1768
|
-
}, [confirmProcessing, session, processPaymentForMode, handleRedirectResult, onError]);
|
|
2050
|
+
}, [confirmProcessing, session, processPaymentForMode, handleRedirectResult, onError, emitDecline]);
|
|
1769
2051
|
const providerOptions = useMemo3(() => {
|
|
1770
2052
|
if (!unified || !session) return void 0;
|
|
1771
2053
|
const opts = {
|
|
@@ -1791,6 +2073,8 @@ function FloPayCheckout({
|
|
|
1791
2073
|
}),
|
|
1792
2074
|
[session, isLoading, loadError, currentMode]
|
|
1793
2075
|
);
|
|
2076
|
+
const shouldShowInterimButtons = Boolean(createSessionParams) && layout === "buttons" && (!flopay || !providerOptions);
|
|
2077
|
+
const shouldKeepDeferredInterimVisible = shouldShowInterimButtons && deferInlineSessionUntilCardClick;
|
|
1794
2078
|
if (isLoading) {
|
|
1795
2079
|
if (loadingNode) return /* @__PURE__ */ jsx5(Fragment3, { children: loadingNode });
|
|
1796
2080
|
if (layout === "buttons") {
|
|
@@ -1819,7 +2103,7 @@ function FloPayCheckout({
|
|
|
1819
2103
|
/* @__PURE__ */ jsx5("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
|
|
1820
2104
|
] });
|
|
1821
2105
|
}
|
|
1822
|
-
if (loadError) {
|
|
2106
|
+
if (loadError && !shouldKeepDeferredInterimVisible) {
|
|
1823
2107
|
if (errorNode) return /* @__PURE__ */ jsx5(Fragment3, { children: errorNode(loadError) });
|
|
1824
2108
|
return /* @__PURE__ */ jsx5(
|
|
1825
2109
|
"div",
|
|
@@ -1834,14 +2118,18 @@ function FloPayCheckout({
|
|
|
1834
2118
|
}
|
|
1835
2119
|
);
|
|
1836
2120
|
}
|
|
1837
|
-
if (
|
|
2121
|
+
if (shouldShowInterimButtons) {
|
|
1838
2122
|
return /* @__PURE__ */ jsx5(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ jsx5(
|
|
1839
2123
|
InterimButtonsView,
|
|
1840
2124
|
{
|
|
1841
2125
|
onButtonClick,
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2126
|
+
onCardButtonClick: deferInlineSessionUntilCardClick ? handleDeferredCardButtonClick : void 0,
|
|
2127
|
+
cardLoading: cardBootstrapPending,
|
|
2128
|
+
cardOpen: deferInlineSessionUntilCardClick ? deferredCardOpen : void 0,
|
|
2129
|
+
errorMessage: shouldKeepDeferredInterimVisible ? loadError?.message ?? null : null,
|
|
2130
|
+
showPayPal: deferInlineSessionUntilCardClick ? false : showPayPal,
|
|
2131
|
+
showApplePay: deferInlineSessionUntilCardClick ? false : showApplePay,
|
|
2132
|
+
showGooglePay: deferInlineSessionUntilCardClick ? false : showGooglePay,
|
|
1845
2133
|
buttonsTheme,
|
|
1846
2134
|
buttonsStyles,
|
|
1847
2135
|
cardButtonContent,
|
|
@@ -1927,6 +2215,7 @@ function FloPayCheckout({
|
|
|
1927
2215
|
currency: session?.currency?.toLowerCase() ?? "usd",
|
|
1928
2216
|
onComplete,
|
|
1929
2217
|
onError,
|
|
2218
|
+
onDecline,
|
|
1930
2219
|
showPayPal,
|
|
1931
2220
|
showApplePay,
|
|
1932
2221
|
showGooglePay,
|
|
@@ -1937,10 +2226,11 @@ function FloPayCheckout({
|
|
|
1937
2226
|
cardBackButtonContent,
|
|
1938
2227
|
cardTitleContent,
|
|
1939
2228
|
onButtonClick,
|
|
2229
|
+
onBeforeButtonClick,
|
|
1940
2230
|
enableAVS,
|
|
1941
2231
|
avsLayout,
|
|
1942
2232
|
country: session?.customer?.country,
|
|
1943
|
-
|
|
2233
|
+
initialCardOpen: deferredCardOpen,
|
|
1944
2234
|
submitLabel,
|
|
1945
2235
|
className
|
|
1946
2236
|
}
|
|
@@ -1973,6 +2263,10 @@ function SessionInjector({
|
|
|
1973
2263
|
}
|
|
1974
2264
|
function InterimButtonsView({
|
|
1975
2265
|
onButtonClick,
|
|
2266
|
+
onCardButtonClick,
|
|
2267
|
+
cardLoading = false,
|
|
2268
|
+
cardOpen,
|
|
2269
|
+
errorMessage,
|
|
1976
2270
|
showPayPal,
|
|
1977
2271
|
showApplePay,
|
|
1978
2272
|
showGooglePay,
|
|
@@ -1983,6 +2277,12 @@ function InterimButtonsView({
|
|
|
1983
2277
|
cardTitleContent
|
|
1984
2278
|
}) {
|
|
1985
2279
|
const [showCardForm, setShowCardForm] = useState3(false);
|
|
2280
|
+
const isCardOpenControlled = typeof cardOpen === "boolean";
|
|
2281
|
+
useEffect4(() => {
|
|
2282
|
+
if (isCardOpenControlled) {
|
|
2283
|
+
setShowCardForm(cardOpen);
|
|
2284
|
+
}
|
|
2285
|
+
}, [cardOpen, isCardOpenControlled]);
|
|
1986
2286
|
const bStyles = useMemo3(() => {
|
|
1987
2287
|
const base = resolveButtonsLayoutTheme2(buttonsTheme);
|
|
1988
2288
|
if (!stylesOverride) return base;
|
|
@@ -2020,20 +2320,26 @@ function InterimButtonsView({
|
|
|
2020
2320
|
"button",
|
|
2021
2321
|
{
|
|
2022
2322
|
type: "button",
|
|
2023
|
-
onClick: () =>
|
|
2323
|
+
onClick: () => {
|
|
2324
|
+
if (!isCardOpenControlled) {
|
|
2325
|
+
setShowCardForm(false);
|
|
2326
|
+
}
|
|
2327
|
+
},
|
|
2024
2328
|
"aria-label": "Back to payment methods",
|
|
2329
|
+
disabled: isCardOpenControlled || cardLoading,
|
|
2025
2330
|
style: {
|
|
2026
2331
|
display: "inline-flex",
|
|
2027
2332
|
alignItems: "center",
|
|
2028
2333
|
gap: hideBackButtonLabel ? 0 : "0.5rem",
|
|
2029
2334
|
background: "none",
|
|
2030
2335
|
border: "none",
|
|
2031
|
-
cursor: "pointer",
|
|
2032
2336
|
color: "#4b5563",
|
|
2033
2337
|
fontSize: "0.85rem",
|
|
2034
2338
|
fontWeight: 500,
|
|
2035
2339
|
padding: 0,
|
|
2036
2340
|
flexShrink: 0,
|
|
2341
|
+
opacity: isCardOpenControlled || cardLoading ? 0.6 : 1,
|
|
2342
|
+
cursor: isCardOpenControlled || cardLoading ? "not-allowed" : "pointer",
|
|
2037
2343
|
...bStyles.backButton
|
|
2038
2344
|
},
|
|
2039
2345
|
children: [
|
|
@@ -2093,10 +2399,16 @@ function InterimButtonsView({
|
|
|
2093
2399
|
"button",
|
|
2094
2400
|
{
|
|
2095
2401
|
type: "button",
|
|
2096
|
-
onClick: () => {
|
|
2402
|
+
onClick: async () => {
|
|
2403
|
+
if (cardLoading) return;
|
|
2404
|
+
if (onCardButtonClick) {
|
|
2405
|
+
await onCardButtonClick();
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2097
2408
|
onButtonClick?.("card");
|
|
2098
2409
|
setShowCardForm(true);
|
|
2099
2410
|
},
|
|
2411
|
+
disabled: cardLoading,
|
|
2100
2412
|
style: {
|
|
2101
2413
|
width: "100%",
|
|
2102
2414
|
padding: "0.9rem 1rem",
|
|
@@ -2106,7 +2418,7 @@ function InterimButtonsView({
|
|
|
2106
2418
|
borderRadius: "8px",
|
|
2107
2419
|
fontSize: bStyles.cardButtonFontSize ?? "0.95rem",
|
|
2108
2420
|
fontWeight: 600,
|
|
2109
|
-
cursor: "pointer",
|
|
2421
|
+
cursor: cardLoading ? "not-allowed" : "pointer",
|
|
2110
2422
|
display: "flex",
|
|
2111
2423
|
alignItems: "center",
|
|
2112
2424
|
justifyContent: "center",
|
|
@@ -2114,6 +2426,7 @@ function InterimButtonsView({
|
|
|
2114
2426
|
boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
|
|
2115
2427
|
transition: "transform 0.1s",
|
|
2116
2428
|
position: "relative",
|
|
2429
|
+
opacity: cardLoading ? 0.6 : 1,
|
|
2117
2430
|
...bStyles.cardButton
|
|
2118
2431
|
},
|
|
2119
2432
|
onMouseDown: (e) => {
|
|
@@ -2125,12 +2438,29 @@ function InterimButtonsView({
|
|
|
2125
2438
|
children: /* @__PURE__ */ jsx5(CardButtonContentSlot, { content: cardButtonContent })
|
|
2126
2439
|
}
|
|
2127
2440
|
),
|
|
2441
|
+
errorMessage && /* @__PURE__ */ jsxs3("div", { style: {
|
|
2442
|
+
margin: "0.25rem 0",
|
|
2443
|
+
padding: "0.625rem 0.875rem",
|
|
2444
|
+
background: "#FEF2F2",
|
|
2445
|
+
border: "1px solid #FECACA",
|
|
2446
|
+
borderRadius: "8px",
|
|
2447
|
+
color: "#991B1B",
|
|
2448
|
+
fontSize: "0.85rem",
|
|
2449
|
+
fontWeight: 600,
|
|
2450
|
+
display: "flex",
|
|
2451
|
+
alignItems: "center",
|
|
2452
|
+
gap: "0.5rem",
|
|
2453
|
+
...bStyles.errorBanner
|
|
2454
|
+
}, children: [
|
|
2455
|
+
/* @__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" }) }),
|
|
2456
|
+
errorMessage
|
|
2457
|
+
] }),
|
|
2128
2458
|
/* @__PURE__ */ jsx5("style", { children: `@keyframes flopay-interim-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }` })
|
|
2129
2459
|
] });
|
|
2130
2460
|
}
|
|
2131
2461
|
|
|
2132
2462
|
// src/checkout-form.tsx
|
|
2133
|
-
import { FloPayError as
|
|
2463
|
+
import { FloPayError as FloPayError4 } from "@flopay/shared";
|
|
2134
2464
|
import { forwardRef as forwardRef2, useCallback as useCallback3, useEffect as useEffect5, useImperativeHandle as useImperativeHandle2, useState as useState4 } from "react";
|
|
2135
2465
|
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2136
2466
|
var WALLET_RESUME_KEY2 = "flopay_wallet_resume";
|
|
@@ -2146,6 +2476,7 @@ function CheckoutFormInner({
|
|
|
2146
2476
|
userId,
|
|
2147
2477
|
onComplete,
|
|
2148
2478
|
onError,
|
|
2479
|
+
onDecline,
|
|
2149
2480
|
onTokenizedBody,
|
|
2150
2481
|
layout = "auto",
|
|
2151
2482
|
submitLabel = "Pay",
|
|
@@ -2177,6 +2508,12 @@ function CheckoutFormInner({
|
|
|
2177
2508
|
},
|
|
2178
2509
|
[onErrorChange]
|
|
2179
2510
|
);
|
|
2511
|
+
const emitDecline = useCallback3(
|
|
2512
|
+
(input, overrides) => {
|
|
2513
|
+
onDecline?.(buildDeclineEvent("card", input, overrides));
|
|
2514
|
+
},
|
|
2515
|
+
[onDecline]
|
|
2516
|
+
);
|
|
2180
2517
|
const processPaymentInternal = useCallback3(
|
|
2181
2518
|
async (tokenizedBody) => {
|
|
2182
2519
|
setProcessing(true);
|
|
@@ -2223,6 +2560,7 @@ function CheckoutFormInner({
|
|
|
2223
2560
|
if (result.error) {
|
|
2224
2561
|
updateError(result.error.message);
|
|
2225
2562
|
onError?.(result.error);
|
|
2563
|
+
emitDecline(result.error);
|
|
2226
2564
|
return;
|
|
2227
2565
|
}
|
|
2228
2566
|
if (result.status === "succeeded" || result.status === "processing") {
|
|
@@ -2257,13 +2595,17 @@ function CheckoutFormInner({
|
|
|
2257
2595
|
}
|
|
2258
2596
|
const errorMessage = json?.message ?? "Payment failed. Please try again.";
|
|
2259
2597
|
updateError(errorMessage);
|
|
2598
|
+
emitDecline(errorMessage, {
|
|
2599
|
+
code: json?.code,
|
|
2600
|
+
declineCode: json?.declineCode ?? json?.gatewayDeclineReason
|
|
2601
|
+
});
|
|
2260
2602
|
} catch (err) {
|
|
2261
2603
|
updateError(err instanceof Error ? err.message : "An unexpected error occurred");
|
|
2262
2604
|
} finally {
|
|
2263
2605
|
setProcessing(false);
|
|
2264
2606
|
}
|
|
2265
2607
|
},
|
|
2266
|
-
[baseUrl, sessionId, userId, email, firstName, lastName, chv, flopay, onComplete, onError, updateError]
|
|
2608
|
+
[baseUrl, sessionId, userId, email, firstName, lastName, chv, flopay, onComplete, onError, updateError, emitDecline]
|
|
2267
2609
|
);
|
|
2268
2610
|
const dispatchTokenizedBody = useCallback3(
|
|
2269
2611
|
(tokenizedBody) => {
|
|
@@ -2287,6 +2629,7 @@ function CheckoutFormInner({
|
|
|
2287
2629
|
if (result.error) {
|
|
2288
2630
|
updateError(result.error.message);
|
|
2289
2631
|
onError?.(result.error);
|
|
2632
|
+
emitDecline(result.error);
|
|
2290
2633
|
} else if (result.status === "succeeded" || result.status === "processing") {
|
|
2291
2634
|
dispatchTokenizedBody({
|
|
2292
2635
|
id: result.paymentIntentId,
|
|
@@ -2300,7 +2643,7 @@ function CheckoutFormInner({
|
|
|
2300
2643
|
setIs3DSActive(false);
|
|
2301
2644
|
}
|
|
2302
2645
|
}
|
|
2303
|
-
}), [flopay, dispatchTokenizedBody, onError, updateError]);
|
|
2646
|
+
}), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
|
|
2304
2647
|
useEffect5(() => {
|
|
2305
2648
|
if (typeof window === "undefined") return;
|
|
2306
2649
|
const stored = localStorage.getItem(WALLET_RESUME_KEY2);
|
|
@@ -2338,7 +2681,7 @@ function CheckoutFormInner({
|
|
|
2338
2681
|
return;
|
|
2339
2682
|
}
|
|
2340
2683
|
if (!sessionId || !email) {
|
|
2341
|
-
throw new
|
|
2684
|
+
throw new FloPayError4("Missing sessionId or email", "validation_error");
|
|
2342
2685
|
}
|
|
2343
2686
|
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
2344
2687
|
method: "POST",
|
|
@@ -2350,16 +2693,18 @@ function CheckoutFormInner({
|
|
|
2350
2693
|
isPaypal: false
|
|
2351
2694
|
})
|
|
2352
2695
|
});
|
|
2353
|
-
if (!intentResponse.ok) throw new
|
|
2696
|
+
if (!intentResponse.ok) throw new FloPayError4("Failed to create payment intent", "api_error");
|
|
2354
2697
|
const intentJson = await intentResponse.json();
|
|
2355
2698
|
const intentClientSecret = intentJson.data?.id;
|
|
2356
|
-
if (!intentClientSecret) throw new
|
|
2699
|
+
if (!intentClientSecret) throw new FloPayError4("No client_secret in payment intent response", "api_error");
|
|
2357
2700
|
const confirmResult = await flopay.confirmCardPayment({
|
|
2358
2701
|
clientSecret: intentClientSecret,
|
|
2359
2702
|
paymentMethodId: pmResult.paymentMethodId
|
|
2360
2703
|
});
|
|
2361
2704
|
if (confirmResult.error) {
|
|
2362
2705
|
updateError(confirmResult.error.message);
|
|
2706
|
+
onError?.(confirmResult.error);
|
|
2707
|
+
emitDecline(confirmResult.error);
|
|
2363
2708
|
return;
|
|
2364
2709
|
}
|
|
2365
2710
|
dispatchTokenizedBody({
|
|
@@ -2376,7 +2721,7 @@ function CheckoutFormInner({
|
|
|
2376
2721
|
}
|
|
2377
2722
|
}
|
|
2378
2723
|
},
|
|
2379
|
-
[flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError]
|
|
2724
|
+
[flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError, emitDecline]
|
|
2380
2725
|
);
|
|
2381
2726
|
const isReady = flopay !== null && elements !== null;
|
|
2382
2727
|
return /* @__PURE__ */ jsxs4("form", { onSubmit: handleSubmit, className, style: { position: "relative" }, children: [
|
|
@@ -2408,7 +2753,7 @@ function CheckoutFormInner({
|
|
|
2408
2753
|
}
|
|
2409
2754
|
|
|
2410
2755
|
// src/paypal-button.tsx
|
|
2411
|
-
import { FloPayError as
|
|
2756
|
+
import { FloPayError as FloPayError5 } from "@flopay/shared";
|
|
2412
2757
|
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef4, useState as useState5 } from "react";
|
|
2413
2758
|
import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2414
2759
|
function PayPalButton({
|
|
@@ -2528,7 +2873,7 @@ function PayPalButton({
|
|
|
2528
2873
|
setSubmitting(true);
|
|
2529
2874
|
onErrorChange?.(null);
|
|
2530
2875
|
if (!sessionId || !email) {
|
|
2531
|
-
throw new
|
|
2876
|
+
throw new FloPayError5("Missing sessionId or email for PayPal payment", "validation_error");
|
|
2532
2877
|
}
|
|
2533
2878
|
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
2534
2879
|
method: "POST",
|
|
@@ -2540,10 +2885,10 @@ function PayPalButton({
|
|
|
2540
2885
|
isPaypal: "true"
|
|
2541
2886
|
})
|
|
2542
2887
|
});
|
|
2543
|
-
if (!intentResponse.ok) throw new
|
|
2888
|
+
if (!intentResponse.ok) throw new FloPayError5("Failed to create payment intent", "api_error");
|
|
2544
2889
|
const intentJson = await intentResponse.json();
|
|
2545
2890
|
const intentClientSecret = intentJson.data?.id;
|
|
2546
|
-
if (!intentClientSecret) throw new
|
|
2891
|
+
if (!intentClientSecret) throw new FloPayError5("No client_secret in response", "api_error");
|
|
2547
2892
|
const result = await flopay.confirmPayment({
|
|
2548
2893
|
clientSecret: intentClientSecret,
|
|
2549
2894
|
returnUrl: window.location.href
|