@rhinestone/deposit-modal 0.13.0 → 0.14.0

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 (57) hide show
  1. package/README.md +43 -2
  2. package/bridge-transcript.json +653 -0
  3. package/dist/ClaimModal-IKTJU43Y.cjs +13 -0
  4. package/dist/ClaimModal-SEKNWRMA.mjs +13 -0
  5. package/dist/{DepositModalReown-YNNHLAV2.cjs → DepositModalReown-46LPVFB6.cjs} +16 -15
  6. package/dist/{DepositModalReown-3H75G3XQ.mjs → DepositModalReown-Q2JMLT47.mjs} +6 -5
  7. package/dist/WithdrawModal-5ICGD4HJ.mjs +12 -0
  8. package/dist/WithdrawModal-VP6PMIRO.cjs +12 -0
  9. package/dist/{chunk-GACIGCSN.cjs → chunk-6KG642KH.cjs} +1 -1
  10. package/dist/{chunk-RAADLTSW.cjs → chunk-7ICSHJFE.cjs} +179 -138
  11. package/dist/{chunk-COCBCQQC.mjs → chunk-7SR7ENVJ.mjs} +46 -5
  12. package/dist/{chunk-OLCD75JK.mjs → chunk-CFKC77SV.mjs} +451 -131
  13. package/dist/{chunk-EOMT5LKX.mjs → chunk-IN77XSWO.mjs} +526 -3
  14. package/dist/{chunk-FZ5FZFIG.mjs → chunk-K4H6TGBE.mjs} +1 -1
  15. package/dist/{chunk-DSIZNRWA.mjs → chunk-KTUVSKR4.mjs} +979 -360
  16. package/dist/{chunk-HOGZKHHY.cjs → chunk-LZFRL2AL.cjs} +3 -3
  17. package/dist/{chunk-7N77GIP4.cjs → chunk-MTFJSE52.cjs} +465 -171
  18. package/dist/{chunk-LTMMETAB.mjs → chunk-PKML3XVS.mjs} +404 -110
  19. package/dist/{chunk-ZMT4ATHR.cjs → chunk-V2OBGLJD.cjs} +564 -41
  20. package/dist/{chunk-XYJFEN5D.mjs → chunk-WUJIKCFU.mjs} +1 -1
  21. package/dist/{chunk-CVGNCUVU.cjs → chunk-X2IPAPXU.cjs} +22 -13
  22. package/dist/{chunk-2G2EIN4A.cjs → chunk-YCQZJQO6.cjs} +1272 -653
  23. package/dist/{chunk-5NUIKYSF.cjs → chunk-YWMWI7PZ.cjs} +458 -138
  24. package/dist/{chunk-YREVFB64.mjs → chunk-ZEO6ADM3.mjs} +16 -7
  25. package/dist/claim.cjs +6 -6
  26. package/dist/claim.d.cts +3 -3
  27. package/dist/claim.d.ts +3 -3
  28. package/dist/claim.mjs +5 -5
  29. package/dist/constants.cjs +2 -2
  30. package/dist/constants.d.cts +1 -1
  31. package/dist/constants.d.ts +1 -1
  32. package/dist/constants.mjs +1 -1
  33. package/dist/deposit.cjs +6 -6
  34. package/dist/deposit.d.cts +2 -2
  35. package/dist/deposit.d.ts +2 -2
  36. package/dist/deposit.mjs +5 -5
  37. package/dist/embed.cjs +310 -102
  38. package/dist/embed.d.cts +7 -1
  39. package/dist/embed.d.ts +7 -1
  40. package/dist/embed.mjs +285 -77
  41. package/dist/index.cjs +9 -9
  42. package/dist/index.d.cts +1 -1
  43. package/dist/index.d.ts +1 -1
  44. package/dist/index.mjs +8 -8
  45. package/dist/types-Bho4OA03.d.ts +982 -0
  46. package/dist/types-DP_3KwlD.d.cts +982 -0
  47. package/dist/withdraw.cjs +5 -5
  48. package/dist/withdraw.d.cts +3 -3
  49. package/dist/withdraw.d.ts +3 -3
  50. package/dist/withdraw.mjs +4 -4
  51. package/package.json +28 -7
  52. package/dist/ClaimModal-CREZO27I.mjs +0 -11
  53. package/dist/ClaimModal-JRIA32U2.cjs +0 -11
  54. package/dist/WithdrawModal-NVRX44IZ.mjs +0 -10
  55. package/dist/WithdrawModal-YYSBPCJJ.cjs +0 -10
  56. package/dist/types-B89I8_8H.d.ts +0 -638
  57. package/dist/types-nnDR4tqA.d.cts +0 -638
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  ProcessingStep,
3
+ analyticsTargetProperties,
3
4
  deriveAccountSalt,
4
5
  isSubmissionUncertain,
5
6
  useTokenPrices,
6
7
  warnRemovedProps
7
- } from "./chunk-COCBCQQC.mjs";
8
+ } from "./chunk-7SR7ENVJ.mjs";
8
9
  import {
10
+ AnalyticsSession,
9
11
  BodyHeader,
10
12
  Button,
11
13
  Callout,
@@ -37,8 +39,9 @@ import {
37
39
  useReadClient,
38
40
  useRpcUrls,
39
41
  useStableRpcUrls,
40
- useTargetTokens
41
- } from "./chunk-EOMT5LKX.mjs";
42
+ useTargetTokens,
43
+ useWithdrawAnalyticsEmitter
44
+ } from "./chunk-IN77XSWO.mjs";
42
45
  import {
43
46
  NATIVE_TOKEN_ADDRESS,
44
47
  getChainIcon,
@@ -50,10 +53,10 @@ import {
50
53
  } from "./chunk-VUATPOWJ.mjs";
51
54
 
52
55
  // src/WithdrawModal.tsx
53
- import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
56
+ import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo4, useRef as useRef3, useState as useState4 } from "react";
54
57
 
