@flopay/react 1.1.0 → 1.1.3

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
@@ -457,15 +457,37 @@ function mergeInlineSessionPatch(params, patch) {
457
457
  };
458
458
  }
459
459
  function buildSyntheticSession(params, checkoutModeOverride) {
460
- const totalAmount = [
461
- ...(params.items ?? []).map((item) => item.overrideAmount ?? item.totalAmount ?? 0),
462
- ...(params.subscriptions ?? []).map((subscription) => subscription.overrideAmount ?? subscription.totalAmount ?? 0)
463
- ].reduce((sum, value) => sum + value, 0);
464
- const currency = params.currency ?? params.items?.find((i) => i.currency)?.currency ?? params.subscriptions?.find((s) => s.currency)?.currency ?? "USD";
460
+ const inputProducts = params.products ?? [
461
+ ...(params.subscriptions ?? []).map((s) => ({
462
+ type: "subscription",
463
+ code: s.code ?? s.providerPlanId,
464
+ name: s.subscriptionName ?? s.providerPlanName ?? s.code ?? s.providerPlanId ?? null,
465
+ quantity: s.quantity ?? 1,
466
+ totalAmount: s.totalAmount,
467
+ overrideAmount: s.overrideAmount,
468
+ currency: s.currency,
469
+ metadata: s.metadata
470
+ })),
471
+ ...(params.items ?? []).map((i) => ({
472
+ type: "item",
473
+ code: i.code ?? i.providerItemId,
474
+ name: i.itemName ?? i.providerItemName ?? i.code ?? i.providerItemId ?? null,
475
+ quantity: i.quantity ?? 1,
476
+ totalAmount: i.totalAmount,
477
+ overrideAmount: i.overrideAmount,
478
+ currency: i.currency,
479
+ metadata: i.metadata
480
+ }))
481
+ ];
482
+ const totalAmount = inputProducts.reduce(
483
+ (sum, p) => sum + (p.overrideAmount ?? p.totalAmount ?? 0),
484
+ 0
485
+ );
486
+ const currency = params.currency ?? inputProducts.find((p) => p.currency)?.currency ?? "USD";
465
487
  return {
466
488
  id: "",
467
489
  clientSecret: "",
468
- mode: "payment",
490
+ mode: inputProducts.some((p) => p.type === "subscription") ? "subscription" : "payment",
469
491
  amount: Math.round(totalAmount * 100),
470
492
  currency,
471
493
  status: "open",
@@ -483,40 +505,18 @@ function buildSyntheticSession(params, checkoutModeOverride) {
483
505
  successUrl: params.successUrl,
484
506
  cancelUrl: params.cancelUrl,
485
507
  checkoutMode: checkoutModeOverride ?? params.checkoutMode ?? "full",
486
- items: (params.items ?? []).map((item, idx) => {
487
- const code = item.code ?? item.providerItemId;
488
- const itemName = item.itemName ?? item.providerItemName ?? code;
489
- return {
490
- uuid: `synthetic-item-${idx}`,
491
- checkoutSessionId: "",
492
- code,
493
- providerItemId: code,
494
- itemName,
495
- providerItemName: itemName,
496
- quantity: item.quantity ?? 1,
497
- totalAmount: item.totalAmount,
498
- overrideAmount: item.overrideAmount ?? null,
499
- currency: item.currency ?? currency,
500
- metadata: item.metadata ?? null
501
- };
502
- }),
503
- subscriptions: (params.subscriptions ?? []).map((subscription, idx) => {
504
- const code = subscription.code ?? subscription.providerPlanId;
505
- const subscriptionName = subscription.subscriptionName ?? subscription.providerPlanName ?? code;
506
- return {
507
- uuid: `synthetic-sub-${idx}`,
508
- checkoutSessionId: "",
509
- code,
510
- providerPlanId: code,
511
- subscriptionName,
512
- providerPlanName: subscriptionName,
513
- quantity: subscription.quantity ?? 1,
514
- totalAmount: subscription.totalAmount,
515
- overrideAmount: subscription.overrideAmount ?? null,
516
- currency: subscription.currency ?? currency,
517
- metadata: subscription.metadata ?? null
518
- };
519
- })
508
+ products: inputProducts.map((p, idx) => ({
509
+ uuid: `synthetic-${p.type}-${idx}`,
510
+ checkoutSessionId: "",
511
+ type: p.type,
512
+ code: p.code,
513
+ name: p.name ?? null,
514
+ quantity: p.quantity ?? 1,
515
+ totalAmount: p.totalAmount,
516
+ overrideAmount: p.overrideAmount ?? null,
517
+ currency: p.currency ?? currency,
518
+ metadata: p.metadata ?? null
519
+ }))
520
520
  };
521
521
  }
522
522
  function mergeAccountPatch(base, patch) {
@@ -544,9 +544,25 @@ function readString(payload, key) {
544
544
  const value = payload?.[key];
545
545
  return typeof value === "string" && value.trim() ? value : void 0;
546
546
  }
547
+ var FRIENDLY_MESSAGE_OVERRIDES = [
548
+ {
549
+ match: /^paypal authorization required\.?$/i,
550
+ replacement: "For your additional security, please re-authenticate this payment via PayPal."
551
+ }
552
+ ];
553
+ function applyFriendlyMessageOverride(message) {
554
+ if (typeof message !== "string") return message ?? void 0;
555
+ const trimmed = message.trim();
556
+ if (!trimmed) return message;
557
+ for (const { match, replacement } of FRIENDLY_MESSAGE_OVERRIDES) {
558
+ if (match.test(trimmed)) return replacement;
559
+ }
560
+ return message;
561
+ }
547
562
  function buildFloPayApiError(payload, fallbackMessage) {
548
563
  const nestedError = isRecord(payload?.error) ? payload.error : null;
549
- const message = readString(payload, "message") ?? readString(nestedError, "message") ?? fallbackMessage;
564
+ const rawMessage = readString(payload, "message") ?? readString(nestedError, "message") ?? fallbackMessage;
565
+ const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
550
566
  const code = readString(payload, "code") ?? readString(payload, "gatewayErrorCode") ?? readString(nestedError, "code");
551
567
  const declineCode = readString(payload, "declineCode") ?? readString(payload, "gatewayDeclineReason") ?? readString(payload, "decline_code") ?? readString(nestedError, "decline_code");
552
568
  return new FloPayError(message, "api_error", {
@@ -848,7 +864,8 @@ function DirectPayPalButton({
848
864
  const forwardError = (message) => {
849
865
  if (cancelled) return;
850
866
  if (isZoidLifecycleMessage(message)) return;
851
- onErrorChangeRef.current?.(message);
867
+ const friendly = applyFriendlyMessageOverride(message) ?? message;
868
+ onErrorChangeRef.current?.(friendly);
852
869
  };
853
870
  const markRenderFailed = (message) => {
854
871
  if (cancelled) return;
@@ -895,14 +912,16 @@ function DirectPayPalButton({
895
912
  return;
896
913
  }
897
914
  const json = await response.json().catch(() => null);
898
- const message = json?.["message"] ?? "PayPal payment failed.";
915
+ const rawMessage = json?.["message"] ?? "PayPal payment failed.";
916
+ const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
899
917
  forwardError(message);
900
918
  onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
901
919
  code: json?.["code"],
902
920
  declineCode: json?.["declineCode"]
903
921
  }));
904
922
  } catch (err) {
905
- const message = err instanceof Error ? err.message : "PayPal payment failed.";
923
+ const rawMessage = err instanceof Error ? err.message : "PayPal payment failed.";
924
+ const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
906
925
  forwardError(message);
907
926
  onDeclineRef.current?.(buildDeclineEvent("paypal", message));
908
927
  }
@@ -3052,9 +3071,11 @@ function getRedirectResultFromCheckoutProcessError(error) {
3052
3071
  }
3053
3072
  function checkoutProcessErrorToFloPayError(error, fallbackMessage = "Payment failed. Please try again.", options) {
3054
3073
  const checkoutMethod = options?.checkoutMethod ?? error?.checkoutMethod ?? (error?.type === "paypal_redirect_required" ? "paypal" : "card");
3074
+ const rawMessage = error?.message ?? fallbackMessage;
3075
+ const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
3055
3076
  return Object.assign(
3056
3077
  new FloPayError4(
3057
- error?.message ?? fallbackMessage,
3078
+ message,
3058
3079
  "api_error",
3059
3080
  {
3060
3081
  code: error?.gatewayErrorCode
@@ -3511,21 +3532,36 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
3511
3532
  }
3512
3533
  function normalizeSavedPaymentError(err) {
3513
3534
  if (err instanceof FloPayError4) {
3535
+ const friendly = applyFriendlyMessageOverride(err.message);
3536
+ if (friendly && friendly !== err.message) {
3537
+ return Object.assign(
3538
+ new FloPayError4(friendly, err.type, {
3539
+ code: err.code,
3540
+ declineCode: err.declineCode,
3541
+ param: err.param,
3542
+ statusCode: err.statusCode
3543
+ }),
3544
+ { checkoutMethod: err.checkoutMethod }
3545
+ );
3546
+ }
3514
3547
  return err;
3515
3548
  }
3516
- return new FloPayError4(
3517
- err instanceof Error ? err.message : "Payment failed. Please try again.",
3518
- "api_error"
3519
- );
3549
+ const rawMessage = err instanceof Error ? err.message : "Payment failed. Please try again.";
3550
+ const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
3551
+ return new FloPayError4(message, "api_error");
3520
3552
  }
3521
3553
  function resolveSavedPaymentPublishableKeys(unified) {
3522
3554
  const publishableKey = unified.data.stripe?.publishableKey;
3523
- if (!publishableKey) {
3555
+ const hasDirectPaypal = Boolean(unified.data.paypal?.publishableKey);
3556
+ if (!publishableKey && !hasDirectPaypal) {
3524
3557
  throw new FloPayError4(
3525
- "No publishable key found in the checkout session. Ensure the session advertises gateways.stripe.",
3558
+ "Session advertises no supported gateways (expected `gateways.stripe` and/or `gateways.paypal`).",
3526
3559
  "validation_error"
3527
3560
  );
3528
3561
  }
3562
+ if (!publishableKey) {
3563
+ return {};
3564
+ }
3529
3565
  return {
3530
3566
  publishableKey,
3531
3567
  // Direct-PayPal sessions can still use Stripe's PayPal Element for the
@@ -3542,6 +3578,9 @@ async function loadSavedPaymentProviders({
3542
3578
  billingApiUrl,
3543
3579
  locale
3544
3580
  }) {
3581
+ if (!publishableKey) {
3582
+ return { flopay: null, paypalFlopay: null };
3583
+ }
3545
3584
  const needsSeparatePaypal = Boolean(paypalPublishableKey) && paypalPublishableKey !== publishableKey;
3546
3585
  const [instance, paypalInstanceOrError] = await Promise.all([
3547
3586
  loadFloPay(publishableKey, {
@@ -3578,6 +3617,11 @@ function resolveDirectPaypalConfig(unified) {
3578
3617
  environment: unified?.data.paypal?.environment
3579
3618
  };
3580
3619
  }
3620
+ function isPayPalOnlyUnified(unified) {
3621
+ if (!unified) return false;
3622
+ if (unified.data.stripe?.publishableKey) return false;
3623
+ return Boolean(resolveDirectPaypalConfig(unified)?.clientId);
3624
+ }
3581
3625
  function canUseStorage() {
3582
3626
  return typeof window !== "undefined" && typeof window.sessionStorage !== "undefined";
3583
3627
  }
@@ -3756,7 +3800,7 @@ function FloPayCheckout({
3756
3800
  const redirectResult = getRedirectResultFromCheckoutProcessError(options?.initialAutoProcessingError);
3757
3801
  let paymentResult;
3758
3802
  if (redirectResult) {
3759
- if (redirectResult.type === "paypal_redirect_required" && savedPaymentKeysRef.current) {
3803
+ if (redirectResult.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
3760
3804
  persistPayPalResumeState({
3761
3805
  sessionId: activeSessionId2,
3762
3806
  publishableKey: savedPaymentKeysRef.current.publishableKey,
@@ -3808,7 +3852,7 @@ function FloPayCheckout({
3808
3852
  if (result.type === "success") {
3809
3853
  paymentResult = result.result;
3810
3854
  } else {
3811
- if (result.type === "paypal_redirect_required" && savedPaymentKeysRef.current) {
3855
+ if (result.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
3812
3856
  persistPayPalResumeState({
3813
3857
  sessionId: activeSessionId2,
3814
3858
  publishableKey: savedPaymentKeysRef.current.publishableKey,
@@ -3899,7 +3943,7 @@ function FloPayCheckout({
3899
3943
  billingApiUrl: resolvedBillingUrl,
3900
3944
  locale
3901
3945
  });
3902
- const paypalStripe = (resumePaypalFlopay ?? resumeFlopay).getRawProvider();
3946
+ const paypalStripe = (resumePaypalFlopay ?? resumeFlopay)?.getRawProvider();
3903
3947
  if (!paypalStripe) {
3904
3948
  throw Object.assign(
3905
3949
  new FloPayError5("PayPal is not available.", "api_error"),
@@ -4167,7 +4211,8 @@ function FloPayCheckout({
4167
4211
  const resolved = await bootstrapInlineSession();
4168
4212
  const resolvedSession = resolved.result.data.session ?? null;
4169
4213
  savedPaymentKeysRef.current = resolveSavedPaymentPublishableKeys(resolved.result);
4170
- if (!cancelled && shouldAutoProcessInlineSession && resolvedSession) {
4214
+ const resolvedPaypalOnly = isPayPalOnlyUnified(resolved.result);
4215
+ if (!cancelled && shouldAutoProcessInlineSession && resolvedSession && !resolvedPaypalOnly) {
4171
4216
  autoCheckoutAttempted.current = true;
4172
4217
  await runSavedPaymentFlow(resolvedSession, {
4173
4218
  attempt3DS: true,
@@ -4181,6 +4226,9 @@ function FloPayCheckout({
4181
4226
  return;
4182
4227
  }
4183
4228
  if (!cancelled && shouldAutoProcessInlineSession) {
4229
+ if (resolvedPaypalOnly) {
4230
+ autoCheckoutAttempted.current = true;
4231
+ }
4184
4232
  setModeOverlayStatus(null);
4185
4233
  setModeOverlayError(null);
4186
4234
  }
@@ -4235,7 +4283,8 @@ function FloPayCheckout({
4235
4283
  const effectiveMode = checkoutModeProp ?? sess.checkoutMode ?? "full";
4236
4284
  setCurrentMode(effectiveMode);
4237
4285
  const hasPayPalRedirectParams = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("payment_intent");
4238
- if (effectiveMode === "auto" && !autoCheckoutAttempted.current && !hasPayPalRedirectParams) {
4286
+ const paypalOnly = isPayPalOnlyUnified(result);
4287
+ if (effectiveMode === "auto" && !autoCheckoutAttempted.current && !hasPayPalRedirectParams && !paypalOnly) {
4239
4288
  autoCheckoutAttempted.current = true;
4240
4289
  const stripeInitPromise = initStripe(result);
4241
4290
  try {
@@ -4315,8 +4364,10 @@ function FloPayCheckout({
4315
4364
  setConfirmProcessing(false);
4316
4365
  }
4317
4366
  }, [activeSessionId, confirmProcessing, runSavedPaymentFlow, session]);
4367
+ const directPaypalConfig = resolveDirectPaypalConfig(unified);
4368
+ const isPaypalOnlySession = isPayPalOnlyUnified(unified);
4318
4369
  const providerOptions = useMemo4(() => {
4319
- if (!unified || !session) return void 0;
4370
+ if (!unified || !session || !unified.data.stripe?.publishableKey) return void 0;
4320
4371
  const opts = {
4321
4372
  appearance,
4322
4373
  paymentMethodCreation: "manual",
@@ -4355,7 +4406,7 @@ function FloPayCheckout({
4355
4406
  cardBootstrapPending
4356
4407
  ]
4357
4408
  );
4358
- const shouldShowInterimButtons = Boolean(createSessionParams) && layout === "buttons" && (!flopay || !providerOptions);
4409
+ const shouldShowInterimButtons = Boolean(createSessionParams) && layout === "buttons" && !isPaypalOnlySession && (!flopay || !providerOptions);
4359
4410
  const modeOverlay = modeOverlayStatus ? /* @__PURE__ */ jsx7(
4360
4411
  ProcessingOverlay,
4361
4412
  {
@@ -4438,6 +4489,48 @@ function FloPayCheckout({
4438
4489
  modeOverlay
4439
4490
  ] });
4440
4491
  }
4492
+ if (isPaypalOnlySession && session && directPaypalConfig) {
4493
+ return /* @__PURE__ */ jsxs5(Fragment3, { children: [
4494
+ /* @__PURE__ */ jsx7(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ jsxs5("div", { className, style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
4495
+ modeError && /* @__PURE__ */ jsx7(
4496
+ "div",
4497
+ {
4498
+ role: "alert",
4499
+ "data-testid": "flopay-error",
4500
+ style: {
4501
+ padding: "0.625rem 0.875rem",
4502
+ background: "#FEF2F2",
4503
+ border: "1px solid #FECACA",
4504
+ borderRadius: "8px",
4505
+ color: "#991B1B",
4506
+ fontSize: "0.85rem",
4507
+ fontWeight: 600
4508
+ },
4509
+ children: modeError
4510
+ }
4511
+ ),
4512
+ /* @__PURE__ */ jsx7(
4513
+ DirectPayPalButton,
4514
+ {
4515
+ sessionId: activeSessionId,
4516
+ billingApiUrl: resolvedBillingUrl,
4517
+ email: session.customer?.email,
4518
+ clientId: directPaypalConfig.clientId,
4519
+ environment: directPaypalConfig.environment,
4520
+ currency: (session.currency ?? "usd").toUpperCase(),
4521
+ isSubscription: session.mode === "subscription",
4522
+ onComplete,
4523
+ onErrorChange: setModeError,
4524
+ onDecline,
4525
+ onButtonClick,
4526
+ session,
4527
+ debug
4528
+ }
4529
+ )
4530
+ ] }) }),
4531
+ modeOverlay
4532
+ ] });
4533
+ }
4441
4534
  if (!flopay || !providerOptions) {
4442
4535
  return /* @__PURE__ */ jsx7(Fragment3, { children: modeOverlay });
4443
4536
  }
@@ -5592,7 +5685,7 @@ function FloPayAutomaticPaymentButton({
5592
5685
  publishableKey,
5593
5686
  paypalPublishableKey
5594
5687
  } = resolveSavedPaymentPublishableKeys(apiResult);
5595
- if (redirectResult.type === "paypal_redirect_required") {
5688
+ if (redirectResult.type === "paypal_redirect_required" && publishableKey) {
5596
5689
  persistPayPalResumeState2({
5597
5690
  sessionId: session.id || resolvedSessionId,
5598
5691
  publishableKey,
@@ -5648,7 +5741,7 @@ function FloPayAutomaticPaymentButton({
5648
5741
  publishableKey,
5649
5742
  paypalPublishableKey
5650
5743
  } = resolveSavedPaymentPublishableKeys(apiResult);
5651
- if (result.type === "paypal_redirect_required") {
5744
+ if (result.type === "paypal_redirect_required" && publishableKey) {
5652
5745
  persistPayPalResumeState2({
5653
5746
  sessionId: session.id || resolvedSessionId,
5654
5747
  publishableKey,
@@ -5709,6 +5802,13 @@ function FloPayAutomaticPaymentButton({
5709
5802
  billingApiUrl: resolvedBillingUrl,
5710
5803
  locale
5711
5804
  });
5805
+ if (!flopay) {
5806
+ throw new FloPayError8(
5807
+ "Stripe is not available for 3DS authentication.",
5808
+ "api_error",
5809
+ { code: "authentication_required" }
5810
+ );
5811
+ }
5712
5812
  const paymentResult = await processSavedPaymentWithIntent({
5713
5813
  billingApiUrl: resolvedBillingUrl,
5714
5814
  sessionId: fallbackSessionId,
@@ -5895,7 +5995,7 @@ function FloPayAutomaticPaymentButton({
5895
5995
  billingApiUrl: resolvedBillingUrl,
5896
5996
  locale
5897
5997
  });
5898
- const paypalStripe = (paypalFlopay ?? flopay).getRawProvider();
5998
+ const paypalStripe = (paypalFlopay ?? flopay)?.getRawProvider();
5899
5999
  if (!paypalStripe) {
5900
6000
  throw Object.assign(
5901
6001
  new FloPayError8("PayPal is not available.", "api_error"),