@flopay/react 0.5.3 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -522,6 +522,27 @@ function buildDeclineEvent(method, input, overrides) {
522
522
  ...declineCode ? { declineCode } : {}
523
523
  };
524
524
  }
525
+ function isRecord(value) {
526
+ return typeof value === "object" && value !== null;
527
+ }
528
+ function readString(payload, key) {
529
+ const value = payload?.[key];
530
+ return typeof value === "string" && value.trim() ? value : void 0;
531
+ }
532
+ function buildFloPayApiError(payload, fallbackMessage) {
533
+ const nestedError = isRecord(payload?.error) ? payload.error : null;
534
+ const message = readString(payload, "message") ?? readString(nestedError, "message") ?? fallbackMessage;
535
+ const code = readString(payload, "code") ?? readString(payload, "gatewayErrorCode") ?? readString(nestedError, "code");
536
+ const declineCode = readString(payload, "declineCode") ?? readString(payload, "gatewayDeclineReason") ?? readString(payload, "decline_code") ?? readString(nestedError, "decline_code");
537
+ return new FloPayError(message, "api_error", {
538
+ ...code ? { code } : {},
539
+ ...declineCode ? { declineCode } : {}
540
+ });
541
+ }
542
+ async function buildFloPayApiErrorFromResponse(response, fallbackMessage) {
543
+ const payload = await response.json().catch(() => null);
544
+ return buildFloPayApiError(payload, fallbackMessage);
545
+ }
525
546
  function mapPayPalIntentStatusToPaymentResult(status) {
526
547
  if (status === "succeeded") {
527
548
  return "succeeded";
@@ -531,6 +552,29 @@ function mapPayPalIntentStatusToPaymentResult(status) {
531
552
  }
532
553
  return "failed";
533
554
  }
555
+ function resolvePaymentIntentPaymentMethodId(paymentIntent) {
556
+ const paymentMethod = paymentIntent?.payment_method;
557
+ if (typeof paymentMethod === "string" && paymentMethod.startsWith("pm_")) {
558
+ return paymentMethod;
559
+ }
560
+ if (paymentMethod && typeof paymentMethod === "object" && typeof paymentMethod.id === "string") {
561
+ return paymentMethod.id;
562
+ }
563
+ return void 0;
564
+ }
565
+ async function retrievePaymentIntentFromProvider(provider, clientSecret) {
566
+ const retriever = provider;
567
+ if (!retriever?.retrievePaymentIntent) {
568
+ return null;
569
+ }
570
+ const { paymentIntent, error } = await retriever.retrievePaymentIntent(clientSecret);
571
+ if (error) {
572
+ throw new FloPayError(error.message ?? "Failed to retrieve payment intent.", "api_error", {
573
+ ...error.code ? { code: error.code } : {}
574
+ });
575
+ }
576
+ return paymentIntent ?? null;
577
+ }
534
578
  function resolveTokenizedPaymentMethodId(tokenizedBody) {
535
579
  const candidates = [
536
580
  tokenizedBody?.id,
@@ -729,8 +773,9 @@ function PayPalButtonInner({
729
773
  accountPatch: beforeClick.accountPatch,
730
774
  sessionId: beforeClick.sessionId
731
775
  };
776
+ onButtonClick?.("paypal");
732
777
  event.resolve();
733
- }, [isProcessing, runBeforeButtonClick, submitting]);
778
+ }, [isProcessing, onButtonClick, runBeforeButtonClick, submitting]);
734
779
  const handlePayPalConfirm = useCallback(async (event) => {
735
780
  if (!stripe || !elements) return;
736
781
  let prepared = beforeClickRef.current;
@@ -748,7 +793,6 @@ function PayPalButtonInner({
748
793
  }
749
794
  const effectiveSessionId = prepared?.sessionId ?? sessionId;
750
795
  const effectiveEmail = prepared?.accountPatch?.email ?? email;
751
- onButtonClick?.("paypal");
752
796
  try {
753
797
  setSubmitting(true);
754
798
  onErrorChange?.(null);
@@ -775,7 +819,16 @@ function PayPalButtonInner({
775
819
  setup_future_usage: "off_session"
776
820
  })
777
821
  });
778
- if (!intentResponse.ok) throw new Error("Failed to create payment intent");
822
+ if (!intentResponse.ok) {
823
+ const intentError = await buildFloPayApiErrorFromResponse(
824
+ intentResponse,
825
+ "Failed to create payment intent"
826
+ );
827
+ onErrorChange?.(intentError.message);
828
+ onDecline?.(buildDeclineEvent("paypal", intentError));
829
+ event.paymentFailed({ reason: "fail", message: intentError.message });
830
+ return;
831
+ }
779
832
  const intentJson = await intentResponse.json();
780
833
  const intentClientSecret = intentJson.data?.id;
781
834
  if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
@@ -814,7 +867,7 @@ function PayPalButtonInner({
814
867
  } finally {
815
868
  setSubmitting(false);
816
869
  }
817
- }, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, onButtonClick, runBeforeButtonClick]);
870
+ }, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]);
818
871
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
819
872
  /* @__PURE__ */ jsx5(ExpressCheckoutReadySwap, { state: loadState, placeholderTestId: "flopay-paypal-placeholder", children: /* @__PURE__ */ jsx5(
820
873
  ExpressCheckoutElement,
@@ -885,7 +938,6 @@ function WalletButtonInner({
885
938
  const method = walletType === "apple_pay" ? "apple_pay" : "google_pay";
886
939
  const effectiveSessionId = prepared?.sessionId ?? sessionId;
887
940
  const effectiveEmail = prepared?.accountPatch?.email ?? email;
888
- onButtonClick?.(walletType === "apple_pay" ? "apple_pay" : "google_pay");
889
941
  try {
890
942
  setSubmitting(true);
891
943
  onErrorChange?.(null);
@@ -912,7 +964,16 @@ function WalletButtonInner({
912
964
  isPaypal: false
913
965
  })
914
966
  });
915
- if (!intentResponse.ok) throw new Error("Failed to create payment intent");
967
+ if (!intentResponse.ok) {
968
+ const intentError = await buildFloPayApiErrorFromResponse(
969
+ intentResponse,
970
+ "Failed to create payment intent"
971
+ );
972
+ onErrorChange?.(intentError.message);
973
+ onDecline?.(buildDeclineEvent(method, intentError));
974
+ event.paymentFailed({ reason: "fail", message: intentError.message });
975
+ return;
976
+ }
916
977
  const intentJson = await intentResponse.json();
917
978
  const intentClientSecret = intentJson.data?.id;
918
979
  if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
@@ -942,7 +1003,7 @@ function WalletButtonInner({
942
1003
  setSubmitting(false);
943
1004
  }
944
1005
  },
945
- [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, onButtonClick, runBeforeButtonClick]
1006
+ [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]
946
1007
  );
947
1008
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
948
1009
  /* @__PURE__ */ jsx5(ExpressCheckoutReadySwap, { state: loadState, placeholderTestId: "flopay-wallet-placeholder", children: /* @__PURE__ */ jsx5(
@@ -962,6 +1023,7 @@ function WalletButtonInner({
962
1023
  accountPatch: beforeClick.accountPatch,
963
1024
  sessionId: beforeClick.sessionId
964
1025
  };
1026
+ onButtonClick?.(lastWalletMethodRef.current);
965
1027
  event.resolve();
966
1028
  },
967
1029
  onConfirm: handleWalletConfirm,
@@ -1527,7 +1589,18 @@ function SplitCardFormInner({
1527
1589
  isPaypal: false
1528
1590
  })
1529
1591
  });
1530
- if (!intentResponse.ok) throw new FloPayError2("Failed to create payment intent", "api_error");
1592
+ if (!intentResponse.ok) {
1593
+ const intentError = await buildFloPayApiErrorFromResponse(
1594
+ intentResponse,
1595
+ "Failed to create payment intent"
1596
+ );
1597
+ setOverlayStatus("error");
1598
+ updateError(intentError.message);
1599
+ onError?.(intentError);
1600
+ emitDecline("card", intentError);
1601
+ await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
1602
+ return;
1603
+ }
1531
1604
  const intentJson = await intentResponse.json();
1532
1605
  const intentClientSecret = intentJson.data?.id;
1533
1606
  if (!intentClientSecret) throw new FloPayError2("No client_secret in payment intent response", "api_error");
@@ -1543,11 +1616,26 @@ function SplitCardFormInner({
1543
1616
  await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
1544
1617
  return;
1545
1618
  }
1619
+ const rawProvider = typeof flopay.getRawProvider === "function" ? flopay.getRawProvider() : null;
1620
+ const confirmedPaymentIntent = await retrievePaymentIntentFromProvider(
1621
+ rawProvider,
1622
+ intentClientSecret
1623
+ );
1624
+ const paymentIntentId = confirmResult.paymentIntentId ?? confirmedPaymentIntent?.id;
1625
+ const paymentMethodId = confirmResult.paymentMethodId ?? resolvePaymentIntentPaymentMethodId(confirmedPaymentIntent) ?? pmResult.paymentMethodId;
1626
+ if (!paymentIntentId) {
1627
+ const error2 = new FloPayError2("No payment intent returned after confirmation.", "api_error");
1628
+ setOverlayStatus("error");
1629
+ updateError(error2.message);
1630
+ onError?.(error2);
1631
+ await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
1632
+ return;
1633
+ }
1546
1634
  handedOff = isSelfContained;
1547
1635
  dispatchTokenizedBody({
1548
- id: pmResult.paymentMethodId,
1636
+ id: paymentMethodId,
1549
1637
  type: "card",
1550
- threeDSecureActionResultTokenId: confirmResult.paymentIntentId,
1638
+ threeDSecureActionResultTokenId: paymentIntentId,
1551
1639
  originalPaymentMethodId: pmResult.paymentMethodId
1552
1640
  });
1553
1641
  } catch (err) {
@@ -2368,12 +2456,12 @@ async function processSavedPaymentWithIntent({
2368
2456
  ));
2369
2457
  const intentJson = await intentResponse.json().catch(() => null);
2370
2458
  if (!intentResponse.ok) {
2459
+ const intentError = buildFloPayApiError(
2460
+ intentJson,
2461
+ "Failed to create payment intent."
2462
+ );
2371
2463
  throw Object.assign(
2372
- new FloPayError3(
2373
- intentJson?.message ?? "Failed to create payment intent.",
2374
- "api_error",
2375
- { code: intentJson?.code ?? intentJson?.gatewayErrorCode }
2376
- ),
2464
+ intentError,
2377
2465
  { checkoutMethod: "card" }
2378
2466
  );
2379
2467
  }
@@ -2396,7 +2484,14 @@ async function processSavedPaymentWithIntent({
2396
2484
  { checkoutMethod: "card" }
2397
2485
  );
2398
2486
  }
2399
- if (!confirmResult.paymentIntentId || confirmResult.status !== "succeeded" && confirmResult.status !== "processing" && confirmResult.status !== "requires_capture") {
2487
+ const rawProvider = typeof flopay.getRawProvider === "function" ? flopay.getRawProvider() : null;
2488
+ const confirmedPaymentIntent = await retrievePaymentIntentFromProvider(
2489
+ rawProvider,
2490
+ intentClientSecret
2491
+ );
2492
+ const confirmedPaymentIntentId = confirmResult.paymentIntentId ?? confirmedPaymentIntent?.id;
2493
+ const confirmedPaymentMethodId = confirmResult.paymentMethodId ?? resolvePaymentIntentPaymentMethodId(confirmedPaymentIntent) ?? paymentMethodId;
2494
+ if (!confirmedPaymentIntentId || confirmResult.status !== "succeeded" && confirmResult.status !== "processing" && confirmResult.status !== "requires_capture") {
2400
2495
  throw Object.assign(
2401
2496
  new FloPayError3(
2402
2497
  `Unfortunately, your payment could not be processed. Please try again using a different payment method or contact your bank for assistance. If the issue persists, feel free to reach out to us for support.`,
@@ -2413,9 +2508,9 @@ async function processSavedPaymentWithIntent({
2413
2508
  sessionId,
2414
2509
  session,
2415
2510
  tokenizedData: {
2416
- id: paymentMethodId,
2511
+ id: confirmedPaymentMethodId,
2417
2512
  type: "card",
2418
- threeDSecureActionResultTokenId: confirmResult.paymentIntentId
2513
+ threeDSecureActionResultTokenId: confirmedPaymentIntentId
2419
2514
  },
2420
2515
  returnUrl
2421
2516
  });
@@ -2430,8 +2525,8 @@ async function processSavedPaymentWithIntent({
2430
2525
  });
2431
2526
  return {
2432
2527
  ...finalResult,
2433
- paymentIntentId: finalResult.paymentIntentId ?? confirmResult.paymentIntentId,
2434
- paymentMethodId: finalResult.paymentMethodId ?? paymentMethodId,
2528
+ paymentIntentId: finalResult.paymentIntentId ?? confirmedPaymentIntentId,
2529
+ paymentMethodId: finalResult.paymentMethodId ?? confirmedPaymentMethodId,
2435
2530
  checkoutMethod: finalResult.checkoutMethod ?? "card"
2436
2531
  };
2437
2532
  }
@@ -2645,6 +2740,7 @@ async function loadSavedPaymentProviders({
2645
2740
  import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
2646
2741
  var DEFAULT_BUTTONS_LAYOUT_BUTTON_HEIGHT2 = 44;
2647
2742
  var PAYPAL_RESUME_STORAGE_KEY = "flopay_checkout_saved_payment_resume";
2743
+ var sessionInflightMap = /* @__PURE__ */ new Map();
2648
2744
  function sleep(ms) {
2649
2745
  return new Promise((resolve) => setTimeout(resolve, ms));
2650
2746
  }
@@ -3011,11 +3107,15 @@ function FloPayCheckout({
3011
3107
  }
3012
3108
  })();
3013
3109
  }, [emitDecline, locale, normalizeSavedPaymentError, resolvedBillingUrl]);
3014
- const inflightRef = useRef3(/* @__PURE__ */ new Map());
3015
3110
  const initializedHashRef = useRef3(null);
3016
3111
  function hashCreateParams(params) {
3017
3112
  const key = JSON.stringify({
3018
3113
  c: params?.clientId,
3114
+ a: {
3115
+ u: params?.account?.userId ?? "",
3116
+ e: params?.account?.email?.trim().toLowerCase() ?? "",
3117
+ country: params?.account?.country ?? ""
3118
+ },
3019
3119
  successUrl: params?.successUrl,
3020
3120
  cancelUrl: params?.cancelUrl,
3021
3121
  i: params?.items?.map((x) => `${x.providerItemId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
@@ -3023,6 +3123,7 @@ function FloPayCheckout({
3023
3123
  couponCodes: params?.couponCodes,
3024
3124
  tagsData: params?.tagsData,
3025
3125
  utmMetadata: params?.utmMetadata,
3126
+ tokenizedData: params?.tokenizedData,
3026
3127
  m: params?.checkoutMode ?? "full"
3027
3128
  });
3028
3129
  let h = 0;
@@ -3087,16 +3188,16 @@ function FloPayCheckout({
3087
3188
  }
3088
3189
  const mergedParams = mergeInlineSessionPatch(baseParams, patch);
3089
3190
  const cacheKey = hashCreateParams(mergedParams);
3090
- let promise = inflightRef.current.get(cacheKey);
3191
+ let promise = sessionInflightMap.get(cacheKey);
3091
3192
  if (!promise) {
3092
3193
  promise = resolveInlineSession(mergedParams, cacheKey);
3093
- inflightRef.current.set(cacheKey, promise);
3194
+ sessionInflightMap.set(cacheKey, promise);
3094
3195
  }
3095
3196
  let resolved;
3096
3197
  try {
3097
3198
  resolved = await promise;
3098
3199
  } finally {
3099
- inflightRef.current.delete(cacheKey);
3200
+ sessionInflightMap.delete(cacheKey);
3100
3201
  }
3101
3202
  initializedHashRef.current = cacheKey;
3102
3203
  try {
@@ -4025,6 +4126,7 @@ function CheckoutFormInner({
4025
4126
  if (!flopay || !elements || isSubmitting) return;
4026
4127
  setProcessing(true);
4027
4128
  updateError(null);
4129
+ let handedOff = false;
4028
4130
  try {
4029
4131
  const submitResult = await flopay.submitElements();
4030
4132
  if (submitResult.error) {
@@ -4054,7 +4156,16 @@ function CheckoutFormInner({
4054
4156
  isPaypal: false
4055
4157
  })
4056
4158
  });
4057
- if (!intentResponse.ok) throw new FloPayError5("Failed to create payment intent", "api_error");
4159
+ if (!intentResponse.ok) {
4160
+ const intentError = await buildFloPayApiErrorFromResponse(
4161
+ intentResponse,
4162
+ "Failed to create payment intent"
4163
+ );
4164
+ updateError(intentError.message);
4165
+ onError?.(intentError);
4166
+ emitDecline(intentError);
4167
+ return;
4168
+ }
4058
4169
  const intentJson = await intentResponse.json();
4059
4170
  const intentClientSecret = intentJson.data?.id;
4060
4171
  if (!intentClientSecret) throw new FloPayError5("No client_secret in payment intent response", "api_error");
@@ -4068,17 +4179,30 @@ function CheckoutFormInner({
4068
4179
  emitDecline(confirmResult.error);
4069
4180
  return;
4070
4181
  }
4182
+ const rawProvider = typeof flopay.getRawProvider === "function" ? flopay.getRawProvider() : null;
4183
+ const confirmedPaymentIntent = await retrievePaymentIntentFromProvider(
4184
+ rawProvider,
4185
+ intentClientSecret
4186
+ );
4187
+ const paymentIntentId = confirmResult.paymentIntentId ?? confirmedPaymentIntent?.id;
4188
+ const paymentMethodId = confirmResult.paymentMethodId ?? resolvePaymentIntentPaymentMethodId(confirmedPaymentIntent) ?? pmResult.paymentMethodId;
4189
+ if (!paymentIntentId) {
4190
+ const error2 = new FloPayError5("No payment intent returned after confirmation.", "api_error");
4191
+ updateError(error2.message);
4192
+ onError?.(error2);
4193
+ return;
4194
+ }
4195
+ handedOff = isSelfContained;
4071
4196
  dispatchTokenizedBody({
4072
- id: pmResult.paymentMethodId,
4197
+ id: paymentMethodId,
4073
4198
  type: "card",
4074
- threeDSecureActionResultTokenId: confirmResult.paymentIntentId,
4199
+ threeDSecureActionResultTokenId: paymentIntentId,
4075
4200
  originalPaymentMethodId: pmResult.paymentMethodId
4076
4201
  });
4077
4202
  } catch (err) {
4078
4203
  updateError(err instanceof Error ? err.message : "An unexpected error occurred");
4079
4204
  } finally {
4080
- if (isSelfContained) {
4081
- } else {
4205
+ if (!handedOff) {
4082
4206
  setProcessing(false);
4083
4207
  }
4084
4208
  }
@@ -4756,7 +4880,10 @@ function FloPayAutomaticPaymentButton({
4756
4880
  return;
4757
4881
  }
4758
4882
  try {
4759
- const result = await api.createAndFetchSession(createSessionDraft);
4883
+ const result = await api.createAndFetchSession({
4884
+ ...createSessionDraft,
4885
+ ...automaticPaymentToken ? { tokenizedData: automaticPaymentToken } : {}
4886
+ });
4760
4887
  await processResolvedSession(result, result.data.session?.id ?? null, {
4761
4888
  fromCreateSession: true
4762
4889
  });