@unifold/ui-react 0.1.70-beta.1 → 0.1.71-beta.1

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.
Files changed (3) hide show
  1. package/dist/index.js +200 -73
  2. package/dist/index.mjs +200 -73
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6702,7 +6702,7 @@ function BankTransfer({
6702
6702
  subdivision_code: userIpInfo?.subdivisionCode || void 0,
6703
6703
  // A provider may advertise multiple rails; default to the first.
6704
6704
  // UIs that want per-rail rows would need to render one entry per element.
6705
- payment_method: provider.payment_methods[0]
6705
+ payment_method_type: provider.payment_methods[0]
6706
6706
  });
6707
6707
  setActiveProvider(provider);
6708
6708
  const convertedPrefilled = await resolvePrefilledSourceAmount(provider.source_currency);
@@ -11257,6 +11257,13 @@ function setStoredSelectedToken(customerId, token) {
11257
11257
  } catch {
11258
11258
  }
11259
11259
  }
11260
+ function clearStoredSelectedToken(customerId) {
11261
+ try {
11262
+ if (typeof window === "undefined" || !customerId) return;
11263
+ localStorage.removeItem(selectedTokenKey(customerId));
11264
+ } catch {
11265
+ }
11266
+ }
11260
11267
 
11261
11268
  // src/components/deposits/stripe-link/PayWithStripeLink.tsx
11262
11269
  var import_jsx_runtime44 = require("react/jsx-runtime");
@@ -11274,6 +11281,31 @@ var STRIPE_SESSION_STATUS = {
11274
11281
  EXPIRED: "expired"
11275
11282
  };
11276
11283
  var STRIPE_SESSION_POLL_INTERVAL_MS = 3e3;
