@unifold/ui-react 0.1.77 → 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
 
@@ -5475,7 +5505,11 @@ function BuyWithCard({
5475
5505
  method: "card",
5476
5506
  flow_type: "deposit",
5477
5507
  provider: selectedProvider.service_provider,
5478
- 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
5479
5513
  });
5480
5514
  onEvent?.({
5481
5515
  id: generatePrefixedKSUID2("sevt"),
@@ -6987,6 +7021,8 @@ var BuyWithWalletPay = React8.forwardRef(
6987
7021
  const [limitUpgradeError, setLimitUpgradeError] = useState15(null);
6988
7022
  const [upgradeSubmitting, setUpgradeSubmitting] = useState15(false);
6989
7023
  const upgradeFlowAbortRef = useRef6(null);
7024
+ const applyLimitRoutingRef = useRef6(() => {
7025
+ });
6990
7026
  const [amount, setAmount] = useState15(amountUsd ?? "");
6991
7027
  const [preparedOrder, setPreparedOrder] = useState15(null);
6992
7028
  const sessionAbortRef = useRef6(null);
@@ -7121,6 +7157,14 @@ var BuyWithWalletPay = React8.forwardRef(
7121
7157
  },
7122
7158
  [email, normalizedPhone, userId]
7123
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
+ });
7124
7168
  const createSessionAndSendOtp = useCallback4(async () => {
7125
7169
  setView("submitting_session");
7126
7170
  setErrorMessage("");
@@ -7130,7 +7174,12 @@ var BuyWithWalletPay = React8.forwardRef(
7130
7174
  email,
7131
7175
  phone: normalizedPhone,
7132
7176
  terms_accepted: true,
7133
- 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.
7134
7183
  },
7135
7184
  publishableKey
7136
7185
  );
@@ -7140,6 +7189,34 @@ var BuyWithWalletPay = React8.forwardRef(
7140
7189
  }
7141
7190
  setClientSecret(session.client_secret);
7142
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
+ }
7143
7220
  await sendOnrampVerificationOtp(session.id, "phone", session.client_secret, publishableKey);
7144
7221
  setResendCooldown(30);
7145
7222
  setView("phone_otp");
@@ -7148,19 +7225,19 @@ var BuyWithWalletPay = React8.forwardRef(
7148
7225
  setErrorMessage(prettifyVerificationError(raw));
7149
7226
  setView("phone_input");
7150
7227
  }
7151
- }, [email, normalizedPhone, publishableKey, storeWalletPaySession]);
7228
+ }, [
7229
+ email,
7230
+ normalizedPhone,
7231
+ publishableKey,
7232
+ storeWalletPaySession,
7233
+ userId,
7234
+ amountUsd,
7235
+ refetchWalletPayLimits
7236
+ ]);
7152
7237
  const clearUpgradeFields = useCallback4(() => {
7153
7238
  setSsnLast4("");
7154
7239
  setDobInput("");
7155
7240
  }, []);
