@unifold/ui-react 0.1.76 → 0.1.78

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
@@ -420,6 +420,9 @@ var en_default = {
420
420
  transactionBreakdown: "Transaction breakdown",
421
421
  networkFee: "Network fee",
422
422
  confirmOrder: "Confirm Order",
423
+ mfaEnrollmentTitle: "Enable SMS or Authy 2FA",
424
+ mfaEnrollmentMessage: "Please make sure you enable SMS or Authy 2FA in your Coinbase account. Press Continue after you enable it.",
425
+ mfaEnrollmentSettingsLink: "Open Coinbase security settings",
423
426
  termsNoticePrefix: "By clicking on Confirm Order, you agree to our ",
424
427
  termsNoticeLink: "terms",
425
428
  termsNoticeSuffix: "."
@@ -550,7 +553,12 @@ var en_default = {
550
553
  prefix: "By continuing, you agree to Coinbase's ",
551
554
  separator: ", ",
552
555
  lastSeparator: ", and ",
553
- suffix: ". Coinbase is the regulated provider of this fiat-to-crypto onramp."
556
+ suffix: ".",
557
+ agreements: {
558
+ guest_checkout_terms: "Terms of Service",
559
+ user_agreement: "User Agreement",
560
+ privacy_policy: "Privacy Policy"
561
+ }
554
562
  },
555
563
  errors: {
556
564
  rateLimited: "Too many attempts. Please wait a few minutes and try again.",
@@ -1211,6 +1219,9 @@ var zh_default = {
1211
1219
  transactionBreakdown: "\u4EA4\u6613\u660E\u7EC6",
1212
1220
  networkFee: "\u7F51\u7EDC\u624B\u7EED\u8D39",
1213
1221
  confirmOrder: "\u786E\u8BA4\u8BA2\u5355",
1222
+ mfaEnrollmentTitle: "\u8BF7\u542F\u7528\u77ED\u4FE1\u6216 Authy \u53CC\u91CD\u9A8C\u8BC1",
1223
+ mfaEnrollmentMessage: "\u8BF7\u786E\u8BA4\u60A8\u5DF2\u5728 Coinbase \u8D26\u6237\u4E2D\u542F\u7528\u77ED\u4FE1\u6216 Authy \u53CC\u91CD\u9A8C\u8BC1\u3002\u542F\u7528\u540E\u8BF7\u6309\u7EE7\u7EED\u3002",
1224
+ mfaEnrollmentSettingsLink: "\u6253\u5F00 Coinbase \u5B89\u5168\u8BBE\u7F6E",
1214
1225
  termsNoticePrefix: "\u70B9\u51FB\u201C\u786E\u8BA4\u8BA2\u5355\u201D\u5373\u8868\u793A\u60A8\u540C\u610F\u6211\u4EEC\u7684",
1215
1226
  termsNoticeLink: "\u6761\u6B3E",
1216
1227
  termsNoticeSuffix: "\u3002"
@@ -1341,7 +1352,12 @@ var zh_default = {
1341
1352
  prefix: "\u7EE7\u7EED\u5373\u8868\u793A\u60A8\u540C\u610F Coinbase \u7684",
1342
1353
  separator: "\u3001",
1343
1354
  lastSeparator: "\u548C",
1344
- suffix: "\u3002Coinbase \u662F\u672C\u6CD5\u5E01\u5151\u52A0\u5BC6\u8D27\u5E01\u5145\u503C\u670D\u52A1\u7684\u53D7\u76D1\u7BA1\u63D0\u4F9B\u65B9\u3002"
1355
+ suffix: "\u3002",
1356
+ agreements: {
1357
+ guest_checkout_terms: "\u670D\u52A1\u6761\u6B3E",
1358
+ user_agreement: "\u7528\u6237\u534F\u8BAE",
1359
+ privacy_policy: "\u9690\u79C1\u653F\u7B56"
1360
+ }
1345
1361
  },
1346
1362
  errors: {
1347
1363
  rateLimited: "\u5C1D\u8BD5\u6B21\u6570\u8FC7\u591A\u3002\u8BF7\u7A0D\u7B49\u51E0\u5206\u949F\u540E\u91CD\u8BD5\u3002",
@@ -2002,6 +2018,9 @@ var zh_TW_default = {
2002
2018
  transactionBreakdown: "\u4EA4\u6613\u660E\u7D30",
2003
2019
  networkFee: "\u7DB2\u8DEF\u624B\u7E8C\u8CBB",
2004
2020
  confirmOrder: "\u78BA\u8A8D\u8A02\u55AE",
2021
+ mfaEnrollmentTitle: "\u8ACB\u555F\u7528\u7C21\u8A0A\u6216 Authy \u96D9\u91CD\u9A57\u8B49",
2022
+ mfaEnrollmentMessage: "\u8ACB\u78BA\u8A8D\u60A8\u5DF2\u5728 Coinbase \u5E33\u6236\u4E2D\u555F\u7528\u7C21\u8A0A\u6216 Authy \u96D9\u91CD\u9A57\u8B49\u3002\u555F\u7528\u5F8C\u8ACB\u6309\u7E7C\u7E8C\u3002",
2023
+ mfaEnrollmentSettingsLink: "\u958B\u555F Coinbase \u5B89\u5168\u8A2D\u5B9A",
2005
2024
  termsNoticePrefix: "\u9EDE\u64CA\u300C\u78BA\u8A8D\u8A02\u55AE\u300D\u5373\u8868\u793A\u60A8\u540C\u610F\u6211\u5011\u7684",
2006
2025
  termsNoticeLink: "\u689D\u6B3E",
2007
2026
  termsNoticeSuffix: "\u3002"
@@ -2132,7 +2151,12 @@ var zh_TW_default = {
2132
2151
  prefix: "\u7E7C\u7E8C\u5373\u8868\u793A\u60A8\u540C\u610F Coinbase \u7684",
2133
2152
  separator: "\u3001",
2134
2153
  lastSeparator: "\u548C",
2135
- suffix: "\u3002Coinbase \u662F\u672C\u6CD5\u5E63\u514C\u52A0\u5BC6\u8CA8\u5E63\u5132\u503C\u670D\u52D9\u7684\u53D7\u76E3\u7BA1\u63D0\u4F9B\u65B9\u3002"
2154
+ suffix: "\u3002",
2155
+ agreements: {
2156
+ guest_checkout_terms: "\u670D\u52D9\u689D\u6B3E",
2157
+ user_agreement: "\u4F7F\u7528\u8005\u5354\u8B70",
2158
+ privacy_policy: "\u96B1\u79C1\u6B0A\u653F\u7B56"
2159
+ }
2136
2160
  },
2137
2161
  errors: {
2138
2162
  rateLimited: "\u5617\u8A66\u6B21\u6578\u904E\u591A\u3002\u8ACB\u7A0D\u7B49\u5E7E\u5206\u9418\u5F8C\u91CD\u8A66\u3002",
@@ -3071,13 +3095,15 @@ import {
3071
3095
  } from "@unifold/core";
3072
3096
 
3073
3097
  // src/hooks/use-deposit-address.ts
3074
- import { useEffect as useEffect3 } from "react";
3075
3098
  import { useQuery } from "@tanstack/react-query";
3076
3099
  import {
3077
3100
  createDepositAddress,
3078
3101
  isDepositAddressValidationError
3079
3102
  } from "@unifold/core";
3080
3103
 
3104
+ // src/hooks/use-internal-user-id.ts
3105
+ import { useEffect as useEffect3 } from "react";
3106
+
3081
3107
  // src/context/AnalyticsContext.tsx
3082
3108
  import * as React5 from "react";
3083
3109
  import { jsx as jsx5 } from "react/jsx-runtime";
@@ -3109,6 +3135,15 @@ function useAnalytics() {
3109
3135
  return { track, startSession, endSession, getSessionId, setUserId, subscribe };
3110
3136
  }
3111
3137
 
3138
+ // src/hooks/use-internal-user-id.ts
3139
+ function useStampInternalUserId({ internalUserId }) {
3140
+ const { setUserId } = useAnalytics();
3141
+ useEffect3(() => {
3142
+ if (!internalUserId) return;
3143
+ setUserId(internalUserId);
3144
+ }, [internalUserId, setUserId]);
3145
+ }
3146
+
3112
3147
  // src/hooks/use-deposit-address.ts
3113
3148
  function useDepositAddress(params) {
3114
3149
  const {
@@ -3122,7 +3157,6 @@ function useDepositAddress(params) {
3122
3157
  contractCalls,
3123
3158
  enabled = true
3124
3159
  } = params;
3125
- const { setUserId } = useAnalytics();
3126
3160
  const query = useQuery({
3127
3161
  queryKey: [
3128
3162
  "unifold",
@@ -3165,11 +3199,7 @@ function useDepositAddress(params) {
3165
3199
  retryDelay: (attempt) => Math.min(1e3 * 2 ** attempt, 1e4)
3166
3200
  // 1s, 2s, 4s (max 10s)
3167
3201
  });
3168
- useEffect3(() => {
3169
- if (query.data?.user_id) {
3170
- setUserId(query.data.user_id);
3171
- }
3172
- }, [query.data?.user_id, setUserId]);
3202
+ useStampInternalUserId({ internalUserId: query.data?.user_id });
3173
3203
  return query;
3174
3204
  }
3175
3205
 
@@ -5454,6 +5484,10 @@ function BuyWithCard({
5454
5484
  wallet_address: wallet.address,
5455
5485
  subdivision_code: userIpInfo?.subdivisionCode || void 0,
5456
5486
  external_id: externalId,
5487
+ // The rail this quote was priced for, so the provider opens on it. Most
5488
+ // are `card`, which is also the server default, but a local rail (e.g.
5489
+ // `upi` in India) is the whole reason its provider was quoted.
5490
+ payment_method_type: selectedProvider.payment_method_type,
5457
5491
  // Exactly one of source_amount / destination_amount (mutually exclusive):
5458
5492
  // destination mode starts the session by the fixed crypto amount (fees on
5459
5493
  // top); otherwise by the entered fiat amount.
@@ -5471,7 +5505,11 @@ function BuyWithCard({
5471
5505
  method: "card",
5472
5506
  flow_type: "deposit",
5473
5507
  provider: selectedProvider.service_provider,
5474
- amount_usd: amount
5508
+ amount_usd: amount,
5509
+ // The execution only exists once the provider's payout lands on this
5510
+ // wallet, and it carries this id — so it's the join key for a purchase
5511
+ // that starts here and never reaches flow_completed.
5512
+ deposit_wallet_id: depositWalletId
5475
5513
  });
5476
5514
  onEvent?.({
5477
5515
  id: generatePrefixedKSUID2("sevt"),
@@ -6983,6 +7021,8 @@ var BuyWithWalletPay = React8.forwardRef(
6983
7021
  const [limitUpgradeError, setLimitUpgradeError] = useState15(null);
6984
7022
  const [upgradeSubmitting, setUpgradeSubmitting] = useState15(false);
6985
7023
  const upgradeFlowAbortRef = useRef6(null);
7024
+ const applyLimitRoutingRef = useRef6(() => {
7025
+ });
6986
7026
  const [amount, setAmount] = useState15(amountUsd ?? "");
6987
7027
  const [preparedOrder, setPreparedOrder] = useState15(null);
6988
7028
  const sessionAbortRef = useRef6(null);
@@ -7117,6 +7157,14 @@ var BuyWithWalletPay = React8.forwardRef(
7117
7157
  },
7118
7158
  [email, normalizedPhone, userId]
7119
7159
  );
7160
+ const { action: initialAction, refetchWalletPayLimits } = useWalletPayInitialScreen({
7161
+ userId,
7162
+ publishableKey,
7163
+ normalizedPhone,
7164
+ userEmail,
7165
+ sessionPhoneVerified: verificationSession?.phone?.status === "verified",
7166
+ isWaiting: defaultTokenLoading || legalAgreementsLoading || userIpInfoLoading
7167
+ });
7120
7168
  const createSessionAndSendOtp = useCallback4(async () => {
7121
7169
  setView("submitting_session");
7122
7170
  setErrorMessage("");
@@ -7126,7 +7174,12 @@ var BuyWithWalletPay = React8.forwardRef(
7126
7174
  email,
7127
7175
  phone: normalizedPhone,
7128
7176
  terms_accepted: true,
7129
- email_already_verified: true
7177
+ // TODO: when the API honors `false`, this will trigger email OTP.
7178
+ // Today `false` still skips email (we don't verify it) and JWT ev
7179
+ // is null. Don't send `true` — that was a fake attest.
7180
+ email_already_verified: false
7181
+ // Coinbase stays Redis-only + JWT until bank-transfer identity
7182
+ // is live. Do not persist from this path yet.
7130
7183
  },
7131
7184
  publishableKey
7132
7185
  );
@@ -7136,6 +7189,34 @@ var BuyWithWalletPay = React8.forwardRef(
7136
7189
  }
7137
7190
  setClientSecret(session.client_secret);
7138
7191
  storeWalletPaySession();
7192
+ if (session.status === "verified") {
7193
+ const tokenRes = await exchangeOnrampVerificationToken(
7194
+ session.id,
7195
+ session.client_secret,
7196
+ publishableKey
7197
+ );
7198
+ setOnrampToken(tokenRes);
7199
+ storeWalletPaySession({
7200
+ emailVerifiedAt: session.email.verified_at,
7201
+ phoneVerifiedAt: session.phone.verified_at,
7202
+ onrampToken: tokenRes.token,
7203
+ onrampTokenExpiresAtMs: Date.now() + tokenRes.expires_in * 1e3
7204
+ });
7205
+ if (amountUsd) setAmount(amountUsd);
7206
+ try {
7207
+ const result = await refetchWalletPayLimits();
7208
+ if (result.data) {
7209
+ const nextView = getViewForLimitStatus(result.data);
7210
+ if (nextView) {
7211
+ applyLimitRoutingRef.current(nextView, result.data.upgrade_status);
7212
+ return;
7213
+ }
7214
+ }
7215
+ } catch {
7216
+ }
7217
+ setView("amount");
7218
+ return;
7219
+ }
7139
7220
  await sendOnrampVerificationOtp(session.id, "phone", session.client_secret, publishableKey);
7140
7221
  setResendCooldown(30);
7141
7222
  setView("phone_otp");
@@ -7144,19 +7225,19 @@ var BuyWithWalletPay = React8.forwardRef(
7144
7225
  setErrorMessage(prettifyVerificationError(raw));
7145
7226
  setView("phone_input");
7146
7227
  }
7147
- }, [email, normalizedPhone, publishableKey, storeWalletPaySession]);
7228
+ }, [
7229
+ email,
7230
+ normalizedPhone,
7231
+ publishableKey,
7232
+ storeWalletPaySession,
7233
+ userId,
7234
+ amountUsd,
7235
+ refetchWalletPayLimits
7236
+ ]);
7148
7237
  const clearUpgradeFields = useCallback4(() => {
7149
7238
  setSsnLast4("");
7150
7239
  setDobInput("");
7151
7240
  }, []);
7152
- const { action: initialAction, refetchWalletPayLimits } = useWalletPayInitialScreen({
7153
- userId,
7154
- publishableKey,
7155
- normalizedPhone,
7156
- userEmail,
7157
- sessionPhoneVerified: verificationSession?.phone?.status === "verified",
7158
- isWaiting: defaultTokenLoading || legalAgreementsLoading || userIpInfoLoading
7159
- });
7160
7241
  const pollLimitUpgradeStatus = useCallback4(
7161
7242
  async (signal) => {
7162
7243
  const POLL_INTERVAL_MS5 = 1500;
@@ -7267,6 +7348,7 @@ var BuyWithWalletPay = React8.forwardRef(
7267
7348
  },
7268
7349
  [startUpgradePolling]
7269
7350
  );
7351
+ applyLimitRoutingRef.current = applyLimitRouting;
7270
7352
  const hasInitializedRef = useRef6(false);
7271
7353
  useEffect12(() => {
7272
7354
  if (hasInitializedRef.current) return;
@@ -7496,7 +7578,10 @@ var BuyWithWalletPay = React8.forwardRef(
7496
7578
  track("flow_started", {
7497
7579
  method,
7498
7580
  flow_type: "deposit",
7499
- amount_usd: walletPayAmountUsd
7581
+ amount_usd: walletPayAmountUsd,
7582
+ // No execution yet — one appears when the payout lands on this
7583
+ // wallet, carrying the same id.
7584
+ deposit_wallet_id: depositWalletId
7500
7585
  });
7501
7586
  onEvent?.({
7502
7587
  id: generatePrefixedKSUID3("sevt"),
@@ -7546,6 +7631,7 @@ var BuyWithWalletPay = React8.forwardRef(
7546
7631
  defaultTokenLoading,
7547
7632
  defaultTokenError,
7548
7633
  depositWallet?.address,
7634
+ depositWalletId,
7549
7635
  userEmail,
7550
7636
  publishableKey,
7551
7637
  onEvent,
@@ -8520,7 +8606,7 @@ var FALLBACK_AGREEMENTS = [
8520
8606
  {
8521
8607
  key: "guest_checkout_terms",
8522
8608
  name: "Guest Checkout Terms of Service",
8523
- url: "https://www.coinbase.com/legal/cb-pay/onramp-guest-checkout-terms"
8609
+ url: "https://www.coinbase.com/legal/guest-checkout/us"
8524
8610
  },
8525
8611
  {
8526
8612
  key: "user_agreement",
@@ -8540,22 +8626,24 @@ function LegalDisclaimer({ legalAgreements, loading }) {
8540
8626
  return /* @__PURE__ */ jsxs14(
8541
8627
  "div",
8542
8628
  {
8543
- className: "uf-mt-3 uf-mb-3 uf-px-1 uf-space-y-1.5 uf-animate-pulse",
8629
+ className: "uf-mt-3 uf-mb-3 uf-px-1 uf-space-y-1.5 uf-animate-pulse uf-opacity-50",
8544
8630
  "aria-hidden": true,
8545
8631
  "aria-busy": "true",
8546
8632
  children: [
8547
- /* @__PURE__ */ jsx18("div", { className: "uf-h-2.5 uf-rounded uf-w-full", style: { backgroundColor: colors2.card } }),
8548
- /* @__PURE__ */ jsx18("div", { className: "uf-h-2.5 uf-rounded uf-w-11/12", style: { backgroundColor: colors2.card } }),
8549
- /* @__PURE__ */ jsx18("div", { className: "uf-h-2.5 uf-rounded uf-w-3/4", style: { backgroundColor: colors2.card } })
8633
+ /* @__PURE__ */ jsx18("div", { className: "uf-h-2 uf-rounded uf-w-full", style: { backgroundColor: colors2.card } }),
8634
+ /* @__PURE__ */ jsx18("div", { className: "uf-h-2 uf-rounded uf-w-11/12", style: { backgroundColor: colors2.card } }),
8635
+ /* @__PURE__ */ jsx18("div", { className: "uf-h-2 uf-rounded uf-w-3/4", style: { backgroundColor: colors2.card } })
8550
8636
  ]
8551
8637
  }
8552
8638
  );
8553
8639
  }
8554
8640
  const agreements = legalAgreements?.agreements ?? FALLBACK_AGREEMENTS;
8641
+ const shortLabels = t.agreements;
8642
+ const lastSeparator = t.lastSeparator.replace(/\s+$/, "\xA0");
8555
8643
  return /* @__PURE__ */ jsxs14(
8556
8644
  "p",
8557
8645
  {
8558
- className: "uf-text-[11px] uf-leading-relaxed uf-mt-3 uf-mb-3 uf-px-1",
8646
+ className: "uf-text-[10px] uf-leading-relaxed uf-mt-3 uf-mb-3 uf-opacity-60 uf-text-center",
8559
8647
  style: { color: colors2.foregroundMuted, fontFamily: fonts.regular },
8560
8648
  children: [
8561
8649
  t.prefix,
@@ -8566,12 +8654,12 @@ function LegalDisclaimer({ legalAgreements, loading }) {
8566
8654
  href: a.url,
8567
8655
  target: "_blank",
8568
8656
  rel: "noopener noreferrer",
8569
- className: "uf-underline",
8570
- style: { color: colors2.foreground },
8571
- children: a.name
8657
+ className: "uf-no-underline",
8658
+ style: { color: "inherit", textDecoration: "none", whiteSpace: "nowrap" },
8659
+ children: shortLabels[a.key] ?? a.name
8572
8660
  }
8573
8661
  ),
8574
- idx < arr.length - 2 ? t.separator : idx === arr.length - 2 ? t.lastSeparator : ""
8662
+ idx < arr.length - 2 ? t.separator : idx === arr.length - 2 ? lastSeparator : ""
8575
8663
  ] }, a.key)),
8576
8664
  t.suffix
8577
8665
  ]
@@ -9255,6 +9343,7 @@ function PayWithCashApp({
9255
9343
  destinationChainType,
9256
9344
  destinationChainId,
9257
9345
  destinationTokenAddress,
9346
+ contractCalls,
9258
9347
  cashAppIconUrl,
9259
9348
  view: controlledView,
9260
9349
  onViewChange,
@@ -9347,7 +9436,8 @@ function PayWithCashApp({
9347
9436
  destination_token_address: destinationTokenAddress,
9348
9437
  recipient_address: recipientAddress,
9349
9438
  source_amount: amount,
9350
- external_id: externalId
9439
+ external_id: externalId,
9440
+ ...contractCalls?.length ? { contract_calls: contractCalls } : {}
9351
9441
  },
9352
9442
  publishableKey
9353
9443
  );
@@ -9360,7 +9450,10 @@ function PayWithCashApp({
9360
9450
  track("flow_started", {
9361
9451
  method: "cashapp",
9362
9452
  flow_type: "deposit",
9363
- amount_usd: amount
9453
+ amount_usd: amount,
9454
+ // No execution yet — one appears when the payout lands on this
9455
+ // wallet, carrying the same id.
9456
+ deposit_wallet_id: depositWalletId
9364
9457
  });
9365
9458
  onEvent?.({
9366
9459
  id: generatePrefixedKSUID5("sevt"),
@@ -9385,6 +9478,8 @@ function PayWithCashApp({
9385
9478
  destinationChainType,
9386
9479
  destinationChainId,
9387
9480
  destinationTokenAddress,
9481
+ contractCalls,
9482
+ depositWalletId,
9388
9483
  onEvent,
9389
9484
  onDepositError,
9390
9485
  track,
@@ -10005,7 +10100,10 @@ function BankTransfer({
10005
10100
  method: "bank_transfer",
10006
10101
  flow_type: "deposit",
10007
10102
  provider: request.service_provider,
10008
- amount_usd: request.source_amount
10103
+ amount_usd: request.source_amount,
10104
+ // No execution yet — one appears when the provider's payout lands on
10105
+ // this wallet, carrying the same id.
10106
+ deposit_wallet_id: pollingWalletId
10009
10107
  });
10010
10108
  onEvent?.({
10011
10109
  id: generatePrefixedKSUID6("sevt"),
@@ -16270,7 +16368,11 @@ function PayWithStripeLink({
16270
16368
  if (checkoutResult.successful) {
16271
16369
  handledTerminalSessionRef.current = null;
16272
16370
  executionReconciledSessionRef.current = null;
16273
- track("flow_started", { method: "stripe_link", flow_type: "deposit" });
16371
+ track("flow_started", {
16372
+ method: "stripe_link",
16373
+ flow_type: "deposit",
16374
+ deposit_wallet_id: depositWalletId
16375
+ });
16274
16376
  setConfirmedSessionId(activeSessionId);
16275
16377
  setSessionStatus(STRIPE_SESSION_STATUS.FULFILLMENT_PROCESSING);
16276
16378
  if (selectedTokenSingleUseRef.current) clearSelectedPaymentToken();
@@ -17767,7 +17869,14 @@ function PayWithStripeLink({
17767
17869
  ]
17768
17870
  }
17769
17871
  ),
17770
- /* @__PURE__ */ jsx50("div", { ref: authMountRef, className: "uf-w-full uf-min-h-[200px]" })
17872
+ /* @__PURE__ */ jsx50(
17873
+ "div",
17874
+ {
17875
+ ref: authMountRef,
17876
+ className: "uf-w-full uf-min-h-[200px]",
17877
+ style: { colorScheme: "only light" }
17878
+ }
17879
+ )
17771
17880
  ]
17772
17881
  }
17773
17882
  );
@@ -19075,6 +19184,9 @@ function CoinbaseConnect({
19075
19184
  flow_type: "deposit",
19076
19185
  method: "exchange_connect",
19077
19186
  provider: IntegrationProvider.COINBASE,
19187
+ // No execution yet — one appears when the exchange's transfer lands on
19188
+ // this wallet, carrying the same id.
19189
+ deposit_wallet_id: transferDepositWalletId,
19078
19190
  amount_usd: sendAmount || void 0,
19079
19191
  currency: selectedAsset?.currency.toLowerCase().replace(/[^a-z0-9]+/g, "_"),
19080
19192
  chain: sourceChain?.chain_id,
@@ -19082,6 +19194,34 @@ function CoinbaseConnect({
19082
19194
  network: sourceChain?.network
19083
19195
  });
19084
19196
  };
19197
+ const applyConfirmResult = (result) => {
19198
+ if (result.status === "mfa_required") {
19199
+ track("verification_started", {
19200
+ flow_type: "deposit",
19201
+ method: "exchange_connect",
19202
+ provider: IntegrationProvider.COINBASE,
19203
+ verification_type: "mfa",
19204
+ verification_status: result.status
19205
+ });
19206
+ transitionTo("mfa");
19207
+ setMfaCode("");
19208
+ setMfaError(false);
19209
+ } else if (result.status === "mfa_enrollment_required") {
19210
+ transitionTo("mfa_enrollment");
19211
+ } else if (result.status === "completed") {
19212
+ trackTransferStarted();
19213
+ setConfirmResult(result);
19214
+ transitionTo("success");
19215
+ onTransferSuccess?.(result);
19216
+ } else {
19217
+ setErrorMessage(result.message || t.errors.transferFailed);
19218
+ transitionTo("error");
19219
+ onTransferError?.({
19220
+ message: result.message || t.errors.transferFailed,
19221
+ code: result.status
19222
+ });
19223
+ }
19224
+ };
19085
19225
  const handleConfirmTransfer = async () => {
19086
19226
  if (!transferIntent || !accessToken) return;
19087
19227
  setIsLoading(true);
@@ -19116,30 +19256,7 @@ function CoinbaseConnect({
19116
19256
  });
19117
19257
  throw err;
19118
19258
  });
19119
- if (result.status === "mfa_required") {
19120
- track("verification_started", {
19121
- flow_type: "deposit",
19122
- method: "exchange_connect",
19123
- provider: IntegrationProvider.COINBASE,
19124
- verification_type: "mfa",
19125
- verification_status: result.status
19126
- });
19127
- transitionTo("mfa");
19128
- setMfaCode("");
19129
- setMfaError(false);
19130
- } else if (result.status === "completed") {
19131
- trackTransferStarted();
19132
- setConfirmResult(result);
19133
- transitionTo("success");
19134
- onTransferSuccess?.(result);
19135
- } else {
19136
- setErrorMessage(result.message || t.errors.transferFailed);
19137
- transitionTo("error");
19138
- onTransferError?.({
19139
- message: result.message || t.errors.transferFailed,
19140
- code: result.status
19141
- });
19142
- }
19259
+ applyConfirmResult(result);
19143
19260
  } catch (err) {
19144
19261
  const message = err instanceof Error ? err.message : t.errors.transferFailed;
19145
19262
  setErrorMessage(message);
@@ -19296,6 +19413,9 @@ function CoinbaseConnect({
19296
19413
  setShowTransferDetails(false);
19297
19414
  transitionTo("enter_amount");
19298
19415
  break;
19416
+ case "mfa_enrollment":
19417
+ transitionTo("transfer_confirm");
19418
+ break;
19299
19419
  case "mfa":
19300
19420
  transitionTo("transfer_confirm");
19301
19421
  break;
@@ -20852,6 +20972,55 @@ function CoinbaseConnect({
20852
20972
  ] })
20853
20973
  ] });
20854
20974
  }
20975
+ if (view === "mfa_enrollment") {
20976
+ return /* @__PURE__ */ jsxs46(Fragment9, { children: [
20977
+ /* @__PURE__ */ jsx51(DepositHeader, { title: t.title, showBack: true, onBack: handleBack, onClose }),
20978
+ /* @__PURE__ */ jsxs46(
20979
+ "div",
20980
+ {
20981
+ className: "uf-flex uf-flex-col uf-items-center uf-px-4 uf-pb-4",
20982
+ style: viewTransitionStyle,
20983
+ children: [
20984
+ /* @__PURE__ */ jsx51(
20985
+ DepositErrorState,
20986
+ {
20987
+ title: t.transfer.mfaEnrollmentTitle,
20988
+ message: t.transfer.mfaEnrollmentMessage
20989
+ }
20990
+ ),
20991
+ /* @__PURE__ */ jsxs46(
20992
+ "a",
20993
+ {
20994
+ href: "https://www.coinbase.com/settings/security",
20995
+ target: "_blank",
20996
+ rel: "noopener noreferrer",
20997
+ className: "uf-text-sm uf-underline uf-mb-8 uf-inline-flex uf-items-center uf-gap-1",
20998
+ style: { color: colors2.primary, fontFamily: fonts.medium },
20999
+ children: [
21000
+ t.transfer.mfaEnrollmentSettingsLink,
21001
+ /* @__PURE__ */ jsx51(ExternalLink4, { className: "uf-w-3.5 uf-h-3.5" })
21002
+ ]
21003
+ }
21004
+ ),
21005
+ /* @__PURE__ */ jsx51(
21006
+ "button",
21007
+ {
21008
+ onClick: handleConfirmTransfer,
21009
+ disabled: isLoading,
21010
+ className: "uf-w-full uf-rounded-xl uf-py-3.5 uf-text-sm uf-font-medium uf-transition-all hover:uf-opacity-90 disabled:uf-opacity-50 uf-flex uf-items-center uf-justify-center uf-gap-2",
21011
+ style: {
21012
+ backgroundColor: colors2.primary,
21013
+ color: "#fff",
21014
+ fontFamily: fonts.medium
21015
+ },
21016
+ children: isLoading ? /* @__PURE__ */ jsx51(Loader27, { className: "uf-w-4 uf-h-4 uf-animate-spin" }) : t.continue
21017
+ }
21018
+ )
21019
+ ]
21020
+ }
21021
+ )
21022
+ ] });
21023
+ }
20855
21024
  if (view === "mfa") {
20856
21025
  return /* @__PURE__ */ jsxs46(Fragment9, { children: [
20857
21026
  /* @__PURE__ */ jsx51(DepositHeader, { title: t.title, showBack: true, onBack: handleBack, onClose }),
@@ -27011,7 +27180,11 @@ function WalletConnect({
27011
27180
  chain: String(token.chain_id),
27012
27181
  network: token.network,
27013
27182
  token: token.symbol ?? token.token_address,
27014
- currency: token.currency
27183
+ currency: token.currency,
27184
+ // No execution exists yet — it's created when this send lands on the
27185
+ // wallet. The wallet id is what that execution will carry, so it's
27186
+ // how a send that never completes is matched to what followed.
27187
+ deposit_wallet_id: activeDepositWallet?.id
27015
27188
  });
27016
27189
  setReceivedUsdAtSubmission(checkoutReceivedUsd ?? "0");
27017
27190
  setHasSignedTransaction(true);
@@ -27601,7 +27774,7 @@ function DepositModal({
27601
27774
  const applePaySubTitle = applePaySubTitleProp ?? t.applePayMenu.subtitle;
27602
27775
  const googlePayTitle = googlePayTitleProp ?? t.googlePayMenu.title;
27603
27776
  const googlePaySubTitle = googlePaySubTitleProp ?? t.googlePayMenu.subtitle;
27604
- const { track, startSession, endSession, getSessionId, subscribe } = useAnalytics();
27777
+ const { track, startSession, endSession, getSessionId, setUserId, subscribe } = useAnalytics();
27605
27778
  const normalizedPrefilledAmountUsd = useMemo17(
27606
27779
  () => normalizePrefilledUsdAmount(prefilledAmountUsd),
27607
27780
  [prefilledAmountUsd]
@@ -27609,6 +27782,7 @@ function DepositModal({
27609
27782
  const analyticsMethod = (method) => method === "transfer" ? "transfer_crypto" : method;
27610
27783
  const journeyActiveRef = useRef16(false);
27611
27784
  const journeySessionIdRef = useRef16(void 0);
27785
+ const journeyExternalUserIdRef = useRef16(void 0);
27612
27786
  const externalUserId = userId || void 0;
27613
27787
  const emitEvent = useCallback14(
27614
27788
  (event) => onEvent?.({ ...event, externalUserId }),
@@ -27636,6 +27810,9 @@ function DepositModal({
27636
27810
  method: analyticsMethod(method),
27637
27811
  flow_type: "deposit",
27638
27812
  amount_usd: execution?.destinationAmountUsd ?? execution?.sourceAmountUsd,
27813
+ // The one high-cardinality field we do send: the join key from a
27814
+ // funnel session back to the execution row it produced.
27815
+ execution_id: data.executionId ?? execution?.id,
27639
27816
  // Flat, low-cardinality execution dimensions for PostHog breakdowns
27640
27817
  // (precise/opaque fields like base units, addresses, tx hashes live in
27641
27818
  // the onEvent DirectExecution instead).
@@ -27663,6 +27840,7 @@ function DepositModal({
27663
27840
  method: analyticsMethod(method),
27664
27841
  flow_type: "deposit",
27665
27842
  error_code: error.code,
27843
+ execution_id: execution?.id,
27666
27844
  failure_reason: execution?.failureReason ?? void 0,
27667
27845
  status: execution?.status,
27668
27846
  source_chain_type: execution?.sourceChainType,
@@ -28091,8 +28269,10 @@ function DepositModal({
28091
28269
  const beginJourney = useCallback14(() => {
28092
28270
  journeySessionIdRef.current = startSession();
28093
28271
  journeyActiveRef.current = true;
28272
+ journeyExternalUserIdRef.current = userId;
28273
+ setUserId(null);
28094
28274
  track("widget_opened", { flow_type: "deposit" });
28095
- }, [startSession, track]);
28275
+ }, [startSession, track, setUserId, userId]);
28096
28276
  const handleClose = () => {
28097
28277
  finalizeJourney();
28098
28278
  onOpenChange(false);
@@ -28117,6 +28297,11 @@ function DepositModal({
28117
28297
  }
28118
28298
  beginJourney();
28119
28299
  }, [open]);
28300
+ useLayoutEffect2(() => {
28301
+ if (!open || journeyExternalUserIdRef.current === userId) return;
28302
+ journeyExternalUserIdRef.current = userId;
28303
+ setUserId(null);
28304
+ }, [open, userId, setUserId]);
28120
28305
  useLayoutEffect2(() => {
28121
28306
  if (!open) return;
28122
28307
  if (resetViewTimeoutRef.current) {
@@ -29028,6 +29213,7 @@ function DepositModal({
29028
29213
  destinationChainType,
29029
29214
  destinationChainId,
29030
29215
  destinationTokenAddress,
29216
+ contractCalls,
29031
29217
  cashAppIconUrl: projectConfig?.asset_cdn_url ? `${projectConfig.asset_cdn_url}/api/public/icons/onramps/svg/cashapp.svg` : void 0,
29032
29218
  view: cashAppView,
29033
29219
  onViewChange: setCashAppView,
@@ -29391,6 +29577,9 @@ function CheckoutModal({
29391
29577
  flowCompletedRef.current = true;
29392
29578
  track("flow_completed", {
29393
29579
  flow_type: "checkout",
29580
+ // Checkout has no direct execution; the intent is what a funnel
29581
+ // session joins back to.
29582
+ payment_intent_id: data.paymentIntentId,
29394
29583
  ...method ? { method: method === "transfer" ? "transfer_crypto" : method } : {}
29395
29584
  });
29396
29585
  }
@@ -29611,7 +29800,7 @@ function CheckoutModal({
29611
29800
  journeySessionIdRef.current = startSession();
29612
29801
  journeyActiveRef.current = true;
29613
29802
  flowCompletedRef.current = false;
29614
- setUserId(null);
29803
+ setUserId(paymentIntentUserId ?? null);
29615
29804
  track("widget_opened", { flow_type: "checkout" });
29616
29805
  if (resetViewTimeoutRef.current) {
29617
29806
  clearTimeout(resetViewTimeoutRef.current);
@@ -30052,7 +30241,7 @@ function CheckoutModal({
30052
30241
  }
30053
30242
 
30054
30243
  // src/components/withdrawals/WithdrawModal.tsx
30055
- import { useState as useState49, useEffect as useEffect48, useLayoutEffect as useLayoutEffect4, useCallback as useCallback17, useMemo as useMemo21, useRef as useRef20 } from "react";
30244
+ import { useState as useState49, useEffect as useEffect47, useLayoutEffect as useLayoutEffect4, useCallback as useCallback17, useMemo as useMemo21, useRef as useRef20 } from "react";
30056
30245
  import { AlertTriangle as AlertTriangle8, ChevronRight as ChevronRight22, Clock as Clock6 } from "lucide-react";
30057
30246
 
30058
30247
  // src/hooks/use-supported-destination-tokens.ts
@@ -30220,7 +30409,7 @@ import { useQuery as useQuery24 } from "@tanstack/react-query";
30220
30409
  import { queryExecutions as queryExecutions4, ActionType as ActionType4 } from "@unifold/core";
30221
30410
  function useExecutions(userId, publishableKey, options) {
30222
30411
  const actionType = options?.actionType ?? ActionType4.Deposit;
30223
- return useQuery24({
30412
+ const query = useQuery24({
30224
30413
  queryKey: ["unifold", "executions", actionType, userId, publishableKey],
30225
30414
  queryFn: () => queryExecutions4(userId, publishableKey, actionType),
30226
30415
  enabled: (options?.enabled ?? true) && !!userId,
@@ -30229,6 +30418,8 @@ function useExecutions(userId, publishableKey, options) {
30229
30418
  gcTime: 1e3 * 60 * 5,
30230
30419
  refetchOnWindowFocus: false
30231
30420
  });
30421
+ useStampInternalUserId({ internalUserId: query.data?.data[0]?.user_id });
30422
+ return query;
30232
30423
  }
30233
30424
 
30234
30425
  // src/hooks/use-withdraw-polling.ts
@@ -30570,7 +30761,7 @@ function WithdrawDoubleInput({
30570
30761
  }
30571
30762
 
30572
30763
  // src/components/withdrawals/WithdrawForm.tsx
30573
- import { useState as useState47, useCallback as useCallback16, useMemo as useMemo20, useEffect as useEffect46 } from "react";
30764
+ import { useState as useState47, useCallback as useCallback16, useMemo as useMemo20, useEffect as useEffect45 } from "react";
30574
30765
  import {
30575
30766
  AlertTriangle as AlertTriangle7,
30576
30767
  ArrowUpDown,
@@ -30853,7 +31044,6 @@ import { useMemo as useMemo19 } from "react";
30853
31044
  import { ActionType as ActionType6 } from "@unifold/core";
30854
31045
 
30855
31046
  // src/hooks/use-get-deposit-address.ts
30856
- import { useEffect as useEffect45 } from "react";
30857
31047
  import { useQuery as useQuery26 } from "@tanstack/react-query";
30858
31048
  import { getDepositAddress } from "@unifold/core";
30859
31049
  function useGetDepositAddress(params) {
@@ -30867,7 +31057,6 @@ function useGetDepositAddress(params) {
30867
31057
  actionType,
30868
31058
  enabled = true
30869
31059
  } = params;
30870
- const { setUserId } = useAnalytics();
30871
31060
  const canFire = !!userId && !!recipientAddress && !!destinationChainType && !!destinationChainId && !!destinationTokenAddress;
30872
31061
  const query = useQuery26({
30873
31062
  queryKey: [
@@ -30899,11 +31088,7 @@ function useGetDepositAddress(params) {
30899
31088
  refetchOnWindowFocus: false,
30900
31089
  retry: 1
30901
31090
  });
30902
- useEffect45(() => {
30903
- if (query.data?.user_id) {
30904
- setUserId(query.data.user_id);
30905
- }
30906
- }, [query.data?.user_id, setUserId]);
31091
+ useStampInternalUserId({ internalUserId: query.data?.user_id });
30907
31092
  return query;
30908
31093
  }
30909
31094
 
@@ -31043,14 +31228,14 @@ function WithdrawForm({
31043
31228
  const [detailsExpanded, setDetailsExpanded] = useState47(false);
31044
31229
  const [glossaryOpen, setGlossaryOpen] = useState47(false);
31045
31230
  const [isMaxed, setIsMaxed] = useState47(false);
31046
- useEffect46(() => {
31231
+ useEffect45(() => {
31047
31232
  setRecipientAddress(recipientAddressProp || "");
31048
31233
  setAmount("");
31049
31234
  setInputUnit("fiat");
31050
31235
  setSubmitError(null);
31051
31236
  setIsMaxed(false);
31052
31237
  }, [recipientAddressProp]);
31053
- useEffect46(() => {
31238
+ useEffect45(() => {
31054
31239
  setIsMaxed(false);
31055
31240
  }, [balanceData?.balanceBaseUnit]);
31056
31241
  const trimmedAddress = recipientAddress.trim();
@@ -31851,7 +32036,7 @@ function WithdrawExecutionItem({ execution, onClick }) {
31851
32036
  }
31852
32037
 
31853
32038
  // src/components/withdrawals/WithdrawConfirmingView.tsx
31854
- import { useState as useState48, useEffect as useEffect47 } from "react";
32039
+ import { useState as useState48, useEffect as useEffect46 } from "react";
31855
32040
  import { Fragment as Fragment19, jsx as jsx76, jsxs as jsxs66 } from "react/jsx-runtime";
31856
32041
  function truncateAddress4(addr) {
31857
32042
  if (addr.length <= 12) return addr;
@@ -31869,7 +32054,7 @@ function WithdrawConfirmingView({
31869
32054
  const t = strings.withdrawModal.details;
31870
32055
  const [showButton, setShowButton] = useState48(false);
31871
32056
  const latestExecution = executions.length > 0 ? executions[executions.length - 1] : null;
31872
- useEffect47(() => {
32057
+ useEffect46(() => {
31873
32058
  if (latestExecution) return;
31874
32059
  const timer = setTimeout(() => setShowButton(true), SHOW_BUTTON_DELAY_MS);
31875
32060
  return () => clearTimeout(timer);
@@ -32031,7 +32216,7 @@ function WithdrawModal({
32031
32216
  );
32032
32217
  const onEventRef = useRef20(emitEvent);
32033
32218
  onEventRef.current = emitEvent;
32034
- useEffect48(
32219
+ useEffect47(
32035
32220
  () => subscribe((event) => {
32036
32221
  if (event.properties?.flow_type !== "withdraw") return;
32037
32222
  const selection = selectionPayload(event);
@@ -32048,12 +32233,16 @@ function WithdrawModal({
32048
32233
  );
32049
32234
  const journeyActiveRef = useRef20(false);
32050
32235
  const journeySessionIdRef = useRef20(void 0);
32236
+ const journeyExternalUserIdRef = useRef20(void 0);
32051
32237
  const onWithdrawSuccessFor = useCallback17(
32052
32238
  (data) => {
32053
32239
  const execution = data.execution?.data.object;
32054
32240
  track("flow_completed", {
32055
32241
  flow_type: "withdraw",
32056
32242
  amount_usd: execution?.destinationAmountUsd ?? execution?.sourceAmountUsd,
32243
+ // The one high-cardinality field we do send: the join key from a
32244
+ // funnel session back to the execution row it produced.
32245
+ execution_id: data.executionId ?? execution?.id,
32057
32246
  // Flat, low-cardinality execution dimensions for PostHog breakdowns.
32058
32247
  status: execution?.status,
32059
32248
  source_chain_type: execution?.sourceChainType,
@@ -32074,6 +32263,7 @@ function WithdrawModal({
32074
32263
  track("flow_failed", {
32075
32264
  flow_type: "withdraw",
32076
32265
  error_code: error.code,
32266
+ execution_id: execution?.id,
32077
32267
  failure_reason: execution?.failureReason ?? void 0,
32078
32268
  status: execution?.status,
32079
32269
  source_chain_type: execution?.sourceChainType,
@@ -32095,7 +32285,7 @@ function WithdrawModal({
32095
32285
  const [resolvedTheme, setResolvedTheme] = useState49(
32096
32286
  theme === "auto" ? "dark" : theme
32097
32287
  );
32098
- useEffect48(() => {
32288
+ useEffect47(() => {
32099
32289
  if (theme === "auto") {
32100
32290
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
32101
32291
  setResolvedTheme(mq.matches ? "dark" : "light");
@@ -32147,6 +32337,7 @@ function WithdrawModal({
32147
32337
  ) ?? null : null;
32148
32338
  const [view, setView] = useState49("form");
32149
32339
  const [withdrawDepositWalletId, setWithdrawDepositWalletId] = useState49();
32340
+ const withdrawDepositWalletIdRef = useRef20(void 0);
32150
32341
  const [selectedExecution, setSelectedExecution] = useState49(null);
32151
32342
  const [submittedTxInfo, setSubmittedTxInfo] = useState49(null);
32152
32343
  const { executions: realtimeExecutions } = useWithdrawPolling({
@@ -32184,6 +32375,7 @@ function WithdrawModal({
32184
32375
  if (!depositWallet) {
32185
32376
  throw new Error(`No deposit wallet available for ${sourceChainType}`);
32186
32377
  }
32378
+ withdrawDepositWalletIdRef.current = depositWallet.id;
32187
32379
  setWithdrawDepositWalletId(depositWallet.id);
32188
32380
  return depositWallet;
32189
32381
  },
@@ -32203,7 +32395,12 @@ function WithdrawModal({
32203
32395
  currency: tokenData?.currency,
32204
32396
  amount: txInfo.amount,
32205
32397
  amount_base_unit: txInfo.amountBaseUnit,
32206
- ...txInfo.amountUsd ? { amount_usd: txInfo.amountUsd } : {}
32398
+ ...txInfo.amountUsd ? { amount_usd: txInfo.amountUsd } : {},
32399
+ // The execution doesn't exist yet — it's created when the user's send
32400
+ // lands on the withdraw deposit wallet, and carries this id. So a
32401
+ // withdrawal confirmed but never completed still joins to whatever
32402
+ // did (or didn't) follow on that wallet.
32403
+ deposit_wallet_id: withdrawDepositWalletIdRef.current
32207
32404
  });
32208
32405
  emitEvent({
32209
32406
  id: generatePrefixedKSUID12("sevt"),
@@ -32237,8 +32434,10 @@ function WithdrawModal({
32237
32434
  const beginJourney = useCallback17(() => {
32238
32435
  journeySessionIdRef.current = startSession();
32239
32436
  journeyActiveRef.current = true;
32437
+ journeyExternalUserIdRef.current = externalUserId;
32438
+ setUserId(null);
32240
32439
  track("widget_opened", { flow_type: "withdraw" });
32241
- }, [startSession, track]);
32440
+ }, [startSession, track, setUserId, externalUserId]);
32242
32441
  const handleClose = useCallback17(() => {
32243
32442
  finalizeJourney();
32244
32443
  onOpenChange(false);
@@ -32247,6 +32446,7 @@ function WithdrawModal({
32247
32446
  setView("form");
32248
32447
  setSelectedExecution(null);
32249
32448
  setSubmittedTxInfo(null);
32449
+ withdrawDepositWalletIdRef.current = void 0;
32250
32450
  setWithdrawDepositWalletId(void 0);
32251
32451
  resetViewTimeoutRef.current = null;
32252
32452
  }, 200);
@@ -32264,9 +32464,15 @@ function WithdrawModal({
32264
32464
  setView("form");
32265
32465
  setSelectedExecution(null);
32266
32466
  setSubmittedTxInfo(null);
32467
+ withdrawDepositWalletIdRef.current = void 0;
32267
32468
  setWithdrawDepositWalletId(void 0);
32268
32469
  }, [open]);
32269
- useEffect48(
32470
+ useLayoutEffect4(() => {
32471
+ if (!open || journeyExternalUserIdRef.current === externalUserId) return;
32472
+ journeyExternalUserIdRef.current = externalUserId;
32473
+ setUserId(null);
32474
+ }, [open, externalUserId, setUserId]);
32475
+ useEffect47(
32270
32476
  () => () => {
32271
32477
  if (journeyActiveRef.current) {
32272
32478
  journeyActiveRef.current = false;
@@ -32275,12 +32481,12 @@ function WithdrawModal({
32275
32481
  },
32276
32482
  [endSession, getSessionId]
32277
32483
  );
32278
- useEffect48(() => {
32484
+ useEffect47(() => {
32279
32485
  if (!open) return;
32280
32486
  const screen = view === "detail" ? "tracker_detail" : view;
32281
32487
  track("screen_viewed", { flow_type: "withdraw", screen });
32282
32488
  }, [open, view, track]);
32283
- useEffect48(
32489
+ useEffect47(
32284
32490
  () => () => {
32285
32491
  if (resetViewTimeoutRef.current) clearTimeout(resetViewTimeoutRef.current);
32286
32492
  },