11284
+ var STRIPE_CONSUMED_PAYMENT_METHOD_CODE = "payment_method_unexpected_state";
11285
+ function isConsumedPaymentMethodError(err) {
11286
+ return err instanceof import_core21.StripeApiResponseError && (err.errorType === "stripe_onramp_payment_method_consumed" || err.stripeCode === STRIPE_CONSUMED_PAYMENT_METHOD_CODE);
11287
+ }
11288
+ var STRIPE_PAYMENT_FAILURE_MESSAGES = {
11289
+ card_declined: "Your card was declined. Try again or use a different payment method.",
11290
+ insufficient_funds: "Your card has insufficient funds. Try a different payment method.",
11291
+ expired_card: "Your card has expired. Use a different payment method.",
11292
+ incorrect_cvc: "Your card\u2019s security code is incorrect. Try again or use a different card.",
11293
+ incorrect_number: "Your card number is incorrect. Try again or use a different card.",
11294
+ card_not_supported: "This card isn\u2019t supported for this purchase. Try a different payment method.",
11295
+ processing_error: "We couldn\u2019t process your card. Please try again in a moment.",
11296
+ authentication_required: "Your bank requires authentication for this payment. Please try again.",
11297
+ payment_intent_authentication_failure: "Card authentication failed. Try again or use a different card.",
11298
+ payment_method_provider_decline: "Your payment provider declined the charge. Try a different payment method.",
11299
+ payment_failed: "Your payment didn\u2019t go through. Try again or use a different payment method."
11300
+ };
11301
+ function describeStripePaymentFailure(reason) {
11302
+ if (!reason) return null;
11303
+ return STRIPE_PAYMENT_FAILURE_MESSAGES[reason] ?? null;
11304
+ }
11305
+ var PAYMENT_FAILED_LEAD = "We couldn\u2019t complete your payment.";
11306
+ function unmappedPaymentFailureMessage(stripeMessage) {
11307
+ return stripeMessage ? `${PAYMENT_FAILED_LEAD} ${stripeMessage}` : `${PAYMENT_FAILED_LEAD} Please try again.`;
11308
+ }
11277
11309
  var NO_AUTOFILL = {
11278
11310
  autoComplete: "off",
11279
11311
  "data-lpignore": "true",
@@ -11375,7 +11407,11 @@ function PayWithStripeLink({
11375
11407
  }, [controlledStep]);
11376
11408
  (0, import_react17.useEffect)(() => {
11377
11409
  setLoading(false);
11378
- setError(null);
11410
+ if (preserveErrorOnNextStepRef.current) {
11411
+ preserveErrorOnNextStepRef.current = false;
11412
+ } else {
11413
+ setError(null);
11414
+ }
11379
11415
  setKycPendingMessage(null);
11380
11416
  setQuoteLoading(false);
11381
11417
  if (step !== "checkout") {
@@ -11464,7 +11500,10 @@ function PayWithStripeLink({
11464
11500
  const pendingAddPaymentRef = (0, import_react17.useRef)(false);
11465
11501
  const autoOpenedAddPaymentRef = (0, import_react17.useRef)(false);
11466
11502
  const checkoutGenRef = (0, import_react17.useRef)(0);
11503
+ const preserveErrorOnNextStepRef = (0, import_react17.useRef)(false);
11504
+ const spentSessionIdsRef = (0, import_react17.useRef)(/* @__PURE__ */ new Set());
11467
11505
  const [stripePaymentUIReady, setStripePaymentUIReady] = (0, import_react17.useState)(false);
11506
+ const [addPaymentHovered, setAddPaymentHovered] = (0, import_react17.useState)(false);
11468
11507
  const [oauthToken, setOauthToken] = (0, import_react17.useState)(null);
11469
11508
  const [refreshTokenValue, setRefreshTokenValue] = (0, import_react17.useState)(null);
11470
11509
  const [customerId, setCustomerId] = (0, import_react17.useState)(null);
@@ -11613,7 +11652,7 @@ function PayWithStripeLink({
11613
11652
  const merged = [
11614
11653
  ...pmResult.data.map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) })),
11615
11654
  ...persistedNotInApi ? [persisted] : []
11616
- ];
11655
+ ].filter((t13) => !deadPaymentTokenIdsRef.current.has(t13.id));
11617
11656
  if (merged.length > 0) {
11618
11657
  const preferred = persisted && merged.find((m) => m.id === persisted.id) || merged[0];
11619
11658
  selectPaymentToken(preferred);
@@ -11678,6 +11717,7 @@ function PayWithStripeLink({
11678
11717
  const [selectedPaymentToken, setSelectedPaymentToken] = (0, import_react17.useState)(null);
11679
11718
  const [paymentDisplay, setPaymentDisplay] = (0, import_react17.useState)(null);
11680
11719
  const selectedTokenSingleUseRef = (0, import_react17.useRef)(false);
11720
+ const deadPaymentTokenIdsRef = (0, import_react17.useRef)(/* @__PURE__ */ new Set());
11681
11721
  const selectPaymentToken = (0, import_react17.useCallback)(
11682
11722
  (token) => {
11683
11723
  selectedTokenSingleUseRef.current = !!token.singleUse;
@@ -11705,9 +11745,25 @@ function PayWithStripeLink({
11705
11745
  setStoredSelectedTokenState(null);
11706
11746
  }
11707
11747
  }, [customerId]);
11748
+ const forgetSelectedPaymentToken = (0, import_react17.useCallback)(
11749
+ (tokenId) => {
11750
+ selectedTokenSingleUseRef.current = false;
11751
+ if (customerId) {
11752
+ const persisted = getStoredSelectedToken(customerId);
11753
+ if (!tokenId || persisted?.id === tokenId) {
11754
+ clearStoredSelectedToken(customerId);
11755
+ }
11756
+ }
11757
+ setSelectedPaymentToken((prev) => !tokenId || prev === tokenId ? null : prev);
11758
+ setPaymentDisplay((prev) => !tokenId || !prev ? null : prev);
11759
+ setStoredSelectedTokenState((prev) => !tokenId || prev?.id === tokenId ? null : prev);
11760
+ },
11761
+ [customerId]
11762
+ );
11708
11763
  const displayPaymentTokens = (() => {
11709
- const apiDisplay = paymentTokens.map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) }));
11710
- if (storedSelectedToken && !paymentTokens.some((t13) => t13.id === storedSelectedToken.id)) {
11764
+ const isDead = (id) => deadPaymentTokenIdsRef.current.has(id);
11765
+ const apiDisplay = paymentTokens.filter((t13) => !isDead(t13.id)).map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) }));
11766
+ if (storedSelectedToken && !isDead(storedSelectedToken.id) && !paymentTokens.some((t13) => t13.id === storedSelectedToken.id)) {
11711
11767
  return [...apiDisplay, storedSelectedToken];
11712
11768
  }
11713
11769
  return apiDisplay;
@@ -12356,7 +12412,7 @@ function PayWithStripeLink({
12356
12412
  const merged = [
12357
12413
  ...existing.data.map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) })),
12358
12414
  ...storedNotInApi ? [stored] : []
12359
- ];
12415
+ ].filter((t13) => !deadPaymentTokenIdsRef.current.has(t13.id));
12360
12416
  if (merged.length === 0) {
12361
12417
  if (!autoOpenedAddPaymentRef.current) {
12362
12418
  autoOpenedAddPaymentRef.current = true;
@@ -12398,6 +12454,9 @@ function PayWithStripeLink({
12398
12454
  paymentInnerRef.current.replaceChildren();
12399
12455
  };
12400
12456
  }, [step, oauthToken, customerId, publishableKey, coordinator]);
12457
+ (0, import_react17.useEffect)(() => {
12458
+ if (step !== "payment") setAddPaymentHovered(false);
12459
+ }, [step]);
12401
12460
  (0, import_react17.useEffect)(() => {
12402
12461
  if (step !== "add_payment") return;
12403
12462
  if (!isCustomerVerified(customer)) {
@@ -12548,6 +12607,17 @@ function PayWithStripeLink({
12548
12607
  }
12549
12608
  if (message) onDepositError?.({ message, error: err });
12550
12609
  };
12610
+ const isRetriablePaymentToken = (id) => !!id && paymentTokens.some((t13) => t13.id === id);
12611
+ const dropConsumedToken = (message, cause) => {
12612
+ if (selectedPaymentToken) deadPaymentTokenIdsRef.current.add(selectedPaymentToken);
12613
+ setSessionForCheckout(null);
12614
+ forgetSelectedPaymentToken(selectedPaymentToken);
12615
+ preserveErrorOnNextStepRef.current = true;
12616
+ setError(message);
12617
+ setStep("amount");
12618
+ onDepositError?.({ message, error: cause });
12619
+ };
12620
+ const CONSUMED_TOKEN_MESSAGE = "That payment method can\u2019t be used. Please choose another.";
12551
12621
  if (!sessionForCheckout || sessionForCheckout.amount !== amount || sessionForCheckout.paymentToken !== selectedPaymentToken) {
12552
12622
  return;
12553
12623
  }
@@ -12555,19 +12625,29 @@ function PayWithStripeLink({
12555
12625
  setLoading(true);
12556
12626
  setError(null);
12557
12627
  setStep("checkout");
12628
+ let lastConfirmError = null;
12558
12629
  try {
12559
- const runCheckout = (sid) => coordinator.performCheckout(sid, async (onrampSessionId) => {
12560
- try {
12561
- await withTokenRefresh(
12562
- (tok) => (0, import_core21.stripeRefreshQuote)(onrampSessionId, tok, publishableKey)
12563
- );
12564
- } catch {
12565
- }
12566
- const confirmResponse = await withTokenRefresh(
12567
- (tok) => (0, import_core21.stripeConfirmSession)(onrampSessionId, tok, {}, publishableKey)
12568
- );
12569
- return confirmResponse.client_secret ?? "";
12570
- });
12630
+ const runCheckout = (sid) => {
12631
+ spentSessionIdsRef.current.add(sid);
12632
+ return coordinator.performCheckout(sid, async (onrampSessionId) => {
12633
+ try {
12634
+ await withTokenRefresh(
12635
+ (tok) => (0, import_core21.stripeRefreshQuote)(onrampSessionId, tok, publishableKey)
12636
+ );
12637
+ } catch {
12638
+ }
12639
+ try {
12640
+ const confirmResponse = await withTokenRefresh(
12641
+ (tok) => (0, import_core21.stripeConfirmSession)(onrampSessionId, tok, {}, publishableKey)
12642
+ );
12643
+ lastConfirmError = null;
12644
+ return confirmResponse.client_secret ?? "";
12645
+ } catch (confirmErr) {
12646
+ lastConfirmError = confirmErr;
12647
+ throw confirmErr;
12648
+ }
12649
+ });
12650
+ };
12571
12651
  const lastErrorFor = async (sid) => {
12572
12652
  const sessionState = await withTokenRefresh(
12573
12653
  (tok) => (0, import_core21.stripeGetSession)(sid, tok, publishableKey)
@@ -12627,8 +12707,9 @@ function PayWithStripeLink({
12627
12707
  setStep("success");
12628
12708
  } else if (lastError === "quote_rate_drifted" || lastError === "charged_with_expired_quote") {
12629
12709
  setSessionForCheckout(null);
12630
- setStep("amount");
12710
+ preserveErrorOnNextStepRef.current = true;
12631
12711
  setError("Pricing updated. Please try again.");
12712
+ setStep("amount");
12632
12713
  } else if (lastError === "missing_document_verification") {
12633
12714
  surfaceStepUpOnAmount(void 0, void 0, "l2");
12634
12715
  } else if (lastError === "transaction_limit_reached") {
@@ -12639,28 +12720,52 @@ function PayWithStripeLink({
12639
12720
  } else if (lastError === "missing_consumer_wallet") {
12640
12721
  setStep("wallet");
12641
12722
  setError(null);
12723
+ } else if (isConsumedPaymentMethodError(lastConfirmError)) {
12724
+ dropConsumedToken(CONSUMED_TOKEN_MESSAGE, lastConfirmError);
12642
12725
  } else {
12643
- setSessionForCheckout(null);
12644
- setError(lastError ?? "Checkout failed. Please try again.");
12645
- setStep("amount");
12726
+ const confirmErr = lastConfirmError instanceof import_core21.StripeApiResponseError ? lastConfirmError : void 0;
12727
+ const reason = describeStripePaymentFailure(lastError) ?? describeStripePaymentFailure(confirmErr?.stripeCode) ?? unmappedPaymentFailureMessage(confirmErr?.stripeMessage);
12728
+ if (!isRetriablePaymentToken(selectedPaymentToken)) {
12729
+ dropConsumedToken(reason, lastConfirmError ?? void 0);
12730
+ } else {
12731
+ setSessionForCheckout(null);
12732
+ preserveErrorOnNextStepRef.current = true;
12733
+ setError(reason);
12734
+ setStep("amount");
12735
+ onDepositError?.({
12736
+ message: lastError ?? confirmErr?.stripeCode ?? "checkout_failed",
12737
+ error: lastConfirmError ?? void 0
12738
+ });
12739
+ }
12646
12740
  }
12647
12741
  } catch (err) {
12648
12742
  if (isStale()) return;
12649
- const msg = err instanceof Error ? err.message : "Checkout failed";
12650
- const errorType = err instanceof import_core21.StripeApiResponseError ? err.errorType : void 0;
12651
- if (errorType === "stripe_onramp_missing_minimum_identity_verification") {
12743
+ const stripeErr = lastConfirmError instanceof import_core21.StripeApiResponseError ? lastConfirmError : err instanceof import_core21.StripeApiResponseError ? err : void 0;
12744
+ const effectiveError = stripeErr ?? err;
12745
+ const msg = stripeErr?.message ?? (err instanceof Error ? err.message : "Checkout failed");
12746
+ const errorType = stripeErr?.errorType;
12747
+ if (isConsumedPaymentMethodError(effectiveError)) {
12748
+ dropConsumedToken(CONSUMED_TOKEN_MESSAGE, effectiveError);
12749
+ } else if (errorType === "stripe_onramp_missing_minimum_identity_verification") {
12652
12750
  setStep("kyc");
12653
12751
  setError(null);
12654
12752
  } else if (errorType === "stripe_onramp_missing_identity_verification") {
12655
- surfaceStepUpOnAmount(err, msg, "l1");
12753
+ surfaceStepUpOnAmount(effectiveError, msg, "l1");
12656
12754
  } else if (errorType === "stripe_onramp_missing_document_verification") {
12657
- surfaceStepUpOnAmount(err, msg, "l2");
12755
+ surfaceStepUpOnAmount(effectiveError, msg, "l2");
12658
12756
  } else if (errorType === "stripe_onramp_purchase_limit_reached") {
12659
- surfaceStepUpOnAmount(err, msg, null);
12757
+ surfaceStepUpOnAmount(effectiveError, msg, null);
12660
12758
  } else {
12661
- setError(msg);
12662
- setStep("amount");
12663
- onDepositError?.({ message: msg, error: err });
12759
+ const reason = describeStripePaymentFailure(stripeErr?.stripeCode) ?? (stripeErr?.stripeMessage ? unmappedPaymentFailureMessage(stripeErr.stripeMessage) : msg || unmappedPaymentFailureMessage());
12760
+ if (!isRetriablePaymentToken(selectedPaymentToken)) {
12761
+ dropConsumedToken(reason, effectiveError);
12762
+ } else {
12763
+ setSessionForCheckout(null);
12764
+ preserveErrorOnNextStepRef.current = true;
12765
+ setError(reason);
12766
+ setStep("amount");
12767
+ onDepositError?.({ message: msg || "checkout_failed", error: effectiveError });
12768
+ }
12664
12769
  }
12665
12770
  } finally {
12666
12771
  setLoading(false);
@@ -12686,7 +12791,7 @@ function PayWithStripeLink({
12686
12791
  const merged = [
12687
12792
  ...existing.data.map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) })),
12688
12793
  ...stored && !existing.data.some((t13) => t13.id === stored.id) ? [stored] : []
12689
- ];
12794
+ ].filter((t13) => !deadPaymentTokenIdsRef.current.has(t13.id));
12690
12795
  if (merged.length > 0) {
12691
12796
  const preferred = stored && merged.find((m) => m.id === stored.id) || merged[0];
12692
12797
  selectPaymentToken(preferred);
@@ -12765,7 +12870,7 @@ function PayWithStripeLink({
12765
12870
  (0, import_react17.useEffect)(() => {
12766
12871
  if (step === "review") {
12767
12872
  const existing = sessionForCheckoutRef.current;
12768
- const hasFreshSession = !!(existing && existing.amount === amount && existing.paymentToken === selectedPaymentToken);
12873
+ const hasFreshSession = !!(existing && existing.amount === amount && existing.paymentToken === selectedPaymentToken && !spentSessionIdsRef.current.has(existing.id));
12769
12874
  if (quoteNonce === quoteNonceAtReviewRef.current && hasFreshSession) return;
12770
12875
  quoteNonceAtReviewRef.current = quoteNonce;
12771
12876
  } else if (step !== "amount") {
@@ -12789,7 +12894,9 @@ function PayWithStripeLink({
12789
12894
  const hasPaymentMethod = !!(isAuthenticated && selectedPaymentToken && onrampWalletAddress && isOnrampWalletRegistered);
12790
12895
  if (hasPaymentMethod) {
12791
12896
  const existing = sessionForCheckoutRef.current;
12792
- const canRefreshExisting = step === "review" && existing != null && existing.amount === amount && existing.paymentToken === selectedPaymentToken;
12897
+ const canRefreshExisting = step === "review" && existing != null && existing.amount === amount && existing.paymentToken === selectedPaymentToken && // Never refresh (and thereby reuse) a session already checked out —
12898
+ // its PaymentIntent is spent. Build a fresh one instead.
12899
+ !spentSessionIdsRef.current.has(existing.id);
12793
12900
  const session = canRefreshExisting ? await withTokenRefresh((tok) => (0, import_core21.stripeRefreshQuote)(existing.id, tok, publishableKey)) : await withTokenRefresh(
12794
12901
  (tok) => (0, import_core21.stripeCreateSession)(
12795
12902
  {
@@ -12917,7 +13024,7 @@ function PayWithStripeLink({
12917
13024
  const exceedsReachedLimit = limitReachedAmount !== null && hasInput && amountNum >= limitReachedAmount;
12918
13025
  const exceedsLimit = exceedsKnownLimit || exceedsReachedLimit || requiredStepUp !== null;
12919
13026
  const isValidAmount = hasInput && amountNum >= 5 && !exceedsLimit;
12920
- const hasValidSession = !!(sessionForCheckout && sessionForCheckout.amount === amount && sessionForCheckout.paymentToken === selectedPaymentToken);
13027
+ const hasValidSession = !!(sessionForCheckout && sessionForCheckout.amount === amount && sessionForCheckout.paymentToken === selectedPaymentToken && !spentSessionIdsRef.current.has(sessionForCheckout.id));
12921
13028
  const canCheckoutDirectly = !!(oauthToken && customerId && selectedPaymentToken && isOnrampWalletRegistered);
12922
13029
  const isPreparingSession = canCheckoutDirectly && isValidAmount && !quoteError && (quoteLoading || !hasValidSession);
12923
13030
  const custTiers = customer?.kyc_tiers ?? [];
@@ -13434,7 +13541,7 @@ function PayWithStripeLink({
13434
13541
  {
13435
13542
  onClick: kycFormMode === "name" ? handleNameNext : handleKycSubmit,
13436
13543
  disabled: loading || (kycFormMode === "name" ? !kycForm.givenName || !kycForm.surname : kycFormMode === "address" ? !kycForm.addressLine1 || !kycForm.addressCity || !kycForm.addressState || !kycForm.addressPostalCode : !kycForm.ssn || !kycForm.dob.day || !kycForm.dob.month || !kycForm.dob.year),
13437
- className: "uf-flex-1 uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
13544
+ className: "uf-flex-1 uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
13438
13545
  style: {
13439
13546
  backgroundColor: components.button.primaryBackground,
13440
13547
  fontFamily: fonts.medium,
@@ -13493,7 +13600,17 @@ function PayWithStripeLink({
13493
13600
  className: "uf-w-full uf-flex-1 uf-overflow-y-auto uf-overflow-x-hidden"
13494
13601
  }
13495
13602
  ),
13496
- !stripePaymentUIReady && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "uf-flex uf-items-center uf-justify-center uf-py-8", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react25.Loader2, { className: "uf-w-8 uf-h-8 uf-animate-spin", style: { color: colors2.primary } }) }),
13603
+ !stripePaymentUIReady && /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "uf-flex uf-flex-col uf-items-center uf-justify-center uf-gap-3 uf-py-10", children: [
13604
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react25.Loader2, { className: "uf-w-8 uf-h-8 uf-animate-spin", style: { color: colors2.primary } }),
13605
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13606
+ "p",
13607
+ {
13608
+ className: "uf-text-sm uf-text-center",
13609
+ style: { color: colors2.foregroundMuted, fontFamily: fonts.regular },
13610
+ children: "Setting up secure payment\u2026"
13611
+ }
13612
+ )
13613
+ ] }),
13497
13614
  error && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13498
13615
  "div",
13499
13616
  {
@@ -13600,35 +13717,14 @@ function PayWithStripeLink({
13600
13717
  isCustomerVerified(customer) && /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "uf-px-1", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
13601
13718
  "button",
13602
13719
  {
13603
- onClick: async () => {
13604
- if (coordinator && sdkAuthenticatedRef.current) {
13605
- setStep("add_payment");
13606
- return;
13607
- }
13608
- pendingAddPaymentRef.current = true;
13609
- if (accessTokenRef.current && email) {
13610
- setLoading(true);
13611
- try {
13612
- await withTokenRefresh(
13613
- (tok) => (0, import_core21.stripeGetCustomer)(customerId, tok, publishableKey)
13614
- );
13615
- const sdk = await initialize();
13616
- if (!sdk) throw new Error("SDK failed to load");
13617
- const authResult = await (0, import_core21.stripeCreateAuthIntent)(email, publishableKey);
13618
- setAuthIntentId(authResult.auth_intent_id);
13619
- setStep("auth");
13620
- } catch {
13621
- setStep("email");
13622
- } finally {
13623
- setLoading(false);
13624
- }
13625
- return;
13626
- }
13627
- setStep("email");
13720
+ onClick: () => {
13721
+ setStep("add_payment");
13628
13722
  },
13723
+ onMouseEnter: () => setAddPaymentHovered(true),
13724
+ onMouseLeave: () => setAddPaymentHovered(false),
13629
13725
  className: "uf-w-full uf-p-3 uf-flex uf-items-center uf-gap-3 uf-transition-colors",
13630
13726
  style: {
13631
- backgroundColor: components.card.backgroundColor,
13727
+ backgroundColor: addPaymentHovered ? colors2.cardHover : components.card.backgroundColor,
13632
13728
  borderRadius: components.card.borderRadius,
13633
13729
  border: `${components.card.borderWidth}px solid ${components.card.borderColor}`
13634
13730
  },
@@ -13743,7 +13839,7 @@ function PayWithStripeLink({
13743
13839
  {
13744
13840
  onClick: () => handleEmailSubmit(),
13745
13841
  disabled: loading || !email.trim(),
13746
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
13842
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
13747
13843
  style: {
13748
13844
  backgroundColor: components.button.primaryBackground,
13749
13845
  fontFamily: fonts.medium,
@@ -13972,7 +14068,7 @@ function PayWithStripeLink({
13972
14068
  {
13973
14069
  onClick: handleRegisterSubmit,
13974
14070
  disabled: loading || phone.length !== 10,
13975
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
14071
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
13976
14072
  style: {
13977
14073
  backgroundColor: components.button.primaryBackground,
13978
14074
  fontFamily: fonts.medium,
@@ -14106,7 +14202,9 @@ function PayWithStripeLink({
14106
14202
  );
14107
14203
  }
14108
14204
  if (step === "amount") {
14109
- const paymentSelector = oauthToken && customerId && isCustomerVerified(customer) ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
14205
+ const canSelectPaymentMethod = !!(oauthToken && customerId && isCustomerVerified(customer));
14206
+ const needsPaymentMethodSelection = canSelectPaymentMethod && !selectedPaymentToken;
14207
+ const paymentSelector = canSelectPaymentMethod ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
14110
14208
  "button",
14111
14209
  {
14112
14210
  onClick: () => setStep("payment"),
@@ -14180,6 +14278,35 @@ function PayWithStripeLink({
14180
14278
  style: { backgroundColor: colors2.background },
14181
14279
  children: [
14182
14280
  /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "uf-mb-6 uf-pt-2", children: [
14281
+ error && /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
14282
+ "div",
14283
+ {
14284
+ className: "uf-flex uf-items-start uf-gap-2 uf-mx-1 uf-mb-4 uf-px-3 uf-py-2.5",
14285
+ role: "alert",
14286
+ style: {
14287
+ backgroundColor: `${colors2.error}14`,
14288
+ border: `1px solid ${colors2.error}40`,
14289
+ borderRadius: components.card.borderRadius
14290
+ },
14291
+ children: [
14292
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
14293
+ import_lucide_react25.AlertTriangle,
14294
+ {
14295
+ className: "uf-w-4 uf-h-4 uf-mt-0.5 uf-shrink-0",
14296
+ style: { color: colors2.error }
14297
+ }
14298
+ ),
14299
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
14300
+ "span",
14301
+ {
14302
+ className: "uf-text-xs uf-text-left",
14303
+ style: { color: colors2.error, fontFamily: fonts.regular },
14304
+ children: error
14305
+ }
14306
+ )
14307
+ ]
14308
+ }
14309
+ ),
14183
14310
  paymentSelector,
14184
14311
  /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "uf-text-center uf-mb-4", children: [
14185
14312
  /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
@@ -14216,7 +14343,7 @@ function PayWithStripeLink({
14216
14343
  children: amount && amountNum < 5 && !error ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { style: { color: colors2.error }, children: "Minimum amount is $5" }) : exceedsLimit && canRaiseLimit && !error ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("span", { style: { color: colors2.foregroundMuted }, children: [
14217
14344
  purchaseLimit !== null ? `Limit $${purchaseLimit.toLocaleString()} \xB7 ` : "Limit reached \xB7 ",
14218
14345
  "Verify to raise"
14219
- ] }) : exceedsLimit && !error ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { style: { color: colors2.error }, children: purchaseLimit !== null ? `Limit $${purchaseLimit.toLocaleString()} \xB7 Try less` : "Limit reached \xB7 Try less" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { style: { color: colors2.error }, children: error }) : quoteError ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { style: { color: colors2.error }, children: quoteError }) : quoteLoading ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
14346
+ ] }) : exceedsLimit && !error ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { style: { color: colors2.error }, children: purchaseLimit !== null ? `Limit $${purchaseLimit.toLocaleString()} \xB7 Try less` : "Limit reached \xB7 Try less" }) : quoteError ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { style: { color: colors2.error }, children: quoteError }) : quoteLoading ? /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
14220
14347
  /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
14221
14348
  import_lucide_react25.Loader2,
14222
14349
  {
@@ -14306,7 +14433,7 @@ function PayWithStripeLink({
14306
14433
  {
14307
14434
  onClick: startLimitStepUp,
14308
14435
  disabled: loading,
14309
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
14436
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
14310
14437
  style: {
14311
14438
  backgroundColor: components.button.primaryBackground,
14312
14439
  fontFamily: fonts.medium,
@@ -14318,8 +14445,8 @@ function PayWithStripeLink({
14318
14445
  "button",
14319
14446
  {
14320
14447
  onClick: handleAmountContinue,
14321
- disabled: loading || !isValidAmount || !quote || isPreparingSession,
14322
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
14448
+ disabled: loading || !isValidAmount || !quote || isPreparingSession || needsPaymentMethodSelection,
14449
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
14323
14450
  style: {
14324
14451
  backgroundColor: components.button.primaryBackground,
14325
14452
  fontFamily: fonts.medium,
@@ -14449,7 +14576,7 @@ function PayWithStripeLink({
14449
14576
  {
14450
14577
  onClick: isBuyNow ? handleCheckout : handleReviewContinue,
14451
14578
  disabled: loading || !quote || isBuyNow && !hasValidSession,
14452
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
14579
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
14453
14580
  style: {
14454
14581
  backgroundColor: components.button.primaryBackground,
14455
14582
  fontFamily: fonts.medium,
@@ -21246,7 +21373,7 @@ function WalletConnect({
21246
21373
  const chainType = activeDepositWallet?.chain_type ?? "ethereum";
21247
21374
  const recipientAddress = activeDepositWallet?.address ?? "";
21248
21375
  const isCheckoutMode = !!checkoutAmountUsd;
21249
- const supportedChainType = chainType === "algorand" || chainType === "xrpl" || chainType === "cardano" || chainType === "n1" ? "ethereum" : chainType;
21376
+ const supportedChainType = chainType === "algorand" || chainType === "xrpl" || chainType === "cardano" || chainType === "n1" || chainType === "tron" ? "ethereum" : chainType;
21250
21377
  const transitionTo = React35.useCallback((nextView) => {
21251
21378
  if (nextView === viewRef.current) return;
21252
21379
  setIsTransitioning(true);
@@ -21595,7 +21722,7 @@ function WalletConnect({
21595
21722
  let cancelled = false;
21596
21723
  setIsLoading(true);
21597
21724
  setError(null);
21598
- const sct = activeDepositWallet.chain_type === "algorand" || activeDepositWallet.chain_type === "xrpl" || activeDepositWallet.chain_type === "cardano" || activeDepositWallet.chain_type === "n1" ? "ethereum" : activeDepositWallet.chain_type;
21725
+ const sct = activeDepositWallet.chain_type === "algorand" || activeDepositWallet.chain_type === "xrpl" || activeDepositWallet.chain_type === "cardano" || activeDepositWallet.chain_type === "n1" || activeDepositWallet.chain_type === "tron" ? "ethereum" : activeDepositWallet.chain_type;
21599
21726
  (0, import_core37.getAddressBalances)(activeWalletInfo.address, sct, publishableKey).then((response) => {
21600
21727
  if (cancelled) return;
21601
21728
  const nonZero = response.balances.filter((b) => b.amount !== "0");
package/dist/index.mjs CHANGED
@@ -6638,7 +6638,7 @@ function BankTransfer({
6638
6638
  subdivision_code: userIpInfo?.subdivisionCode || void 0,
6639
6639
  // A provider may advertise multiple rails; default to the first.
6640
6640
  // UIs that want per-rail rows would need to render one entry per element.
6641
- payment_method: provider.payment_methods[0]
6641
+ payment_method_type: provider.payment_methods[0]
6642
6642
  });
6643
6643
  setActiveProvider(provider);
6644
6644
  const convertedPrefilled = await resolvePrefilledSourceAmount(provider.source_currency);
@@ -11218,6 +11218,13 @@ function setStoredSelectedToken(customerId, token) {
11218
11218
  } catch {
11219
11219
  }
11220
11220
  }
11221
+ function clearStoredSelectedToken(customerId) {
11222
+ try {
11223
+ if (typeof window === "undefined" || !customerId) return;
11224
+ localStorage.removeItem(selectedTokenKey(customerId));
11225
+ } catch {
11226
+ }
11227
+ }
11221
11228
 
11222
11229
  // src/components/deposits/stripe-link/PayWithStripeLink.tsx
11223
11230
  import { Fragment as Fragment6, jsx as jsx44, jsxs as jsxs41 } from "react/jsx-runtime";
@@ -11235,6 +11242,31 @@ var STRIPE_SESSION_STATUS = {
11235
11242
  EXPIRED: "expired"
11236
11243
  };
11237
11244
  var STRIPE_SESSION_POLL_INTERVAL_MS = 3e3;
11245
+ var STRIPE_CONSUMED_PAYMENT_METHOD_CODE = "payment_method_unexpected_state";
11246
+ function isConsumedPaymentMethodError(err) {
11247
+ return err instanceof StripeApiResponseError && (err.errorType === "stripe_onramp_payment_method_consumed" || err.stripeCode === STRIPE_CONSUMED_PAYMENT_METHOD_CODE);
11248
+ }
11249
+ var STRIPE_PAYMENT_FAILURE_MESSAGES = {
11250
+ card_declined: "Your card was declined. Try again or use a different payment method.",
11251
+ insufficient_funds: "Your card has insufficient funds. Try a different payment method.",
11252
+ expired_card: "Your card has expired. Use a different payment method.",
11253
+ incorrect_cvc: "Your card\u2019s security code is incorrect. Try again or use a different card.",
11254
+ incorrect_number: "Your card number is incorrect. Try again or use a different card.",
11255
+ card_not_supported: "This card isn\u2019t supported for this purchase. Try a different payment method.",
11256
+ processing_error: "We couldn\u2019t process your card. Please try again in a moment.",
11257
+ authentication_required: "Your bank requires authentication for this payment. Please try again.",
11258
+ payment_intent_authentication_failure: "Card authentication failed. Try again or use a different card.",
11259
+ payment_method_provider_decline: "Your payment provider declined the charge. Try a different payment method.",
11260
+ payment_failed: "Your payment didn\u2019t go through. Try again or use a different payment method."
11261
+ };
11262
+ function describeStripePaymentFailure(reason) {
11263
+ if (!reason) return null;
11264
+ return STRIPE_PAYMENT_FAILURE_MESSAGES[reason] ?? null;
11265
+ }
11266
+ var PAYMENT_FAILED_LEAD = "We couldn\u2019t complete your payment.";
11267
+ function unmappedPaymentFailureMessage(stripeMessage) {
11268
+ return stripeMessage ? `${PAYMENT_FAILED_LEAD} ${stripeMessage}` : `${PAYMENT_FAILED_LEAD} Please try again.`;
11269
+ }
11238
11270
  var NO_AUTOFILL = {
11239
11271
  autoComplete: "off",
11240
11272
  "data-lpignore": "true",
@@ -11336,7 +11368,11 @@ function PayWithStripeLink({
11336
11368
  }, [controlledStep]);
11337
11369
  useEffect24(() => {
11338
11370
  setLoading(false);
11339
- setError(null);
11371
+ if (preserveErrorOnNextStepRef.current) {
11372
+ preserveErrorOnNextStepRef.current = false;
11373
+ } else {
11374
+ setError(null);
11375
+ }
11340
11376
  setKycPendingMessage(null);
11341
11377
  setQuoteLoading(false);
11342
11378
  if (step !== "checkout") {
@@ -11425,7 +11461,10 @@ function PayWithStripeLink({
11425
11461
  const pendingAddPaymentRef = useRef9(false);
11426
11462
  const autoOpenedAddPaymentRef = useRef9(false);
11427
11463
  const checkoutGenRef = useRef9(0);
11464
+ const preserveErrorOnNextStepRef = useRef9(false);
11465
+ const spentSessionIdsRef = useRef9(/* @__PURE__ */ new Set());
11428
11466
  const [stripePaymentUIReady, setStripePaymentUIReady] = useState29(false);
11467
+ const [addPaymentHovered, setAddPaymentHovered] = useState29(false);
11429
11468
  const [oauthToken, setOauthToken] = useState29(null);
11430
11469
  const [refreshTokenValue, setRefreshTokenValue] = useState29(null);
11431
11470
  const [customerId, setCustomerId] = useState29(null);
@@ -11574,7 +11613,7 @@ function PayWithStripeLink({
11574
11613
  const merged = [
11575
11614
  ...pmResult.data.map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) })),
11576
11615
  ...persistedNotInApi ? [persisted] : []
11577
- ];
11616
+ ].filter((t13) => !deadPaymentTokenIdsRef.current.has(t13.id));
11578
11617
  if (merged.length > 0) {
11579
11618
  const preferred = persisted && merged.find((m) => m.id === persisted.id) || merged[0];
11580
11619
  selectPaymentToken(preferred);
@@ -11639,6 +11678,7 @@ function PayWithStripeLink({
11639
11678
  const [selectedPaymentToken, setSelectedPaymentToken] = useState29(null);
11640
11679
  const [paymentDisplay, setPaymentDisplay] = useState29(null);
11641
11680
  const selectedTokenSingleUseRef = useRef9(false);
11681
+ const deadPaymentTokenIdsRef = useRef9(/* @__PURE__ */ new Set());
11642
11682
  const selectPaymentToken = useCallback6(
11643
11683
  (token) => {
11644
11684
  selectedTokenSingleUseRef.current = !!token.singleUse;
@@ -11666,9 +11706,25 @@ function PayWithStripeLink({
11666
11706
  setStoredSelectedTokenState(null);
11667
11707
  }
11668
11708
  }, [customerId]);
11709
+ const forgetSelectedPaymentToken = useCallback6(
11710
+ (tokenId) => {
11711
+ selectedTokenSingleUseRef.current = false;
11712
+ if (customerId) {
11713
+ const persisted = getStoredSelectedToken(customerId);
11714
+ if (!tokenId || persisted?.id === tokenId) {
11715
+ clearStoredSelectedToken(customerId);
11716
+ }
11717
+ }
11718
+ setSelectedPaymentToken((prev) => !tokenId || prev === tokenId ? null : prev);
11719
+ setPaymentDisplay((prev) => !tokenId || !prev ? null : prev);
11720
+ setStoredSelectedTokenState((prev) => !tokenId || prev?.id === tokenId ? null : prev);
11721
+ },
11722
+ [customerId]
11723
+ );
11669
11724
  const displayPaymentTokens = (() => {
11670
- const apiDisplay = paymentTokens.map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) }));
11671
- if (storedSelectedToken && !paymentTokens.some((t13) => t13.id === storedSelectedToken.id)) {
11725
+ const isDead = (id) => deadPaymentTokenIdsRef.current.has(id);
11726
+ const apiDisplay = paymentTokens.filter((t13) => !isDead(t13.id)).map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) }));
11727
+ if (storedSelectedToken && !isDead(storedSelectedToken.id) && !paymentTokens.some((t13) => t13.id === storedSelectedToken.id)) {
11672
11728
  return [...apiDisplay, storedSelectedToken];
11673
11729
  }
11674
11730
  return apiDisplay;
@@ -12317,7 +12373,7 @@ function PayWithStripeLink({
12317
12373
  const merged = [
12318
12374
  ...existing.data.map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) })),
12319
12375
  ...storedNotInApi ? [stored] : []
12320
- ];
12376
+ ].filter((t13) => !deadPaymentTokenIdsRef.current.has(t13.id));
12321
12377
  if (merged.length === 0) {
12322
12378
  if (!autoOpenedAddPaymentRef.current) {
12323
12379
  autoOpenedAddPaymentRef.current = true;
@@ -12359,6 +12415,9 @@ function PayWithStripeLink({
12359
12415
  paymentInnerRef.current.replaceChildren();
12360
12416
  };
12361
12417
  }, [step, oauthToken, customerId, publishableKey, coordinator]);
12418
+ useEffect24(() => {
12419
+ if (step !== "payment") setAddPaymentHovered(false);
12420
+ }, [step]);
12362
12421
  useEffect24(() => {
12363
12422
  if (step !== "add_payment") return;
12364
12423
  if (!isCustomerVerified(customer)) {
@@ -12509,6 +12568,17 @@ function PayWithStripeLink({
12509
12568
  }
12510
12569
  if (message) onDepositError?.({ message, error: err });
12511
12570
  };
12571
+ const isRetriablePaymentToken = (id) => !!id && paymentTokens.some((t13) => t13.id === id);
12572
+ const dropConsumedToken = (message, cause) => {
12573
+ if (selectedPaymentToken) deadPaymentTokenIdsRef.current.add(selectedPaymentToken);
12574
+ setSessionForCheckout(null);
12575
+ forgetSelectedPaymentToken(selectedPaymentToken);
12576
+ preserveErrorOnNextStepRef.current = true;
12577
+ setError(message);
12578
+ setStep("amount");
12579
+ onDepositError?.({ message, error: cause });
12580
+ };
12581
+ const CONSUMED_TOKEN_MESSAGE = "That payment method can\u2019t be used. Please choose another.";
12512
12582
  if (!sessionForCheckout || sessionForCheckout.amount !== amount || sessionForCheckout.paymentToken !== selectedPaymentToken) {
12513
12583
  return;
12514
12584
  }
@@ -12516,19 +12586,29 @@ function PayWithStripeLink({
12516
12586
  setLoading(true);
12517
12587
  setError(null);
12518
12588
  setStep("checkout");
12589
+ let lastConfirmError = null;
12519
12590
  try {
12520
- const runCheckout = (sid) => coordinator.performCheckout(sid, async (onrampSessionId) => {
12521
- try {
12522
- await withTokenRefresh(
12523
- (tok) => stripeRefreshQuote(onrampSessionId, tok, publishableKey)
12524
- );
12525
- } catch {
12526
- }
12527
- const confirmResponse = await withTokenRefresh(
12528
- (tok) => stripeConfirmSession(onrampSessionId, tok, {}, publishableKey)
12529
- );
12530
- return confirmResponse.client_secret ?? "";
12531
- });
12591
+ const runCheckout = (sid) => {
12592
+ spentSessionIdsRef.current.add(sid);
12593
+ return coordinator.performCheckout(sid, async (onrampSessionId) => {
12594
+ try {
12595
+ await withTokenRefresh(
12596
+ (tok) => stripeRefreshQuote(onrampSessionId, tok, publishableKey)
12597
+ );
12598
+ } catch {
12599
+ }
12600
+ try {
12601
+ const confirmResponse = await withTokenRefresh(
12602
+ (tok) => stripeConfirmSession(onrampSessionId, tok, {}, publishableKey)
12603
+ );
12604
+ lastConfirmError = null;
12605
+ return confirmResponse.client_secret ?? "";
12606
+ } catch (confirmErr) {
12607
+ lastConfirmError = confirmErr;
12608
+ throw confirmErr;
12609
+ }
12610
+ });
12611
+ };
12532
12612
  const lastErrorFor = async (sid) => {
12533
12613
  const sessionState = await withTokenRefresh(
12534
12614
  (tok) => stripeGetSession(sid, tok, publishableKey)
@@ -12588,8 +12668,9 @@ function PayWithStripeLink({
12588
12668
  setStep("success");
12589
12669
  } else if (lastError === "quote_rate_drifted" || lastError === "charged_with_expired_quote") {
12590
12670
  setSessionForCheckout(null);
12591
- setStep("amount");
12671
+ preserveErrorOnNextStepRef.current = true;
12592
12672
  setError("Pricing updated. Please try again.");
12673
+ setStep("amount");
12593
12674
  } else if (lastError === "missing_document_verification") {
12594
12675
  surfaceStepUpOnAmount(void 0, void 0, "l2");
12595
12676
  } else if (lastError === "transaction_limit_reached") {
@@ -12600,28 +12681,52 @@ function PayWithStripeLink({
12600
12681
  } else if (lastError === "missing_consumer_wallet") {
12601
12682
  setStep("wallet");
12602
12683
  setError(null);
12684
+ } else if (isConsumedPaymentMethodError(lastConfirmError)) {
12685
+ dropConsumedToken(CONSUMED_TOKEN_MESSAGE, lastConfirmError);
12603
12686
  } else {
12604
- setSessionForCheckout(null);
12605
- setError(lastError ?? "Checkout failed. Please try again.");
12606
- setStep("amount");
12687
+ const confirmErr = lastConfirmError instanceof StripeApiResponseError ? lastConfirmError : void 0;
12688
+ const reason = describeStripePaymentFailure(lastError) ?? describeStripePaymentFailure(confirmErr?.stripeCode) ?? unmappedPaymentFailureMessage(confirmErr?.stripeMessage);
12689
+ if (!isRetriablePaymentToken(selectedPaymentToken)) {
12690
+ dropConsumedToken(reason, lastConfirmError ?? void 0);
12691
+ } else {
12692
+ setSessionForCheckout(null);
12693
+ preserveErrorOnNextStepRef.current = true;
12694
+ setError(reason);
12695
+ setStep("amount");
12696
+ onDepositError?.({
12697
+ message: lastError ?? confirmErr?.stripeCode ?? "checkout_failed",
12698
+ error: lastConfirmError ?? void 0
12699
+ });
12700
+ }
12607
12701
  }
12608
12702
  } catch (err) {
12609
12703
  if (isStale()) return;
12610
- const msg = err instanceof Error ? err.message : "Checkout failed";
12611
- const errorType = err instanceof StripeApiResponseError ? err.errorType : void 0;
12612
- if (errorType === "stripe_onramp_missing_minimum_identity_verification") {
12704
+ const stripeErr = lastConfirmError instanceof StripeApiResponseError ? lastConfirmError : err instanceof StripeApiResponseError ? err : void 0;
12705
+ const effectiveError = stripeErr ?? err;
12706
+ const msg = stripeErr?.message ?? (err instanceof Error ? err.message : "Checkout failed");
12707
+ const errorType = stripeErr?.errorType;
12708
+ if (isConsumedPaymentMethodError(effectiveError)) {
12709
+ dropConsumedToken(CONSUMED_TOKEN_MESSAGE, effectiveError);
12710
+ } else if (errorType === "stripe_onramp_missing_minimum_identity_verification") {
12613
12711
  setStep("kyc");
12614
12712
  setError(null);
12615
12713
  } else if (errorType === "stripe_onramp_missing_identity_verification") {
12616
- surfaceStepUpOnAmount(err, msg, "l1");
12714
+ surfaceStepUpOnAmount(effectiveError, msg, "l1");
12617
12715
  } else if (errorType === "stripe_onramp_missing_document_verification") {
12618
- surfaceStepUpOnAmount(err, msg, "l2");
12716
+ surfaceStepUpOnAmount(effectiveError, msg, "l2");
12619
12717
  } else if (errorType === "stripe_onramp_purchase_limit_reached") {
12620
- surfaceStepUpOnAmount(err, msg, null);
12718
+ surfaceStepUpOnAmount(effectiveError, msg, null);
12621
12719
  } else {
12622
- setError(msg);
12623
- setStep("amount");
12624
- onDepositError?.({ message: msg, error: err });
12720
+ const reason = describeStripePaymentFailure(stripeErr?.stripeCode) ?? (stripeErr?.stripeMessage ? unmappedPaymentFailureMessage(stripeErr.stripeMessage) : msg || unmappedPaymentFailureMessage());
12721
+ if (!isRetriablePaymentToken(selectedPaymentToken)) {
12722
+ dropConsumedToken(reason, effectiveError);
12723
+ } else {
12724
+ setSessionForCheckout(null);
12725
+ preserveErrorOnNextStepRef.current = true;
12726
+ setError(reason);
12727
+ setStep("amount");
12728
+ onDepositError?.({ message: msg || "checkout_failed", error: effectiveError });
12729
+ }
12625
12730
  }
12626
12731
  } finally {
12627
12732
  setLoading(false);
@@ -12647,7 +12752,7 @@ function PayWithStripeLink({
12647
12752
  const merged = [
12648
12753
  ...existing.data.map((t13) => ({ id: t13.id, ...formatPaymentToken(t13) })),
12649
12754
  ...stored && !existing.data.some((t13) => t13.id === stored.id) ? [stored] : []
12650
- ];
12755
+ ].filter((t13) => !deadPaymentTokenIdsRef.current.has(t13.id));
12651
12756
  if (merged.length > 0) {
12652
12757
  const preferred = stored && merged.find((m) => m.id === stored.id) || merged[0];
12653
12758
  selectPaymentToken(preferred);
@@ -12726,7 +12831,7 @@ function PayWithStripeLink({
12726
12831
  useEffect24(() => {
12727
12832
  if (step === "review") {
12728
12833
  const existing = sessionForCheckoutRef.current;
12729
- const hasFreshSession = !!(existing && existing.amount === amount && existing.paymentToken === selectedPaymentToken);
12834
+ const hasFreshSession = !!(existing && existing.amount === amount && existing.paymentToken === selectedPaymentToken && !spentSessionIdsRef.current.has(existing.id));
12730
12835
  if (quoteNonce === quoteNonceAtReviewRef.current && hasFreshSession) return;
12731
12836
  quoteNonceAtReviewRef.current = quoteNonce;
12732
12837
  } else if (step !== "amount") {
@@ -12750,7 +12855,9 @@ function PayWithStripeLink({
12750
12855
  const hasPaymentMethod = !!(isAuthenticated && selectedPaymentToken && onrampWalletAddress && isOnrampWalletRegistered);
12751
12856
  if (hasPaymentMethod) {
12752
12857
  const existing = sessionForCheckoutRef.current;
12753
- const canRefreshExisting = step === "review" && existing != null && existing.amount === amount && existing.paymentToken === selectedPaymentToken;
12858
+ const canRefreshExisting = step === "review" && existing != null && existing.amount === amount && existing.paymentToken === selectedPaymentToken && // Never refresh (and thereby reuse) a session already checked out —
12859
+ // its PaymentIntent is spent. Build a fresh one instead.
12860
+ !spentSessionIdsRef.current.has(existing.id);
12754
12861
  const session = canRefreshExisting ? await withTokenRefresh((tok) => stripeRefreshQuote(existing.id, tok, publishableKey)) : await withTokenRefresh(
12755
12862
  (tok) => stripeCreateSession(
12756
12863
  {
@@ -12878,7 +12985,7 @@ function PayWithStripeLink({
12878
12985
  const exceedsReachedLimit = limitReachedAmount !== null && hasInput && amountNum >= limitReachedAmount;
12879
12986
  const exceedsLimit = exceedsKnownLimit || exceedsReachedLimit || requiredStepUp !== null;
12880
12987
  const isValidAmount = hasInput && amountNum >= 5 && !exceedsLimit;
12881
- const hasValidSession = !!(sessionForCheckout && sessionForCheckout.amount === amount && sessionForCheckout.paymentToken === selectedPaymentToken);
12988
+ const hasValidSession = !!(sessionForCheckout && sessionForCheckout.amount === amount && sessionForCheckout.paymentToken === selectedPaymentToken && !spentSessionIdsRef.current.has(sessionForCheckout.id));
12882
12989
  const canCheckoutDirectly = !!(oauthToken && customerId && selectedPaymentToken && isOnrampWalletRegistered);
12883
12990
  const isPreparingSession = canCheckoutDirectly && isValidAmount && !quoteError && (quoteLoading || !hasValidSession);
12884
12991
  const custTiers = customer?.kyc_tiers ?? [];
@@ -13395,7 +13502,7 @@ function PayWithStripeLink({
13395
13502
  {
13396
13503
  onClick: kycFormMode === "name" ? handleNameNext : handleKycSubmit,
13397
13504
  disabled: loading || (kycFormMode === "name" ? !kycForm.givenName || !kycForm.surname : kycFormMode === "address" ? !kycForm.addressLine1 || !kycForm.addressCity || !kycForm.addressState || !kycForm.addressPostalCode : !kycForm.ssn || !kycForm.dob.day || !kycForm.dob.month || !kycForm.dob.year),
13398
- className: "uf-flex-1 uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
13505
+ className: "uf-flex-1 uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
13399
13506
  style: {
13400
13507
  backgroundColor: components.button.primaryBackground,
13401
13508
  fontFamily: fonts.medium,
@@ -13454,7 +13561,17 @@ function PayWithStripeLink({
13454
13561
  className: "uf-w-full uf-flex-1 uf-overflow-y-auto uf-overflow-x-hidden"
13455
13562
  }
13456
13563
  ),
13457
- !stripePaymentUIReady && /* @__PURE__ */ jsx44("div", { className: "uf-flex uf-items-center uf-justify-center uf-py-8", children: /* @__PURE__ */ jsx44(Loader25, { className: "uf-w-8 uf-h-8 uf-animate-spin", style: { color: colors2.primary } }) }),
13564
+ !stripePaymentUIReady && /* @__PURE__ */ jsxs41("div", { className: "uf-flex uf-flex-col uf-items-center uf-justify-center uf-gap-3 uf-py-10", children: [
13565
+ /* @__PURE__ */ jsx44(Loader25, { className: "uf-w-8 uf-h-8 uf-animate-spin", style: { color: colors2.primary } }),
13566
+ /* @__PURE__ */ jsx44(
13567
+ "p",
13568
+ {
13569
+ className: "uf-text-sm uf-text-center",
13570
+ style: { color: colors2.foregroundMuted, fontFamily: fonts.regular },
13571
+ children: "Setting up secure payment\u2026"
13572
+ }
13573
+ )
13574
+ ] }),
13458
13575
  error && /* @__PURE__ */ jsx44(
13459
13576
  "div",
13460
13577
  {
@@ -13561,35 +13678,14 @@ function PayWithStripeLink({
13561
13678
  isCustomerVerified(customer) && /* @__PURE__ */ jsx44("div", { className: "uf-px-1", children: /* @__PURE__ */ jsxs41(
13562
13679
  "button",
13563
13680
  {
13564
- onClick: async () => {
13565
- if (coordinator && sdkAuthenticatedRef.current) {
13566
- setStep("add_payment");
13567
- return;
13568
- }
13569
- pendingAddPaymentRef.current = true;
13570
- if (accessTokenRef.current && email) {
13571
- setLoading(true);
13572
- try {
13573
- await withTokenRefresh(
13574
- (tok) => stripeGetCustomer(customerId, tok, publishableKey)
13575
- );
13576
- const sdk = await initialize();
13577
- if (!sdk) throw new Error("SDK failed to load");
13578
- const authResult = await stripeCreateAuthIntent(email, publishableKey);
13579
- setAuthIntentId(authResult.auth_intent_id);
13580
- setStep("auth");
13581
- } catch {
13582
- setStep("email");
13583
- } finally {
13584
- setLoading(false);
13585
- }
13586
- return;
13587
- }
13588
- setStep("email");
13681
+ onClick: () => {
13682
+ setStep("add_payment");
13589
13683
  },
13684
+ onMouseEnter: () => setAddPaymentHovered(true),
13685
+ onMouseLeave: () => setAddPaymentHovered(false),
13590
13686
  className: "uf-w-full uf-p-3 uf-flex uf-items-center uf-gap-3 uf-transition-colors",
13591
13687
  style: {
13592
- backgroundColor: components.card.backgroundColor,
13688
+ backgroundColor: addPaymentHovered ? colors2.cardHover : components.card.backgroundColor,
13593
13689
  borderRadius: components.card.borderRadius,
13594
13690
  border: `${components.card.borderWidth}px solid ${components.card.borderColor}`
13595
13691
  },
@@ -13704,7 +13800,7 @@ function PayWithStripeLink({
13704
13800
  {
13705
13801
  onClick: () => handleEmailSubmit(),
13706
13802
  disabled: loading || !email.trim(),
13707
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
13803
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
13708
13804
  style: {
13709
13805
  backgroundColor: components.button.primaryBackground,
13710
13806
  fontFamily: fonts.medium,
@@ -13933,7 +14029,7 @@ function PayWithStripeLink({
13933
14029
  {
13934
14030
  onClick: handleRegisterSubmit,
13935
14031
  disabled: loading || phone.length !== 10,
13936
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
14032
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
13937
14033
  style: {
13938
14034
  backgroundColor: components.button.primaryBackground,
13939
14035
  fontFamily: fonts.medium,
@@ -14067,7 +14163,9 @@ function PayWithStripeLink({
14067
14163
  );
14068
14164
  }
14069
14165
  if (step === "amount") {
14070
- const paymentSelector = oauthToken && customerId && isCustomerVerified(customer) ? /* @__PURE__ */ jsxs41(
14166
+ const canSelectPaymentMethod = !!(oauthToken && customerId && isCustomerVerified(customer));
14167
+ const needsPaymentMethodSelection = canSelectPaymentMethod && !selectedPaymentToken;
14168
+ const paymentSelector = canSelectPaymentMethod ? /* @__PURE__ */ jsxs41(
14071
14169
  "button",
14072
14170
  {
14073
14171
  onClick: () => setStep("payment"),
@@ -14141,6 +14239,35 @@ function PayWithStripeLink({
14141
14239
  style: { backgroundColor: colors2.background },
14142
14240
  children: [
14143
14241
  /* @__PURE__ */ jsxs41("div", { className: "uf-mb-6 uf-pt-2", children: [
14242
+ error && /* @__PURE__ */ jsxs41(
14243
+ "div",
14244
+ {
14245
+ className: "uf-flex uf-items-start uf-gap-2 uf-mx-1 uf-mb-4 uf-px-3 uf-py-2.5",
14246
+ role: "alert",
14247
+ style: {
14248
+ backgroundColor: `${colors2.error}14`,
14249
+ border: `1px solid ${colors2.error}40`,
14250
+ borderRadius: components.card.borderRadius
14251
+ },
14252
+ children: [
14253
+ /* @__PURE__ */ jsx44(
14254
+ AlertTriangle2,
14255
+ {
14256
+ className: "uf-w-4 uf-h-4 uf-mt-0.5 uf-shrink-0",
14257
+ style: { color: colors2.error }
14258
+ }
14259
+ ),
14260
+ /* @__PURE__ */ jsx44(
14261
+ "span",
14262
+ {
14263
+ className: "uf-text-xs uf-text-left",
14264
+ style: { color: colors2.error, fontFamily: fonts.regular },
14265
+ children: error
14266
+ }
14267
+ )
14268
+ ]
14269
+ }
14270
+ ),
14144
14271
  paymentSelector,
14145
14272
  /* @__PURE__ */ jsxs41("div", { className: "uf-text-center uf-mb-4", children: [
14146
14273
  /* @__PURE__ */ jsx44(
@@ -14177,7 +14304,7 @@ function PayWithStripeLink({
14177
14304
  children: amount && amountNum < 5 && !error ? /* @__PURE__ */ jsx44("span", { style: { color: colors2.error }, children: "Minimum amount is $5" }) : exceedsLimit && canRaiseLimit && !error ? /* @__PURE__ */ jsxs41("span", { style: { color: colors2.foregroundMuted }, children: [
14178
14305
  purchaseLimit !== null ? `Limit $${purchaseLimit.toLocaleString()} \xB7 ` : "Limit reached \xB7 ",
14179
14306
  "Verify to raise"
14180
- ] }) : exceedsLimit && !error ? /* @__PURE__ */ jsx44("span", { style: { color: colors2.error }, children: purchaseLimit !== null ? `Limit $${purchaseLimit.toLocaleString()} \xB7 Try less` : "Limit reached \xB7 Try less" }) : error ? /* @__PURE__ */ jsx44("span", { style: { color: colors2.error }, children: error }) : quoteError ? /* @__PURE__ */ jsx44("span", { style: { color: colors2.error }, children: quoteError }) : quoteLoading ? /* @__PURE__ */ jsxs41(Fragment6, { children: [
14307
+ ] }) : exceedsLimit && !error ? /* @__PURE__ */ jsx44("span", { style: { color: colors2.error }, children: purchaseLimit !== null ? `Limit $${purchaseLimit.toLocaleString()} \xB7 Try less` : "Limit reached \xB7 Try less" }) : quoteError ? /* @__PURE__ */ jsx44("span", { style: { color: colors2.error }, children: quoteError }) : quoteLoading ? /* @__PURE__ */ jsxs41(Fragment6, { children: [
14181
14308
  /* @__PURE__ */ jsx44(
14182
14309
  Loader25,
14183
14310
  {
@@ -14267,7 +14394,7 @@ function PayWithStripeLink({
14267
14394
  {
14268
14395
  onClick: startLimitStepUp,
14269
14396
  disabled: loading,
14270
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
14397
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
14271
14398
  style: {
14272
14399
  backgroundColor: components.button.primaryBackground,
14273
14400
  fontFamily: fonts.medium,
@@ -14279,8 +14406,8 @@ function PayWithStripeLink({
14279
14406
  "button",
14280
14407
  {
14281
14408
  onClick: handleAmountContinue,
14282
- disabled: loading || !isValidAmount || !quote || isPreparingSession,
14283
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
14409
+ disabled: loading || !isValidAmount || !quote || isPreparingSession || needsPaymentMethodSelection,
14410
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
14284
14411
  style: {
14285
14412
  backgroundColor: components.button.primaryBackground,
14286
14413
  fontFamily: fonts.medium,
@@ -14410,7 +14537,7 @@ function PayWithStripeLink({
14410
14537
  {
14411
14538
  onClick: isBuyNow ? handleCheckout : handleReviewContinue,
14412
14539
  disabled: loading || !quote || isBuyNow && !hasValidSession,
14413
- className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-colors disabled:uf-opacity-40",
14540
+ className: "uf-w-full uf-py-3.5 uf-text-white uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-40",
14414
14541
  style: {
14415
14542
  backgroundColor: components.button.primaryBackground,
14416
14543
  fontFamily: fonts.medium,
@@ -21278,7 +21405,7 @@ function WalletConnect({
21278
21405
  const chainType = activeDepositWallet?.chain_type ?? "ethereum";
21279
21406
  const recipientAddress = activeDepositWallet?.address ?? "";
21280
21407
  const isCheckoutMode = !!checkoutAmountUsd;
21281
- const supportedChainType = chainType === "algorand" || chainType === "xrpl" || chainType === "cardano" || chainType === "n1" ? "ethereum" : chainType;
21408
+ const supportedChainType = chainType === "algorand" || chainType === "xrpl" || chainType === "cardano" || chainType === "n1" || chainType === "tron" ? "ethereum" : chainType;
21282
21409
  const transitionTo = React35.useCallback((nextView) => {
21283
21410
  if (nextView === viewRef.current) return;
21284
21411
  setIsTransitioning(true);
@@ -21627,7 +21754,7 @@ function WalletConnect({
21627
21754
  let cancelled = false;
21628
21755
  setIsLoading(true);
21629
21756
  setError(null);
21630
- const sct = activeDepositWallet.chain_type === "algorand" || activeDepositWallet.chain_type === "xrpl" || activeDepositWallet.chain_type === "cardano" || activeDepositWallet.chain_type === "n1" ? "ethereum" : activeDepositWallet.chain_type;
21757
+ const sct = activeDepositWallet.chain_type === "algorand" || activeDepositWallet.chain_type === "xrpl" || activeDepositWallet.chain_type === "cardano" || activeDepositWallet.chain_type === "n1" || activeDepositWallet.chain_type === "tron" ? "ethereum" : activeDepositWallet.chain_type;
21631
21758
  getAddressBalances2(activeWalletInfo.address, sct, publishableKey).then((response) => {
21632
21759
  if (cancelled) return;
21633
21760
  const nonZero = response.balances.filter((b) => b.amount !== "0");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifold/ui-react",
3
- "version": "0.1.70-beta.1",
3
+ "version": "0.1.71-beta.1",
4
4
  "description": "Unifold UI React - Deposit and onramp components for React applications",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -42,7 +42,7 @@
42
42
  "mipd": "^0.0.7",
43
43
  "qr-code-styling": "^1.6.0-rc.1",
44
44
  "tailwind-merge": "^2.0.0",
45
- "@unifold/core": "0.1.70-beta.1"
45
+ "@unifold/core": "0.1.71-beta.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@solana/web3.js": "^1.87.0",