7156
- const { action: initialAction, refetchWalletPayLimits } = useWalletPayInitialScreen({
7157
- userId,
7158
- publishableKey,
7159
- normalizedPhone,
7160
- userEmail,
7161
- sessionPhoneVerified: verificationSession?.phone?.status === "verified",
7162
- isWaiting: defaultTokenLoading || legalAgreementsLoading || userIpInfoLoading
7163
- });
7164
7241
  const pollLimitUpgradeStatus = useCallback4(
7165
7242
  async (signal) => {
7166
7243
  const POLL_INTERVAL_MS5 = 1500;
@@ -7271,6 +7348,7 @@ var BuyWithWalletPay = React8.forwardRef(
7271
7348
  },
7272
7349
  [startUpgradePolling]
7273
7350
  );
7351
+ applyLimitRoutingRef.current = applyLimitRouting;
7274
7352
  const hasInitializedRef = useRef6(false);
7275
7353
  useEffect12(() => {
7276
7354
  if (hasInitializedRef.current) return;
@@ -7500,7 +7578,10 @@ var BuyWithWalletPay = React8.forwardRef(
7500
7578
  track("flow_started", {
7501
7579
  method,
7502
7580
  flow_type: "deposit",
7503
- 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
7504
7585
  });
7505
7586
  onEvent?.({
7506
7587
  id: generatePrefixedKSUID3("sevt"),
@@ -7550,6 +7631,7 @@ var BuyWithWalletPay = React8.forwardRef(
7550
7631
  defaultTokenLoading,
7551
7632
  defaultTokenError,
7552
7633
  depositWallet?.address,
7634
+ depositWalletId,
7553
7635
  userEmail,
7554
7636
  publishableKey,
7555
7637
  onEvent,
@@ -8524,7 +8606,7 @@ var FALLBACK_AGREEMENTS = [
8524
8606
  {
8525
8607
  key: "guest_checkout_terms",
8526
8608
  name: "Guest Checkout Terms of Service",
8527
- url: "https://www.coinbase.com/legal/cb-pay/onramp-guest-checkout-terms"
8609
+ url: "https://www.coinbase.com/legal/guest-checkout/us"
8528
8610
  },
8529
8611
  {
8530
8612
  key: "user_agreement",
@@ -8544,22 +8626,24 @@ function LegalDisclaimer({ legalAgreements, loading }) {
8544
8626
  return /* @__PURE__ */ jsxs14(
8545
8627
  "div",
8546
8628
  {
8547
- 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",
8548
8630
  "aria-hidden": true,
8549
8631
  "aria-busy": "true",
8550
8632
  children: [
8551
- /* @__PURE__ */ jsx18("div", { className: "uf-h-2.5 uf-rounded uf-w-full", style: { backgroundColor: colors2.card } }),
8552
- /* @__PURE__ */ jsx18("div", { className: "uf-h-2.5 uf-rounded uf-w-11/12", style: { backgroundColor: colors2.card } }),
8553
- /* @__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 } })
8554
8636
  ]
8555
8637
  }
8556
8638
  );
8557
8639
  }
8558
8640
  const agreements = legalAgreements?.agreements ?? FALLBACK_AGREEMENTS;
8641
+ const shortLabels = t.agreements;
8642
+ const lastSeparator = t.lastSeparator.replace(/\s+$/, "\xA0");
8559
8643
  return /* @__PURE__ */ jsxs14(
8560
8644
  "p",
8561
8645
  {
8562
- 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",
8563
8647
  style: { color: colors2.foregroundMuted, fontFamily: fonts.regular },
8564
8648
  children: [
8565
8649
  t.prefix,
@@ -8570,12 +8654,12 @@ function LegalDisclaimer({ legalAgreements, loading }) {
8570
8654
  href: a.url,
8571
8655
  target: "_blank",
8572
8656
  rel: "noopener noreferrer",
8573
- className: "uf-underline",
8574
- style: { color: colors2.foreground },
8575
- children: a.name
8657
+ className: "uf-no-underline",
8658
+ style: { color: "inherit", textDecoration: "none", whiteSpace: "nowrap" },
8659
+ children: shortLabels[a.key] ?? a.name
8576
8660
  }
8577
8661
  ),
8578
- idx < arr.length - 2 ? t.separator : idx === arr.length - 2 ? t.lastSeparator : ""
8662
+ idx < arr.length - 2 ? t.separator : idx === arr.length - 2 ? lastSeparator : ""
8579
8663
  ] }, a.key)),
8580
8664
  t.suffix
8581
8665
  ]
@@ -9259,6 +9343,7 @@ function PayWithCashApp({
9259
9343
  destinationChainType,
9260
9344
  destinationChainId,
9261
9345
  destinationTokenAddress,
9346
+ contractCalls,
9262
9347
  cashAppIconUrl,
9263
9348
  view: controlledView,
9264
9349
  onViewChange,
@@ -9351,7 +9436,8 @@ function PayWithCashApp({
9351
9436
  destination_token_address: destinationTokenAddress,
9352
9437
  recipient_address: recipientAddress,
9353
9438
  source_amount: amount,
9354
- external_id: externalId
9439
+ external_id: externalId,
9440
+ ...contractCalls?.length ? { contract_calls: contractCalls } : {}
9355
9441
  },
9356
9442
  publishableKey
9357
9443
  );
@@ -9364,7 +9450,10 @@ function PayWithCashApp({
9364
9450
  track("flow_started", {
9365
9451
  method: "cashapp",
9366
9452
  flow_type: "deposit",
9367
- 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
9368
9457
  });
9369
9458
  onEvent?.({
9370
9459
  id: generatePrefixedKSUID5("sevt"),
@@ -9389,6 +9478,8 @@ function PayWithCashApp({
9389
9478
  destinationChainType,
9390
9479
  destinationChainId,
9391
9480
  destinationTokenAddress,
9481
+ contractCalls,
9482
+ depositWalletId,
9392
9483
  onEvent,
9393
9484
  onDepositError,
9394
9485
  track,
@@ -10009,7 +10100,10 @@ function BankTransfer({
10009
10100
  method: "bank_transfer",
10010
10101
  flow_type: "deposit",
10011
10102
  provider: request.service_provider,
10012
- 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
10013
10107
  });
10014
10108
  onEvent?.({
10015
10109
  id: generatePrefixedKSUID6("sevt"),
@@ -16274,7 +16368,11 @@ function PayWithStripeLink({
16274
16368
  if (checkoutResult.successful) {
16275
16369
  handledTerminalSessionRef.current = null;
16276
16370
  executionReconciledSessionRef.current = null;
16277
- 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
+ });
16278
16376
  setConfirmedSessionId(activeSessionId);
16279
16377
  setSessionStatus(STRIPE_SESSION_STATUS.FULFILLMENT_PROCESSING);
16280
16378
  if (selectedTokenSingleUseRef.current) clearSelectedPaymentToken();
@@ -17771,7 +17869,14 @@ function PayWithStripeLink({
17771
17869
  ]
17772
17870
  }
17773
17871
  ),
17774
- /* @__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
+ )
17775
17880
  ]
17776
17881
  }
17777
17882
  );
@@ -19079,6 +19184,9 @@ function CoinbaseConnect({
19079
19184
  flow_type: "deposit",
19080
19185
  method: "exchange_connect",
19081
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,
19082
19190
  amount_usd: sendAmount || void 0,
19083
19191
  currency: selectedAsset?.currency.toLowerCase().replace(/[^a-z0-9]+/g, "_"),
19084
19192
  chain: sourceChain?.chain_id,
@@ -19086,6 +19194,34 @@ function CoinbaseConnect({
19086
19194
  network: sourceChain?.network
19087
19195
  });
19088
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
+ };
19089
19225
  const handleConfirmTransfer = async () => {
19090
19226
  if (!transferIntent || !accessToken) return;
19091
19227
  setIsLoading(true);
@@ -19120,30 +19256,7 @@ function CoinbaseConnect({
19120
19256
  });
19121
19257
  throw err;
19122
19258
  });
19123
- if (result.status === "mfa_required") {
19124
- track("verification_started", {
19125
- flow_type: "deposit",
19126
- method: "exchange_connect",
19127
- provider: IntegrationProvider.COINBASE,
19128
- verification_type: "mfa",
19129
- verification_status: result.status
19130
- });
19131
- transitionTo("mfa");
19132
- setMfaCode("");
19133
- setMfaError(false);
19134
- } else if (result.status === "completed") {
19135
- trackTransferStarted();
19136
- setConfirmResult(result);
19137
- transitionTo("success");
19138
- onTransferSuccess?.(result);
19139
- } else {
19140
- setErrorMessage(result.message || t.errors.transferFailed);
19141
- transitionTo("error");
19142
- onTransferError?.({
19143
- message: result.message || t.errors.transferFailed,
19144
- code: result.status
19145
- });
19146
- }
19259
+ applyConfirmResult(result);
19147
19260
  } catch (err) {
19148
19261
  const message = err instanceof Error ? err.message : t.errors.transferFailed;
19149
19262
  setErrorMessage(message);
@@ -19300,6 +19413,9 @@ function CoinbaseConnect({
19300
19413
  setShowTransferDetails(false);
19301
19414
  transitionTo("enter_amount");
19302
19415
  break;
19416
+ case "mfa_enrollment":
19417
+ transitionTo("transfer_confirm");
19418
+ break;
19303
19419
  case "mfa":
19304
19420
  transitionTo("transfer_confirm");
19305
19421
  break;
@@ -20856,6 +20972,55 @@ function CoinbaseConnect({
20856
20972
  ] })
20857
20973
  ] });
20858
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
+ }
20859
21024
  if (view === "mfa") {
20860
21025
  return /* @__PURE__ */ jsxs46(Fragment9, { children: [
20861
21026
  /* @__PURE__ */ jsx51(DepositHeader, { title: t.title, showBack: true, onBack: handleBack, onClose }),
@@ -27015,7 +27180,11 @@ function WalletConnect({
27015
27180
  chain: String(token.chain_id),
27016
27181
  network: token.network,
27017
27182
  token: token.symbol ?? token.token_address,
27018
- 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
27019
27188
  });
27020
27189
  setReceivedUsdAtSubmission(checkoutReceivedUsd ?? "0");
27021
27190
  setHasSignedTransaction(true);
@@ -27605,7 +27774,7 @@ function DepositModal({
27605
27774
  const applePaySubTitle = applePaySubTitleProp ?? t.applePayMenu.subtitle;
27606
27775
  const googlePayTitle = googlePayTitleProp ?? t.googlePayMenu.title;
27607
27776
  const googlePaySubTitle = googlePaySubTitleProp ?? t.googlePayMenu.subtitle;
27608
- const { track, startSession, endSession, getSessionId, subscribe } = useAnalytics();
27777
+ const { track, startSession, endSession, getSessionId, setUserId, subscribe } = useAnalytics();
27609
27778
  const normalizedPrefilledAmountUsd = useMemo17(
27610
27779
  () => normalizePrefilledUsdAmount(prefilledAmountUsd),
27611
27780
  [prefilledAmountUsd]
@@ -27613,6 +27782,7 @@ function DepositModal({
27613
27782
  const analyticsMethod = (method) => method === "transfer" ? "transfer_crypto" : method;
27614
27783
  const journeyActiveRef = useRef16(false);
27615
27784
  const journeySessionIdRef = useRef16(void 0);
27785
+ const journeyExternalUserIdRef = useRef16(void 0);
27616
27786
  const externalUserId = userId || void 0;
27617
27787
  const emitEvent = useCallback14(
27618
27788
  (event) => onEvent?.({ ...event, externalUserId }),
@@ -27640,6 +27810,9 @@ function DepositModal({
27640
27810
  method: analyticsMethod(method),
27641
27811
  flow_type: "deposit",
27642
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,
27643
27816
  // Flat, low-cardinality execution dimensions for PostHog breakdowns
27644
27817
  // (precise/opaque fields like base units, addresses, tx hashes live in
27645
27818
  // the onEvent DirectExecution instead).
@@ -27667,6 +27840,7 @@ function DepositModal({
27667
27840
  method: analyticsMethod(method),
27668
27841
  flow_type: "deposit",
27669
27842
  error_code: error.code,
27843
+ execution_id: execution?.id,
27670
27844
  failure_reason: execution?.failureReason ?? void 0,
27671
27845
  status: execution?.status,
27672
27846
  source_chain_type: execution?.sourceChainType,
@@ -28095,8 +28269,10 @@ function DepositModal({
28095
28269
  const beginJourney = useCallback14(() => {
28096
28270
  journeySessionIdRef.current = startSession();
28097
28271
  journeyActiveRef.current = true;
28272
+ journeyExternalUserIdRef.current = userId;
28273
+ setUserId(null);
28098
28274
  track("widget_opened", { flow_type: "deposit" });
28099
- }, [startSession, track]);
28275
+ }, [startSession, track, setUserId, userId]);
28100
28276
  const handleClose = () => {
28101
28277
  finalizeJourney();
28102
28278
  onOpenChange(false);
@@ -28121,6 +28297,11 @@ function DepositModal({
28121
28297
  }
28122
28298
  beginJourney();
28123
28299
  }, [open]);
28300
+ useLayoutEffect2(() => {
28301
+ if (!open || journeyExternalUserIdRef.current === userId) return;
28302
+ journeyExternalUserIdRef.current = userId;
28303
+ setUserId(null);
28304
+ }, [open, userId, setUserId]);
28124
28305
  useLayoutEffect2(() => {
28125
28306
  if (!open) return;
28126
28307
  if (resetViewTimeoutRef.current) {
@@ -29032,6 +29213,7 @@ function DepositModal({
29032
29213
  destinationChainType,
29033
29214
  destinationChainId,
29034
29215
  destinationTokenAddress,
29216
+ contractCalls,
29035
29217
  cashAppIconUrl: projectConfig?.asset_cdn_url ? `${projectConfig.asset_cdn_url}/api/public/icons/onramps/svg/cashapp.svg` : void 0,
29036
29218
  view: cashAppView,
29037
29219
  onViewChange: setCashAppView,
@@ -29395,6 +29577,9 @@ function CheckoutModal({
29395
29577
  flowCompletedRef.current = true;
29396
29578
  track("flow_completed", {
29397
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,
29398
29583
  ...method ? { method: method === "transfer" ? "transfer_crypto" : method } : {}
29399
29584
  });
29400
29585
  }
@@ -29615,7 +29800,7 @@ function CheckoutModal({
29615
29800
  journeySessionIdRef.current = startSession();
29616
29801
  journeyActiveRef.current = true;
29617
29802
  flowCompletedRef.current = false;
29618
- setUserId(null);
29803
+ setUserId(paymentIntentUserId ?? null);
29619
29804
  track("widget_opened", { flow_type: "checkout" });
29620
29805
  if (resetViewTimeoutRef.current) {
29621
29806
  clearTimeout(resetViewTimeoutRef.current);
@@ -30056,7 +30241,7 @@ function CheckoutModal({
30056
30241
  }
30057
30242
 
30058
30243
  // src/components/withdrawals/WithdrawModal.tsx
30059
- 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";
30060
30245
  import { AlertTriangle as AlertTriangle8, ChevronRight as ChevronRight22, Clock as Clock6 } from "lucide-react";
30061
30246
 
30062
30247
  // src/hooks/use-supported-destination-tokens.ts
@@ -30224,7 +30409,7 @@ import { useQuery as useQuery24 } from "@tanstack/react-query";
30224
30409
  import { queryExecutions as queryExecutions4, ActionType as ActionType4 } from "@unifold/core";
30225
30410
  function useExecutions(userId, publishableKey, options) {
30226
30411
  const actionType = options?.actionType ?? ActionType4.Deposit;
30227
- return useQuery24({
30412
+ const query = useQuery24({
30228
30413
  queryKey: ["unifold", "executions", actionType, userId, publishableKey],
30229
30414
  queryFn: () => queryExecutions4(userId, publishableKey, actionType),
30230
30415
  enabled: (options?.enabled ?? true) && !!userId,
@@ -30233,6 +30418,8 @@ function useExecutions(userId, publishableKey, options) {
30233
30418
  gcTime: 1e3 * 60 * 5,
30234
30419
  refetchOnWindowFocus: false
30235
30420
  });
30421
+ useStampInternalUserId({ internalUserId: query.data?.data[0]?.user_id });
30422
+ return query;
30236
30423
  }
30237
30424
 
30238
30425
  // src/hooks/use-withdraw-polling.ts
@@ -30574,7 +30761,7 @@ function WithdrawDoubleInput({
30574
30761
  }
30575
30762
 
30576
30763
  // src/components/withdrawals/WithdrawForm.tsx
30577
- 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";
30578
30765
  import {
30579
30766
  AlertTriangle as AlertTriangle7,
30580
30767
  ArrowUpDown,
@@ -30857,7 +31044,6 @@ import { useMemo as useMemo19 } from "react";
30857
31044
  import { ActionType as ActionType6 } from "@unifold/core";
30858
31045
 
30859
31046
  // src/hooks/use-get-deposit-address.ts
30860
- import { useEffect as useEffect45 } from "react";
30861
31047
  import { useQuery as useQuery26 } from "@tanstack/react-query";
30862
31048
  import { getDepositAddress } from "@unifold/core";
30863
31049
  function useGetDepositAddress(params) {
@@ -30871,7 +31057,6 @@ function useGetDepositAddress(params) {
30871
31057
  actionType,
30872
31058
  enabled = true
30873
31059
  } = params;
30874
- const { setUserId } = useAnalytics();
30875
31060
  const canFire = !!userId && !!recipientAddress && !!destinationChainType && !!destinationChainId && !!destinationTokenAddress;
30876
31061
  const query = useQuery26({
30877
31062
  queryKey: [
@@ -30903,11 +31088,7 @@ function useGetDepositAddress(params) {
30903
31088
  refetchOnWindowFocus: false,
30904
31089
  retry: 1
30905
31090
  });
30906
- useEffect45(() => {
30907
- if (query.data?.user_id) {
30908
- setUserId(query.data.user_id);
30909
- }
30910
- }, [query.data?.user_id, setUserId]);
31091
+ useStampInternalUserId({ internalUserId: query.data?.user_id });
30911
31092
  return query;
30912
31093
  }
30913
31094
 
@@ -31047,14 +31228,14 @@ function WithdrawForm({
31047
31228
  const [detailsExpanded, setDetailsExpanded] = useState47(false);
31048
31229
  const [glossaryOpen, setGlossaryOpen] = useState47(false);
31049
31230
  const [isMaxed, setIsMaxed] = useState47(false);
31050
- useEffect46(() => {
31231
+ useEffect45(() => {
31051
31232
  setRecipientAddress(recipientAddressProp || "");
31052
31233
  setAmount("");
31053
31234
  setInputUnit("fiat");
31054
31235
  setSubmitError(null);
31055
31236
  setIsMaxed(false);
31056
31237
  }, [recipientAddressProp]);
31057
- useEffect46(() => {
31238
+ useEffect45(() => {
31058
31239
  setIsMaxed(false);
31059
31240
  }, [balanceData?.balanceBaseUnit]);
31060
31241
  const trimmedAddress = recipientAddress.trim();
@@ -31855,7 +32036,7 @@ function WithdrawExecutionItem({ execution, onClick }) {
31855
32036
  }
31856
32037
 
31857
32038
  // src/components/withdrawals/WithdrawConfirmingView.tsx
31858
- import { useState as useState48, useEffect as useEffect47 } from "react";
32039
+ import { useState as useState48, useEffect as useEffect46 } from "react";
31859
32040
  import { Fragment as Fragment19, jsx as jsx76, jsxs as jsxs66 } from "react/jsx-runtime";
31860
32041
  function truncateAddress4(addr) {
31861
32042
  if (addr.length <= 12) return addr;
@@ -31873,7 +32054,7 @@ function WithdrawConfirmingView({
31873
32054
  const t = strings.withdrawModal.details;
31874
32055
  const [showButton, setShowButton] = useState48(false);
31875
32056
  const latestExecution = executions.length > 0 ? executions[executions.length - 1] : null;
31876
- useEffect47(() => {
32057
+ useEffect46(() => {
31877
32058
  if (latestExecution) return;
31878
32059
  const timer = setTimeout(() => setShowButton(true), SHOW_BUTTON_DELAY_MS);
31879
32060
  return () => clearTimeout(timer);
@@ -32035,7 +32216,7 @@ function WithdrawModal({
32035
32216
  );
32036
32217
  const onEventRef = useRef20(emitEvent);
32037
32218
  onEventRef.current = emitEvent;
32038
- useEffect48(
32219
+ useEffect47(
32039
32220
  () => subscribe((event) => {
32040
32221
  if (event.properties?.flow_type !== "withdraw") return;
32041
32222
  const selection = selectionPayload(event);
@@ -32052,12 +32233,16 @@ function WithdrawModal({
32052
32233
  );
32053
32234
  const journeyActiveRef = useRef20(false);
32054
32235
  const journeySessionIdRef = useRef20(void 0);
32236
+ const journeyExternalUserIdRef = useRef20(void 0);
32055
32237
  const onWithdrawSuccessFor = useCallback17(
32056
32238
  (data) => {
32057
32239
  const execution = data.execution?.data.object;
32058
32240
  track("flow_completed", {
32059
32241
  flow_type: "withdraw",
32060
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,
32061
32246
  // Flat, low-cardinality execution dimensions for PostHog breakdowns.
32062
32247
  status: execution?.status,
32063
32248
  source_chain_type: execution?.sourceChainType,
@@ -32078,6 +32263,7 @@ function WithdrawModal({
32078
32263
  track("flow_failed", {
32079
32264
  flow_type: "withdraw",
32080
32265
  error_code: error.code,
32266
+ execution_id: execution?.id,
32081
32267
  failure_reason: execution?.failureReason ?? void 0,
32082
32268
  status: execution?.status,
32083
32269
  source_chain_type: execution?.sourceChainType,
@@ -32099,7 +32285,7 @@ function WithdrawModal({
32099
32285
  const [resolvedTheme, setResolvedTheme] = useState49(
32100
32286
  theme === "auto" ? "dark" : theme
32101
32287
  );
32102
- useEffect48(() => {
32288
+ useEffect47(() => {
32103
32289
  if (theme === "auto") {
32104
32290
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
32105
32291
  setResolvedTheme(mq.matches ? "dark" : "light");
@@ -32151,6 +32337,7 @@ function WithdrawModal({
32151
32337
  ) ?? null : null;
32152
32338
  const [view, setView] = useState49("form");
32153
32339
  const [withdrawDepositWalletId, setWithdrawDepositWalletId] = useState49();
32340
+ const withdrawDepositWalletIdRef = useRef20(void 0);
32154
32341
  const [selectedExecution, setSelectedExecution] = useState49(null);
32155
32342
  const [submittedTxInfo, setSubmittedTxInfo] = useState49(null);
32156
32343
  const { executions: realtimeExecutions } = useWithdrawPolling({
@@ -32188,6 +32375,7 @@ function WithdrawModal({
32188
32375
  if (!depositWallet) {
32189
32376
  throw new Error(`No deposit wallet available for ${sourceChainType}`);
32190
32377
  }
32378
+ withdrawDepositWalletIdRef.current = depositWallet.id;
32191
32379
  setWithdrawDepositWalletId(depositWallet.id);
32192
32380
  return depositWallet;
32193
32381
  },
@@ -32207,7 +32395,12 @@ function WithdrawModal({
32207
32395
  currency: tokenData?.currency,
32208
32396
  amount: txInfo.amount,
32209
32397
  amount_base_unit: txInfo.amountBaseUnit,
32210
- ...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
32211
32404
  });
32212
32405
  emitEvent({
32213
32406
  id: generatePrefixedKSUID12("sevt"),
@@ -32241,8 +32434,10 @@ function WithdrawModal({
32241
32434
  const beginJourney = useCallback17(() => {
32242
32435
  journeySessionIdRef.current = startSession();
32243
32436
  journeyActiveRef.current = true;
32437
+ journeyExternalUserIdRef.current = externalUserId;
32438
+ setUserId(null);
32244
32439
  track("widget_opened", { flow_type: "withdraw" });
32245
- }, [startSession, track]);
32440
+ }, [startSession, track, setUserId, externalUserId]);
32246
32441
  const handleClose = useCallback17(() => {
32247
32442
  finalizeJourney();
32248
32443
  onOpenChange(false);
@@ -32251,6 +32446,7 @@ function WithdrawModal({
32251
32446
  setView("form");
32252
32447
  setSelectedExecution(null);
32253
32448
  setSubmittedTxInfo(null);
32449
+ withdrawDepositWalletIdRef.current = void 0;
32254
32450
  setWithdrawDepositWalletId(void 0);
32255
32451
  resetViewTimeoutRef.current = null;
32256
32452
  }, 200);
@@ -32268,9 +32464,15 @@ function WithdrawModal({
32268
32464
  setView("form");
32269
32465
  setSelectedExecution(null);
32270
32466
  setSubmittedTxInfo(null);
32467
+ withdrawDepositWalletIdRef.current = void 0;
32271
32468
  setWithdrawDepositWalletId(void 0);
32272
32469
  }, [open]);
32273
- useEffect48(
32470
+ useLayoutEffect4(() => {
32471
+ if (!open || journeyExternalUserIdRef.current === externalUserId) return;
32472
+ journeyExternalUserIdRef.current = externalUserId;
32473
+ setUserId(null);
32474
+ }, [open, externalUserId, setUserId]);
32475
+ useEffect47(
32274
32476
  () => () => {
32275
32477
  if (journeyActiveRef.current) {
32276
32478
  journeyActiveRef.current = false;
@@ -32279,12 +32481,12 @@ function WithdrawModal({
32279
32481
  },
32280
32482
  [endSession, getSessionId]
32281
32483
  );
32282
- useEffect48(() => {
32484
+ useEffect47(() => {
32283
32485
  if (!open) return;
32284
32486
  const screen = view === "detail" ? "tracker_detail" : view;
32285
32487
  track("screen_viewed", { flow_type: "withdraw", screen });
32286
32488
  }, [open, view, track]);
32287
- useEffect48(
32489
+ useEffect47(
32288
32490
  () => () => {
32289
32491
  if (resetViewTimeoutRef.current) clearTimeout(resetViewTimeoutRef.current);
32290
32492
  },