55
58
  // src/WithdrawFlow.tsx
56
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
59
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
57
60
 
58
61
  // src/store/withdraw.ts
59
62
  import { createContext, createElement, useContext } from "react";
@@ -219,7 +222,7 @@ function WithdrawFormStep({
219
222
  registrationError = null,
220
223
  onRecipientChange,
221
224
  onSubmit,
222
- onBalanceUsdChange
225
+ onFriction
223
226
  }) {
224
227
  const [recipient, setRecipient] = useState(defaultRecipient ?? "");
225
228
  const [amount, setAmount] = useState(defaultAmount ?? "");
@@ -232,6 +235,7 @@ function WithdrawFormStep({
232
235
  const chainDropdownRef = useRef(null);
233
236
  const tokenDropdownRef = useRef(null);
234
237
  const publicClientChainId = publicClient.chain?.id;
238
+ const onFrictionRef = useLatestRef(onFriction);
235
239
  useClickOutside(chainDropdownRef, () => setShowChainDropdown(false));
236
240
  useClickOutside(tokenDropdownRef, () => setShowTokenDropdown(false));
237
241
  const targetSymbol = getTokenSymbol(targetToken, targetChain);
@@ -272,6 +276,7 @@ function WithdrawFormStep({
272
276
  } catch {
273
277
  if (active) {
274
278
  setBalance(null);
279
+ onFrictionRef.current?.("wallet_balance_unavailable");
275
280
  timeoutId = setTimeout(fetchBalance, retryDelayMs);
276
281
  retryDelayMs = Math.min(Math.round(retryDelayMs * 1.5), 3e4);
277
282
  }
@@ -286,7 +291,7 @@ function WithdrawFormStep({
286
291
  clearTimeout(timeoutId);
287
292
  }
288
293
  };
289
- }, [accountAddress, publicClient, publicClientChainId, asset]);
294
+ }, [accountAddress, publicClient, publicClientChainId, asset, onFrictionRef]);
290
295
  const formattedBalance = useMemo(() => {
291
296
  if (balance === null) return "...";
292
297
  try {
@@ -301,18 +306,9 @@ function WithdrawFormStep({
301
306
  }
302
307
  }, [balance, asset.decimals]);
303
308
  useEffect(() => {
304
- if (balance === null) return;
305
- const sym = asset.symbol.toUpperCase();
306
- if (sym !== "USDC" && sym !== "USDT" && sym !== "USDT0") return;
307
- try {
308
- const raw = formatUnits(balance, asset.decimals);
309
- const numeric = Number(raw);
310
- if (!Number.isFinite(numeric)) return;
311
- onBalanceUsdChange?.(numeric);
312
- } catch {
313
- return;
314
- }
315
- }, [balance, asset.decimals, asset.symbol, onBalanceUsdChange]);
309
+ if (targetTokenOptions.length > 0 || !recipient || !amount) return;
310
+ onFrictionRef.current?.("route_target_tokens_unavailable");
311
+ }, [targetTokenOptions.length, recipient, amount, onFrictionRef]);
316
312
  const amountUsd = useMemo(() => {
317
313
  if (!amount) return null;
318
314
  const parsed = Number(amount);
@@ -332,6 +328,7 @@ function WithdrawFormStep({
332
328
  const handleWithdraw = useCallback(async () => {
333
329
  if (!recipient || !/^0x[a-fA-F0-9]{40}$/.test(recipient)) {
334
330
  setError("Enter a valid recipient address");
331
+ onFriction?.("recipient_invalid");
335
332
  return;
336
333
  }
337
334
  const lowerRecipient = recipient.toLowerCase();
@@ -339,11 +336,13 @@ function WithdrawFormStep({
339
336
  setError(
340
337
  "Recipient can't be your own account. Send to a different wallet."
341
338
  );
339
+ onFriction?.("recipient_not_allowed");
342
340
  return;
343
341
  }
344
342
  const parsed = Number(amount);
345
343
  if (!amount || !Number.isFinite(parsed) || parsed <= 0) {
346
344
  setError("Enter a valid amount");
345
+ onFriction?.("amount_invalid");
347
346
  return;
348
347
  }
349
348
  if (balance !== null) {
@@ -351,10 +350,12 @@ function WithdrawFormStep({
351
350
  const amountInUnits = parseUnits(amount, asset.decimals);
352
351
  if (amountInUnits > balance) {
353
352
  setError("Insufficient balance");
353
+ onFriction?.("amount_insufficient_balance");
354
354
  return;
355
355
  }
356
356
  } catch {
357
357
  setError("Invalid amount");
358
+ onFriction?.("amount_invalid");
358
359
  return;
359
360
  }
360
361
  }
@@ -375,7 +376,8 @@ function WithdrawFormStep({
375
376
  asset.decimals,
376
377
  accountAddress,
377
378
  depositAddress,
378
- onSubmit
379
+ onSubmit,
380
+ onFriction
379
381
  ]);
380
382
  if (isBalanceLoading) {
381
383
  return /* @__PURE__ */ jsxs("div", { className: "rs-screen", children: [
@@ -751,7 +753,171 @@ WithdrawReviewStep.displayName = "WithdrawReviewStep";
751
753
 
752
754
  // src/WithdrawFlow.tsx
753
755
  import { parseUnits as parseUnits2 } from "viem";
754
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
756
+
757
+ // src/core/withdraw-analytics.tsx
758
+ import {
759
+ createContext as createContext2,
760
+ useContext as useContext2,
761
+ useMemo as useMemo2
762
+ } from "react";
763
+ import { jsx as jsx3 } from "react/jsx-runtime";
764
+ function withdrawFailureReasonForError(error) {
765
+ if (isSubmissionUncertain(error)) return "submission_uncertain";
766
+ const message = (error instanceof Error ? error.message : String(error ?? "")).toLowerCase();
767
+ if (message.includes("changed before submission")) {
768
+ return "registration_target_changed";
769
+ }
770
+ if (message.includes("did not return a txhash")) {
771
+ return "submission_missing_hash";
772
+ }
773
+ if (message.includes("reject") || message.includes("denied")) {
774
+ return "submission_rejected";
775
+ }
776
+ return "submission_failed";
777
+ }
778
+ function createWithdrawAnalyticsController(emit) {
779
+ let step = null;
780
+ let afterHandoff = false;
781
+ let sameRoute = null;
782
+ let opened = false;
783
+ let closed = false;
784
+ const dedupe = /* @__PURE__ */ new Set();
785
+ const stamp = (payload) => sameRoute === null ? payload : { ...payload, same_route: sameRoute };
786
+ const send = (payload) => {
787
+ controller.openSession();
788
+ emit(stamp(payload));
789
+ };
790
+ const emitOnce = (key, payload) => {
791
+ if (dedupe.has(key)) return;
792
+ dedupe.add(key);
793
+ send(payload);
794
+ };
795
+ const controller = {
796
+ openSession() {
797
+ if (opened) return;
798
+ opened = true;
799
+ emit({ type: "withdraw_modal_open" });
800
+ },
801
+ openStep(nextStep) {
802
+ controller.openSession();
803
+ if (step === nextStep) return;
804
+ step = nextStep;
805
+ emitOnce(`step-open:${nextStep}`, {
806
+ type: "withdraw_modal_step_open",
807
+ step: nextStep
808
+ });
809
+ },
810
+ completeStep(completedStep = step ?? void 0) {
811
+ if (!completedStep) return;
812
+ emitOnce(`step-complete:${completedStep}`, {
813
+ type: "withdraw_modal_step_complete",
814
+ step: completedStep
815
+ });
816
+ },
817
+ friction(reason, eventStep = step ?? void 0) {
818
+ if (!eventStep) return;
819
+ emitOnce(`friction:${eventStep}:${reason}`, {
820
+ type: "withdraw_modal_friction",
821
+ step: eventStep,
822
+ reason
823
+ });
824
+ },
825
+ failure(reason, retryable, eventStep = step ?? void 0) {
826
+ if (!eventStep) return;
827
+ emitOnce(`failure:${eventStep}:${reason}`, {
828
+ type: "withdraw_modal_failure",
829
+ step: eventStep,
830
+ reason,
831
+ retryable
832
+ });
833
+ },
834
+ retry(reason, eventStep = step ?? void 0) {
835
+ if (!eventStep) return;
836
+ send({ type: "withdraw_modal_retry", step: eventStep, reason });
837
+ for (const key of [...dedupe]) {
838
+ if (key.includes(`:${eventStep}:`)) dedupe.delete(key);
839
+ }
840
+ },
841
+ handoff({ transactionHash, managedAccount, sameRoute: route }, eventStep) {
842
+ const handoffStep = eventStep ?? step ?? "submit";
843
+ if (step !== handoffStep) controller.openStep(handoffStep);
844
+ sameRoute = route;
845
+ afterHandoff = true;
846
+ emitOnce(`handoff:${transactionHash}`, {
847
+ type: "withdraw_modal_handoff",
848
+ step: handoffStep,
849
+ transaction_hash: transactionHash,
850
+ managed_account: managedAccount,
851
+ same_route: route
852
+ });
853
+ },
854
+ outcome(outcome) {
855
+ if (!step) return;
856
+ emitOnce(`outcome:${outcome}`, {
857
+ type: "withdraw_modal_ui_outcome",
858
+ step,
859
+ outcome
860
+ });
861
+ },
862
+ abandon(reason) {
863
+ if (!step || afterHandoff) return;
864
+ emitOnce(`abandon:${step}:${reason}`, {
865
+ type: "withdraw_modal_abandoned",
866
+ step,
867
+ reason
868
+ });
869
+ },
870
+ close(source) {
871
+ if (closed) return;
872
+ closed = true;
873
+ send({
874
+ type: "withdraw_modal_close",
875
+ source,
876
+ step,
877
+ after_handoff: afterHandoff
878
+ });
879
+ },
880
+ /** "New withdrawal" on the terminal screen: a fresh attempt in the same
881
+ * session, so the envelope and its session id stay. */
882
+ resetAttempt() {
883
+ step = null;
884
+ afterHandoff = false;
885
+ sameRoute = null;
886
+ dedupe.clear();
887
+ },
888
+ get step() {
889
+ return step;
890
+ },
891
+ get afterHandoff() {
892
+ return afterHandoff;
893
+ },
894
+ get sameRoute() {
895
+ return sameRoute;
896
+ }
897
+ };
898
+ return controller;
899
+ }
900
+ var WithdrawAnalyticsControllerContext = createContext2(null);
901
+ var noopController = createWithdrawAnalyticsController(() => {
902
+ });
903
+ function WithdrawAnalyticsControllerProvider({
904
+ children,
905
+ controllerRef
906
+ }) {
907
+ const emit = useWithdrawAnalyticsEmitter();
908
+ const controller = useMemo2(
909
+ () => createWithdrawAnalyticsController(emit),
910
+ [emit]
911
+ );
912
+ if (controllerRef) controllerRef.current = controller;
913
+ return /* @__PURE__ */ jsx3(WithdrawAnalyticsControllerContext.Provider, { value: controller, children });
914
+ }
915
+ function useWithdrawAnalyticsController() {
916
+ return useContext2(WithdrawAnalyticsControllerContext) ?? noopController;
917
+ }
918
+
919
+ // src/WithdrawFlow.tsx
920
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
755
921
  var ADDR_REGEX = /^0x[a-fA-F0-9]{40}$/;
756
922
  var REGISTRATION_DEBOUNCE_MS = 600;
757
923
  var WITHDRAW_INDEXING_DELAY_MS = 2e3;
@@ -773,17 +939,19 @@ function WithdrawFlow({
773
939
  onStepChange,
774
940
  onClose,
775
941
  onLifecycle,
776
- onEvent,
777
942
  onError,
778
943
  debug
779
944
  }) {
780
945
  const rpcUrls = useRpcUrls();
781
946
  const onStepChangeRef = useLatestRef(onStepChange);
782
- const onEventRef = useLatestRef(onEvent);
947
+ const analytics = useWithdrawAnalyticsController();
783
948
  const onLifecycleRef = useLatestRef(onLifecycle);
784
949
  const onErrorRef = useLatestRef(onError);
785
950
  const isFlowActiveRef = useRef2(true);
786
951
  const submitGenerationRef = useRef2(0);
952
+ const lastSubmitFailureRef = useRef2(
953
+ null
954
+ );
787
955
  useEffect2(() => {
788
956
  isFlowActiveRef.current = true;
789
957
  return () => {
@@ -807,7 +975,6 @@ function WithdrawFlow({
807
975
  const [recipientInput, setRecipientInput] = useState3(
808
976
  defaultRecipient ?? ""
809
977
  );
810
- const [totalBalanceUsd, setTotalBalanceUsd] = useState3(0);
811
978
  useEffect2(() => {
812
979
  setTargetChain(targetChainProp);
813
980
  }, [targetChainProp]);
@@ -822,7 +989,7 @@ function WithdrawFlow({
822
989
  const flowStep = useWithdrawStore((s) => s.step);
823
990
  const reviewedRecipient = useWithdrawStore((s) => s.recipient);
824
991
  const publicClient = useReadClient(sourceChain, rpcUrls);
825
- const recipientForRegistration = useMemo2(() => {
992
+ const recipientForRegistration = useMemo3(() => {
826
993
  if (!recipientInput) return void 0;
827
994
  return ADDR_REGEX.test(recipientInput) ? recipientInput : void 0;
828
995
  }, [recipientInput]);
@@ -907,7 +1074,7 @@ function WithdrawFlow({
907
1074
  onLifecycleRef,
908
1075
  onErrorRef
909
1076
  ]);
910
- const asset = useMemo2(() => {
1077
+ const asset = useMemo3(() => {
911
1078
  const symbol = getTokenSymbol(sourceToken, sourceChain);
912
1079
  const decimals = getTokenDecimalsByAddress(sourceToken, sourceChain);
913
1080
  return {
@@ -927,33 +1094,35 @@ function WithdrawFlow({
927
1094
  recipient: reviewedRecipient
928
1095
  } : null;
929
1096
  const isReviewedTargetReady = registration.kind === "ready" && reviewedTarget !== null && targetsEqual(registration.registeredTarget, reviewedTarget);
1097
+ useEffect2(() => {
1098
+ if (flowStep !== "review") return;
1099
+ if (registration.kind !== "ready" || isReviewedTargetReady) return;
1100
+ analytics.friction("registration_stale", "review");
1101
+ }, [analytics, flowStep, registration.kind, isReviewedTargetReady]);
930
1102
  const reviewBackHandler = useCallback2(() => {
931
1103
  submitGenerationRef.current += 1;
1104
+ analytics.abandon("back");
932
1105
  storeApi.dispatch({ type: "review/back" });
933
- }, [storeApi]);
1106
+ }, [analytics, storeApi]);
934
1107
  useEffect2(() => {
935
1108
  onStepChangeRef.current?.(
936
1109
  flowStep === "review" ? reviewBackHandler : void 0,
937
1110
  flowStep
938
1111
  );
939
1112
  }, [flowStep, reviewBackHandler, onStepChangeRef]);
940
- const lastStepOpenEventKeyRef = useRef2(null);
941
1113
  useEffect2(() => {
942
- if (flowStep !== "form") {
943
- lastStepOpenEventKeyRef.current = null;
944
- return;
1114
+ analytics.openSession();
1115
+ analytics.openStep(analyticsStepForFlowStep(flowStep));
1116
+ }, [analytics, flowStep]);
1117
+ useEffect2(() => {
1118
+ if (registration.kind === "pending") {
1119
+ analytics.friction("registration_pending");
1120
+ } else if (registration.kind === "stale") {
1121
+ analytics.friction("registration_stale");
1122
+ } else if (registration.kind === "failed") {
1123
+ analytics.failure("registration_failed", true);
945
1124
  }
946
- if (lastStepOpenEventKeyRef.current === "form") return;
947
- lastStepOpenEventKeyRef.current = "form";
948
- const tokenSymbol = getTokenSymbol(targetToken, targetChain);
949
- const chainName = getChainName(targetChain);
950
- onEventRef.current?.({
951
- type: "withdraw_modal_select_amount_open",
952
- pred_balance: totalBalanceUsd,
953
- default_token: tokenSymbol,
954
- default_chain: chainName
955
- });
956
- }, [flowStep, targetToken, targetChain, totalBalanceUsd, onEventRef]);
1125
+ }, [analytics, registration.kind]);
957
1126
  useEffect2(() => {
958
1127
  logFlow("state:changed", {
959
1128
  step: flowStep,
@@ -974,33 +1143,34 @@ function WithdrawFlow({
974
1143
  ]);
975
1144
  const handleError = useCallback2(
976
1145
  (message, code, cause) => {
1146
+ if (code === "PROCESS_TIMEOUT") {
1147
+ analytics.friction("processor_status_poll_failed", "processing");
1148
+ }
977
1149
  logFlowError("flow:error", cause ?? message, { code });
978
1150
  onErrorRef.current?.({ message, code, cause });
979
1151
  },
980
- [logFlowError, onErrorRef]
1152
+ [analytics, logFlowError, onErrorRef]
981
1153
  );
982
1154
  const handleFormContinue = useCallback2(
983
1155
  (recipient, amountValue) => {
984
- const tokenSymbol = getTokenSymbol(targetToken, targetChain);
985
- const chainName = getChainName(targetChain);
986
- onEventRef.current?.({
987
- type: "withdraw_modal_select_amount_cta_click",
988
- pred_balance: totalBalanceUsd,
989
- selected_token: tokenSymbol,
990
- selected_chain: chainName,
991
- amount: amountValue,
992
- cta_name: "withdraw"
993
- });
1156
+ analytics.completeStep("form");
994
1157
  storeApi.dispatch({
995
1158
  type: "form/continue",
996
1159
  amount: amountValue,
997
1160
  recipient
998
1161
  });
999
1162
  },
1000
- [targetToken, targetChain, totalBalanceUsd, onEventRef, storeApi]
1163
+ [analytics, storeApi]
1001
1164
  );
1002
1165
  const handleReviewConfirm = useCallback2(
1003
1166
  async () => {
1167
+ const previousFailure = lastSubmitFailureRef.current;
1168
+ if (previousFailure) {
1169
+ lastSubmitFailureRef.current = null;
1170
+ analytics.retry(previousFailure, "submit");
1171
+ }
1172
+ analytics.completeStep("review");
1173
+ analytics.openStep("submit");
1004
1174
  const state = storeApi.getState();
1005
1175
  const recipient = state.recipient;
1006
1176
  const amountValue = state.amount;
@@ -1018,11 +1188,13 @@ function WithdrawFlow({
1018
1188
  };
1019
1189
  const matchingRegistration = getMatchingRegistration();
1020
1190
  if (!matchingRegistration) {
1191
+ analytics.failure("registration_target_changed", true, "submit");
1021
1192
  throw new Error("Withdrawal target is still being prepared");
1022
1193
  }
1023
1194
  const smartAccount = matchingRegistration.smartAccount;
1024
1195
  const lowerRecipient = recipient.toLowerCase();
1025
1196
  if (lowerRecipient === accountAddress.toLowerCase() || lowerRecipient === smartAccount.toLowerCase()) {
1197
+ analytics.failure("recipient_not_allowed", false, "submit");
1026
1198
  throw new Error(
1027
1199
  "Recipient can't be your own account. Send to a different wallet."
1028
1200
  );
@@ -1060,6 +1232,15 @@ function WithdrawFlow({
1060
1232
  if (!result?.txHash) {
1061
1233
  throw new Error("onSendTransaction did not return a txHash");
1062
1234
  }
1235
+ analytics.handoff(
1236
+ {
1237
+ transactionHash: result.txHash,
1238
+ managedAccount: smartAccount,
1239
+ sameRoute: isSameRoute
1240
+ },
1241
+ "submit"
1242
+ );
1243
+ analytics.completeStep("submit");
1063
1244
  logFlow("withdraw:submitted", {
1064
1245
  txHash: result.txHash,
1065
1246
  smartAccount,
@@ -1086,6 +1267,9 @@ function WithdrawFlow({
1086
1267
  });
1087
1268
  } catch (err) {
1088
1269
  const raw = err instanceof Error ? err.message : "Withdraw failed";
1270
+ const reason = withdrawFailureReasonForError(err);
1271
+ lastSubmitFailureRef.current = reason;
1272
+ analytics.failure(reason, reason !== "submission_uncertain", "submit");
1089
1273
  logFlowError("submit:failed", err, {
1090
1274
  sourceChain,
1091
1275
  sourceToken,
@@ -1097,6 +1281,7 @@ function WithdrawFlow({
1097
1281
  }
1098
1282
  },
1099
1283
  [
1284
+ analytics,
1100
1285
  targetChain,
1101
1286
  targetToken,
1102
1287
  asset.decimals,
@@ -1114,6 +1299,8 @@ function WithdrawFlow({
1114
1299
  );
1115
1300
  const handleWithdrawComplete = useCallback2(
1116
1301
  (txHash, destinationTxHash, context) => {
1302
+ analytics.openStep("processing");
1303
+ analytics.outcome("completed");
1117
1304
  logFlow("withdraw:complete", { txHash, destinationTxHash, ...context });
1118
1305
  onLifecycleRef.current?.({
1119
1306
  type: "complete",
@@ -1131,10 +1318,13 @@ function WithdrawFlow({
1131
1318
  });
1132
1319
  storeApi.dispatch({ type: "withdraw/complete" });
1133
1320
  },
1134
- [logFlow, onLifecycleRef, storeApi]
1321
+ [analytics, logFlow, onLifecycleRef, storeApi]
1135
1322
  );
1136
1323
  const handleWithdrawFailed = useCallback2(
1137
1324
  (txHash, error) => {
1325
+ analytics.openStep("processing");
1326
+ analytics.outcome("failed");
1327
+ analytics.failure("processor_failed", false, "processing");
1138
1328
  logFlowError("withdraw:failed", error, { txHash });
1139
1329
  onLifecycleRef.current?.({
1140
1330
  type: "failed",
@@ -1143,11 +1333,11 @@ function WithdrawFlow({
1143
1333
  });
1144
1334
  storeApi.dispatch({ type: "withdraw/failed" });
1145
1335
  },
1146
- [logFlowError, onLifecycleRef, storeApi]
1336
+ [analytics, logFlowError, onLifecycleRef, storeApi]
1147
1337
  );
1148
1338
  const targetChainOptions = useDestinationChains();
1149
1339
  const targetTokensFor = useTargetTokens();
1150
- const targetTokenOptions = useMemo2(
1340
+ const targetTokenOptions = useMemo3(
1151
1341
  () => targetTokensFor(targetChain),
1152
1342
  [targetChain, targetTokensFor]
1153
1343
  );
@@ -1175,10 +1365,16 @@ function WithdrawFlow({
1175
1365
  const handleTargetTokenChange = useCallback2((token) => {
1176
1366
  setTargetToken(token);
1177
1367
  }, []);
1368
+ const handleFormFriction = useCallback2(
1369
+ (reason) => {
1370
+ analytics.friction(reason, "form");
1371
+ },
1372
+ [analytics]
1373
+ );
1178
1374
  const depositAddress = registration.kind === "ready" || registration.kind === "stale" ? registration.smartAccount : void 0;
1179
1375
  const stepView = deriveStepView(flowStep, registration);
1180
1376
  return /* @__PURE__ */ jsxs3("div", { className: "rs-modal-body", children: [
1181
- stepView === "form" && /* @__PURE__ */ jsx3(
1377
+ stepView === "form" && /* @__PURE__ */ jsx4(
1182
1378
  WithdrawFormStep,
1183
1379
  {
1184
1380
  publicClient,
@@ -1197,10 +1393,10 @@ function WithdrawFlow({
1197
1393
  registrationError: registration.kind === "failed" ? registration.error : null,
1198
1394
  onRecipientChange: setRecipientInput,
1199
1395
  onSubmit: async (r, a) => handleFormContinue(r, a),
1200
- onBalanceUsdChange: setTotalBalanceUsd
1396
+ onFriction: handleFormFriction
1201
1397
  }
1202
1398
  ),
1203
- stepView === "review" && /* @__PURE__ */ jsx3(
1399
+ stepView === "review" && /* @__PURE__ */ jsx4(
1204
1400
  WithdrawReviewStep,
1205
1401
  {
1206
1402
  sourceChain,
@@ -1217,7 +1413,7 @@ function WithdrawFlow({
1217
1413
  onError: handleError
1218
1414
  }
1219
1415
  ),
1220
- stepView === "processing" && registration.kind !== "idle" && /* @__PURE__ */ jsx3(
1416
+ stepView === "processing" && registration.kind !== "idle" && /* @__PURE__ */ jsx4(
1221
1417
  ProcessingStep,
1222
1418
  {
1223
1419
  smartAccount: storeApi.getState().processingSmartAccount ?? (registration.kind === "ready" || registration.kind === "stale" ? registration.smartAccount : "0x0"),
@@ -1233,7 +1429,10 @@ function WithdrawFlow({
1233
1429
  service,
1234
1430
  flowLabel: "withdraw",
1235
1431
  onClose,
1236
- onNewDeposit: () => storeApi.dispatch({ type: "flow/reset" }),
1432
+ onNewDeposit: () => {
1433
+ analytics.resetAttempt();
1434
+ storeApi.dispatch({ type: "flow/reset" });
1435
+ },
1237
1436
  onDepositComplete: handleWithdrawComplete,
1238
1437
  onDepositFailed: handleWithdrawFailed,
1239
1438
  onError: handleError,
@@ -1249,6 +1448,13 @@ function toBaseUnits(amount, decimals) {
1249
1448
  return "0";
1250
1449
  }
1251
1450
  }
1451
+ function analyticsStepForFlowStep(step) {
1452
+ if (step === "review") return "review";
1453
+ if (step === "processing" || step === "complete" || step === "failed") {
1454
+ return "processing";
1455
+ }
1456
+ return "form";
1457
+ }
1252
1458
  function deriveStepView(step, _registration) {
1253
1459
  if (step === "review") return "review";
1254
1460
  if (step === "processing" || step === "complete" || step === "failed") {
@@ -1258,7 +1464,32 @@ function deriveStepView(step, _registration) {
1258
1464
  }
1259
1465
 
1260
1466
  // src/WithdrawModal.tsx
1261
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1467
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1468
+ function buildWithdrawAnalyticsSessionProperties({
1469
+ inline = false,
1470
+ closeOnOverlayClick = false,
1471
+ targetChain,
1472
+ targetToken,
1473
+ recipient,
1474
+ defaultAmount,
1475
+ seedTargetChain,
1476
+ seedTargetToken
1477
+ }) {
1478
+ return {
1479
+ presentation: inline ? "inline" : "overlay",
1480
+ overlay_close_enabled: !inline && closeOnOverlayClick,
1481
+ prefills: {
1482
+ target_chain: targetChain !== void 0,
1483
+ target_token: targetToken !== void 0,
1484
+ recipient: recipient !== void 0,
1485
+ amount: defaultAmount !== void 0
1486
+ },
1487
+ ...analyticsTargetProperties({
1488
+ targetChain: seedTargetChain,
1489
+ targetToken: seedTargetToken
1490
+ })
1491
+ };
1492
+ }
1262
1493
  function WithdrawModal({
1263
1494
  accountAddress,
1264
1495
  sourceChain: sourceChainProp,
@@ -1282,6 +1513,8 @@ function WithdrawModal({
1282
1513
  onLifecycle,
1283
1514
  onEvent,
1284
1515
  onError,
1516
+ enableAnalyticsIngest,
1517
+ analyticsIngestUrl,
1285
1518
  debug,
1286
1519
  // Every prop 0.9.0 removed lands here, since none of them is named above.
1287
1520
  // `uiConfig` is destructured, so its removed nested key isn't scanned — that
@@ -1295,6 +1528,10 @@ function WithdrawModal({
1295
1528
  checkBackendUrl("WithdrawModal", backendUrl);
1296
1529
  const modalRef = useRef3(null);
1297
1530
  const onReadyRef = useLatestRef(onReady);
1531
+ const analyticsControllerRef = useRef3(
1532
+ null
1533
+ );
1534
+ const previousOpenRef = useRef3(isOpen);
1298
1535
  const [backHandler, setBackHandler] = useState4(
1299
1536
  void 0
1300
1537
  );
@@ -1303,11 +1540,11 @@ function WithdrawModal({
1303
1540
  const targetChain = getChainId(targetChainProp ?? sourceChainProp);
1304
1541
  const sourceChain = getChainId(sourceChainProp);
1305
1542
  const rpcUrls = useStableRpcUrls(rpcUrlsProp);
1306
- const service = useMemo3(
1543
+ const service = useMemo4(
1307
1544
  () => createDepositService(backendUrl, { debug, debugScope: "service:withdraw" }),
1308
1545
  [backendUrl, debug]
1309
1546
  );
1310
- const store = useMemo3(() => createWithdrawStore(), []);
1547
+ const store = useMemo4(() => createWithdrawStore(), []);
1311
1548
  const onErrorRef = useLatestRef(onError);
1312
1549
  useEffect3(() => {
1313
1550
  if (isOpen && typeof onSendTransaction !== "function") {
@@ -1315,8 +1552,38 @@ function WithdrawModal({
1315
1552
  message: "WithdrawModal requires an `onSendTransaction` function.",
1316
1553
  code: "WITHDRAW_MISSING_SEND_TRANSACTION"
1317
1554
  });
1555
+ analyticsControllerRef.current?.failure(
1556
+ "submission_handler_missing",
1557
+ false,
1558
+ "form"
1559
+ );
1318
1560
  }
1319
1561
  }, [isOpen, onSendTransaction, onErrorRef]);
1562
+ const closeWithSource = useCallback3(
1563
+ (source) => {
1564
+ const analytics = analyticsControllerRef.current;
1565
+ analytics?.abandon("modal_close");
1566
+ analytics?.close(source);
1567
+ onClose();
1568
+ },
1569
+ [onClose]
1570
+ );
1571
+ const handleModalDismiss = useCallback3(
1572
+ (reason) => {
1573
+ closeWithSource(
1574
+ reason === "escape" ? "escape" : reason === "overlay" ? "overlay" : "header_button"
1575
+ );
1576
+ },
1577
+ [closeWithSource]
1578
+ );
1579
+ useEffect3(() => {
1580
+ if (previousOpenRef.current && !isOpen) {
1581
+ const analytics = analyticsControllerRef.current;
1582
+ analytics?.abandon("modal_close");
1583
+ analytics?.close("host_controlled");
1584
+ }
1585
+ previousOpenRef.current = isOpen;
1586
+ }, [isOpen]);
1320
1587
  useEffect3(() => {
1321
1588
  if (!isOpen || !modalRef.current) return;
1322
1589
  return applyTheme(modalRef.current, theme);
@@ -1342,65 +1609,92 @@ function WithdrawModal({
1342
1609
  submitting: false
1343
1610
  });
1344
1611
  }, [screen, backHandler, onUiStateRef]);
1612
+ const analyticsSessionProperties = buildWithdrawAnalyticsSessionProperties({
1613
+ inline,
1614
+ closeOnOverlayClick,
1615
+ targetChain: targetChainProp,
1616
+ targetToken,
1617
+ recipient,
1618
+ defaultAmount,
1619
+ seedTargetChain: targetChain,
1620
+ seedTargetToken: targetToken ?? sourceToken
1621
+ });
1345
1622
  const showBackButton = uiConfig?.showBackButton ?? true;
1346
1623
  const canGoBack = backHandler !== void 0;
1347
- return /* @__PURE__ */ jsx4(RpcUrlsProvider, { value: rpcUrls, children: /* @__PURE__ */ jsx4(ChainCatalogProvider, { service, rpcUrls, children: /* @__PURE__ */ jsx4(WithdrawStoreProvider, { store, children: /* @__PURE__ */ jsx4(
1624
+ return /* @__PURE__ */ jsx5(RpcUrlsProvider, { value: rpcUrls, children: /* @__PURE__ */ jsx5(ChainCatalogProvider, { service, rpcUrls, children: /* @__PURE__ */ jsx5(WithdrawStoreProvider, { store, children: /* @__PURE__ */ jsx5(
1348
1625
  Modal,
1349
1626
  {
1350
1627
  isOpen,
1351
- onClose,
1628
+ onClose: handleModalDismiss,
1352
1629
  className,
1353
1630
  inline,
1354
1631
  closeOnOverlayClick,
1355
- children: /* @__PURE__ */ jsxs4("div", { ref: modalRef, className: "rs-modal", children: [
1356
- /* @__PURE__ */ jsxs4("div", { className: "rs-modal-header--redesigned", children: [
1357
- /* @__PURE__ */ jsx4("div", { className: "rs-modal-header-nav-left", children: showBackButton && canGoBack && backHandler && /* @__PURE__ */ jsx4(
1358
- "button",
1359
- {
1360
- type: "button",
1361
- className: "rs-modal-header-back",
1362
- "aria-label": "Go back",
1363
- onClick: backHandler,
1364
- children: /* @__PURE__ */ jsx4(ChevronLeftIcon, {})
1365
- }
1366
- ) }),
1367
- /* @__PURE__ */ jsx4("div", { className: "rs-modal-header-nav-right", children: /* @__PURE__ */ jsx4(
1368
- "button",
1632
+ children: /* @__PURE__ */ jsx5(
1633
+ AnalyticsSession,
1634
+ {
1635
+ modal: "withdraw",
1636
+ sessionProperties: analyticsSessionProperties,
1637
+ onEvent,
1638
+ backendUrl,
1639
+ enableIngest: enableAnalyticsIngest,
1640
+ ingestUrl: analyticsIngestUrl,
1641
+ children: /* @__PURE__ */ jsx5(
1642
+ WithdrawAnalyticsControllerProvider,
1369
1643
  {
1370
- type: "button",
1371
- onClick: onClose,
1372
- className: "rs-modal-close",
1373
- "aria-label": "Close",
1374
- children: /* @__PURE__ */ jsx4(CloseIcon, {})
1644
+ controllerRef: analyticsControllerRef,
1645
+ children: /* @__PURE__ */ jsxs4("div", { ref: modalRef, className: "rs-modal", children: [
1646
+ /* @__PURE__ */ jsxs4("div", { className: "rs-modal-header--redesigned", children: [
1647
+ /* @__PURE__ */ jsx5("div", { className: "rs-modal-header-nav-left", children: showBackButton && canGoBack && backHandler && /* @__PURE__ */ jsx5(
1648
+ "button",
1649
+ {
1650
+ type: "button",
1651
+ className: "rs-modal-header-back",
1652
+ "aria-label": "Go back",
1653
+ onClick: backHandler,
1654
+ children: /* @__PURE__ */ jsx5(ChevronLeftIcon, {})
1655
+ }
1656
+ ) }),
1657
+ /* @__PURE__ */ jsx5("div", { className: "rs-modal-header-nav-right", children: /* @__PURE__ */ jsx5(
1658
+ "button",
1659
+ {
1660
+ type: "button",
1661
+ onClick: () => closeWithSource("header_button"),
1662
+ className: "rs-modal-close",
1663
+ "aria-label": "Close",
1664
+ children: /* @__PURE__ */ jsx5(CloseIcon, {})
1665
+ }
1666
+ ) })
1667
+ ] }),
1668
+ /* @__PURE__ */ jsx5(
1669
+ WithdrawFlow,
1670
+ {
1671
+ accountAddress,
1672
+ sourceChain,
1673
+ sourceToken,
1674
+ targetChain,
1675
+ targetToken: targetToken ?? sourceToken,
1676
+ recipient,
1677
+ amount: defaultAmount,
1678
+ service,
1679
+ forceRegister,
1680
+ onSendTransaction,
1681
+ onStepChange: handleStepChange,
1682
+ onClose: closeWithSource,
1683
+ onLifecycle,
1684
+ onError,
1685
+ debug
1686
+ }
1687
+ )
1688
+ ] })
1375
1689
  }
1376
- ) })
1377
- ] }),
1378
- /* @__PURE__ */ jsx4(
1379
- WithdrawFlow,
1380
- {
1381
- accountAddress,
1382
- sourceChain,
1383
- sourceToken,
1384
- targetChain,
1385
- targetToken: targetToken ?? sourceToken,
1386
- recipient,
1387
- amount: defaultAmount,
1388
- service,
1389
- forceRegister,
1390
- onSendTransaction,
1391
- onStepChange: handleStepChange,
1392
- onClose,
1393
- onLifecycle,
1394
- onEvent,
1395
- onError,
1396
- debug
1397
- }
1398
- )
1399
- ] })
1690
+ )
1691
+ }
1692
+ )
1400
1693
  }
1401
1694
  ) }) }) });
1402
1695
  }
1403
1696
 
1404
1697
  export {
1698
+ buildWithdrawAnalyticsSessionProperties,
1405
1699
  WithdrawModal
1406
1700
  };