@rhinestone/deposit-modal 0.12.2 → 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 (68) hide show
  1. package/README.md +73 -18
  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-VG66OTIU.cjs → DepositModalReown-46LPVFB6.cjs} +58 -35
  6. package/dist/{DepositModalReown-TEC3MEZ4.mjs → DepositModalReown-Q2JMLT47.mjs} +45 -22
  7. package/dist/{QRCode-WUC652SH.mjs → QRCode-5RGIZZHL.mjs} +1 -1
  8. package/dist/{QRCode-5DXFNKI2.cjs → QRCode-JKXG4RHN.cjs} +2 -2
  9. package/dist/WithdrawModal-5ICGD4HJ.mjs +12 -0
  10. package/dist/WithdrawModal-VP6PMIRO.cjs +12 -0
  11. package/dist/{chunk-O7BCVQ6L.cjs → chunk-4KCSH4T3.cjs} +157 -199
  12. package/dist/chunk-6KG642KH.cjs +31 -0
  13. package/dist/{chunk-PWLEBV5V.cjs → chunk-7ICSHJFE.cjs} +296 -220
  14. package/dist/{chunk-J46WDSRD.mjs → chunk-7SR7ENVJ.mjs} +165 -89
  15. package/dist/{chunk-2TPSF77V.mjs → chunk-CFKC77SV.mjs} +455 -261
  16. package/dist/{chunk-75DVGSAX.cjs → chunk-DW2EQA2U.cjs} +6 -3
  17. package/dist/{chunk-FXXUIK4E.mjs → chunk-IN77XSWO.mjs} +899 -153
  18. package/dist/chunk-K4H6TGBE.mjs +31 -0
  19. package/dist/{chunk-SDSKIJC7.mjs → chunk-KTUVSKR4.mjs} +2410 -1177
  20. package/dist/{chunk-HL5NUKZW.cjs → chunk-LZFRL2AL.cjs} +3 -3
  21. package/dist/{chunk-UQIJU6HV.cjs → chunk-MTFJSE52.cjs} +499 -191
  22. package/dist/{chunk-FLVLYO64.mjs → chunk-PKML3XVS.mjs} +430 -122
  23. package/dist/{chunk-4ZXQPFNU.mjs → chunk-RN5VKWI6.mjs} +4 -1
  24. package/dist/{chunk-BRYNGLDM.cjs → chunk-V2OBGLJD.cjs} +961 -215
  25. package/dist/{chunk-3STTWRAN.mjs → chunk-VUATPOWJ.mjs} +147 -189
  26. package/dist/{chunk-JE4R5VA3.mjs → chunk-WUJIKCFU.mjs} +1 -1
  27. package/dist/chunk-X2IPAPXU.cjs +163 -0
  28. package/dist/{chunk-OIN4CXCP.cjs → chunk-YCQZJQO6.cjs} +2343 -1110
  29. package/dist/{chunk-TM7VGHAG.cjs → chunk-YWMWI7PZ.cjs} +462 -268
  30. package/dist/chunk-ZEO6ADM3.mjs +163 -0
  31. package/dist/claim.cjs +7 -6
  32. package/dist/claim.d.cts +11 -3
  33. package/dist/claim.d.ts +11 -3
  34. package/dist/claim.mjs +6 -5
  35. package/dist/{constants-FuzG3h4m.d.cts → constants-DBrWhJWE.d.cts} +12 -7
  36. package/dist/{constants-FuzG3h4m.d.ts → constants-DBrWhJWE.d.ts} +12 -7
  37. package/dist/constants.cjs +3 -9
  38. package/dist/constants.d.cts +2 -3
  39. package/dist/constants.d.ts +2 -3
  40. package/dist/constants.mjs +4 -10
  41. package/dist/deposit.cjs +8 -8
  42. package/dist/deposit.d.cts +2 -2
  43. package/dist/deposit.d.ts +2 -2
  44. package/dist/deposit.mjs +7 -7
  45. package/dist/embed.cjs +1476 -0
  46. package/dist/embed.d.cts +183 -0
  47. package/dist/embed.d.ts +183 -0
  48. package/dist/embed.mjs +1476 -0
  49. package/dist/index.cjs +11 -16
  50. package/dist/index.d.cts +3 -3
  51. package/dist/index.d.ts +3 -3
  52. package/dist/index.mjs +14 -19
  53. package/dist/modal-ui-state-OD1RmKkK.d.cts +30 -0
  54. package/dist/modal-ui-state-OD1RmKkK.d.ts +30 -0
  55. package/dist/polymarket.cjs +17 -7
  56. package/dist/polymarket.mjs +13 -3
  57. package/dist/styles.css +124 -98
  58. package/dist/types-Bho4OA03.d.ts +982 -0
  59. package/dist/types-DP_3KwlD.d.cts +982 -0
  60. package/dist/withdraw.cjs +6 -6
  61. package/dist/withdraw.d.cts +11 -3
  62. package/dist/withdraw.d.ts +11 -3
  63. package/dist/withdraw.mjs +5 -5
  64. package/package.json +59 -6
  65. package/dist/chunk-IAGFDSNC.mjs +0 -11
  66. package/dist/chunk-L32UI7LA.cjs +0 -11
  67. package/dist/types-BO4iP6fG.d.cts +0 -612
  68. package/dist/types-oFblf65g.d.ts +0 -612
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  ProcessingStep,
3
+ analyticsTargetProperties,
3
4
  deriveAccountSalt,
5
+ isSubmissionUncertain,
4
6
  useTokenPrices,
5
7
  warnRemovedProps
6
- } from "./chunk-J46WDSRD.mjs";
8
+ } from "./chunk-7SR7ENVJ.mjs";
7
9
  import {
10
+ AnalyticsSession,
8
11
  BodyHeader,
9
12
  Button,
10
13
  Callout,
@@ -29,15 +32,16 @@ import {
29
32
  formatTokenAmount,
30
33
  formatUserError,
31
34
  getAssetId,
32
- getPublicClient,
33
35
  isNativeAsset,
34
36
  isRecognizedToken,
35
37
  useDestinationChains,
36
38
  useLatestRef,
39
+ useReadClient,
37
40
  useRpcUrls,
38
41
  useStableRpcUrls,
39
- useTargetTokens
40
- } from "./chunk-FXXUIK4E.mjs";
42
+ useTargetTokens,
43
+ useWithdrawAnalyticsEmitter
44
+ } from "./chunk-IN77XSWO.mjs";
41
45
  import {
42
46
  NATIVE_TOKEN_ADDRESS,
43
47
  getChainIcon,
@@ -46,13 +50,13 @@ import {
46
50
  getTokenDecimalsByAddress,
47
51
  getTokenSymbol,
48
52
  targetChainToCaip2
49
- } from "./chunk-3STTWRAN.mjs";
53
+ } from "./chunk-VUATPOWJ.mjs";
50
54
 
51
55
  // src/WithdrawModal.tsx
52
- 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";
53
57
 
54
58
  // src/WithdrawFlow.tsx
55
- 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";
56
60
 
57
61
  // src/store/withdraw.ts
58
62
  import { createContext, createElement, useContext } from "react";
@@ -218,7 +222,7 @@ function WithdrawFormStep({
218
222
  registrationError = null,
219
223
  onRecipientChange,
220
224
  onSubmit,
221
- onBalanceUsdChange
225
+ onFriction
222
226
  }) {
223
227
  const [recipient, setRecipient] = useState(defaultRecipient ?? "");
224
228
  const [amount, setAmount] = useState(defaultAmount ?? "");
@@ -231,6 +235,7 @@ function WithdrawFormStep({
231
235
  const chainDropdownRef = useRef(null);
232
236
  const tokenDropdownRef = useRef(null);
233
237
  const publicClientChainId = publicClient.chain?.id;
238
+ const onFrictionRef = useLatestRef(onFriction);
234
239
  useClickOutside(chainDropdownRef, () => setShowChainDropdown(false));
235
240
  useClickOutside(tokenDropdownRef, () => setShowTokenDropdown(false));
236
241
  const targetSymbol = getTokenSymbol(targetToken, targetChain);
@@ -271,6 +276,7 @@ function WithdrawFormStep({
271
276
  } catch {
272
277
  if (active) {
273
278
  setBalance(null);
279
+ onFrictionRef.current?.("wallet_balance_unavailable");
274
280
  timeoutId = setTimeout(fetchBalance, retryDelayMs);
275
281
  retryDelayMs = Math.min(Math.round(retryDelayMs * 1.5), 3e4);
276
282
  }
@@ -285,7 +291,7 @@ function WithdrawFormStep({
285
291
  clearTimeout(timeoutId);
286
292
  }
287
293
  };
288
- }, [accountAddress, publicClient, publicClientChainId, asset]);
294
+ }, [accountAddress, publicClient, publicClientChainId, asset, onFrictionRef]);
289
295
  const formattedBalance = useMemo(() => {
290
296
  if (balance === null) return "...";
291
297
  try {
@@ -300,18 +306,9 @@ function WithdrawFormStep({
300
306
  }
301
307
  }, [balance, asset.decimals]);
302
308
  useEffect(() => {
303
- if (balance === null) return;
304
- const sym = asset.symbol.toUpperCase();
305
- if (sym !== "USDC" && sym !== "USDT" && sym !== "USDT0") return;
306
- try {
307
- const raw = formatUnits(balance, asset.decimals);
308
- const numeric = Number(raw);
309
- if (!Number.isFinite(numeric)) return;
310
- onBalanceUsdChange?.(numeric);
311
- } catch {
312
- return;
313
- }
314
- }, [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]);
315
312
  const amountUsd = useMemo(() => {
316
313
  if (!amount) return null;
317
314
  const parsed = Number(amount);
@@ -331,6 +328,7 @@ function WithdrawFormStep({
331
328
  const handleWithdraw = useCallback(async () => {
332
329
  if (!recipient || !/^0x[a-fA-F0-9]{40}$/.test(recipient)) {
333
330
  setError("Enter a valid recipient address");
331
+ onFriction?.("recipient_invalid");
334
332
  return;
335
333
  }
336
334
  const lowerRecipient = recipient.toLowerCase();
@@ -338,11 +336,13 @@ function WithdrawFormStep({
338
336
  setError(
339
337
  "Recipient can't be your own account. Send to a different wallet."
340
338
  );
339
+ onFriction?.("recipient_not_allowed");
341
340
  return;
342
341
  }
343
342
  const parsed = Number(amount);
344
343
  if (!amount || !Number.isFinite(parsed) || parsed <= 0) {
345
344
  setError("Enter a valid amount");
345
+ onFriction?.("amount_invalid");
346
346
  return;
347
347
  }
348
348
  if (balance !== null) {
@@ -350,10 +350,12 @@ function WithdrawFormStep({
350
350
  const amountInUnits = parseUnits(amount, asset.decimals);
351
351
  if (amountInUnits > balance) {
352
352
  setError("Insufficient balance");
353
+ onFriction?.("amount_insufficient_balance");
353
354
  return;
354
355
  }
355
356
  } catch {
356
357
  setError("Invalid amount");
358
+ onFriction?.("amount_invalid");
357
359
  return;
358
360
  }
359
361
  }
@@ -374,7 +376,8 @@ function WithdrawFormStep({
374
376
  asset.decimals,
375
377
  accountAddress,
376
378
  depositAddress,
377
- onSubmit
379
+ onSubmit,
380
+ onFriction
378
381
  ]);
379
382
  if (isBalanceLoading) {
380
383
  return /* @__PURE__ */ jsxs("div", { className: "rs-screen", children: [
@@ -660,6 +663,7 @@ function WithdrawReviewStep({
660
663
  prices
661
664
  }) ?? // No price for the target token — an honest dash beats a wrong number.
662
665
  "\u2014";
666
+ const [submissionUncertain, setSubmissionUncertain] = useState2(false);
663
667
  const handleConfirm = async () => {
664
668
  setError(null);
665
669
  setIsSubmitting(true);
@@ -667,14 +671,16 @@ function WithdrawReviewStep({
667
671
  await onConfirm();
668
672
  } catch (err) {
669
673
  const raw = err instanceof Error ? err.message : "Withdraw failed";
670
- const message = formatUserError(raw);
674
+ const uncertain = isSubmissionUncertain(err);
675
+ const message = uncertain ? "Withdrawal status is uncertain. Check your activity before trying again." : formatUserError(raw);
676
+ if (uncertain) setSubmissionUncertain(true);
671
677
  setError(message);
672
678
  onError?.(message, "WITHDRAW_REVIEW_ERROR", raw);
673
679
  } finally {
674
680
  setIsSubmitting(false);
675
681
  }
676
682
  };
677
- const disabled = isSubmitting || registrationPending || Boolean(registrationError);
683
+ const disabled = isSubmitting || submissionUncertain || registrationPending || Boolean(registrationError);
678
684
  return /* @__PURE__ */ jsxs2("div", { className: "rs-screen", children: [
679
685
  /* @__PURE__ */ jsxs2("div", { className: "rs-screen-body rs-screen-body--gap-32", children: [
680
686
  /* @__PURE__ */ jsx2(
@@ -747,7 +753,171 @@ WithdrawReviewStep.displayName = "WithdrawReviewStep";
747
753
 
748
754
  // src/WithdrawFlow.tsx
749
755
  import { parseUnits as parseUnits2 } from "viem";
750
- 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";
751
921
  var ADDR_REGEX = /^0x[a-fA-F0-9]{40}$/;
752
922
  var REGISTRATION_DEBOUNCE_MS = 600;
753
923
  var WITHDRAW_INDEXING_DELAY_MS = 2e3;
@@ -769,17 +939,19 @@ function WithdrawFlow({
769
939
  onStepChange,
770
940
  onClose,
771
941
  onLifecycle,
772
- onEvent,
773
942
  onError,
774
943
  debug
775
944
  }) {
776
945
  const rpcUrls = useRpcUrls();
777
946
  const onStepChangeRef = useLatestRef(onStepChange);
778
- const onEventRef = useLatestRef(onEvent);
947
+ const analytics = useWithdrawAnalyticsController();
779
948
  const onLifecycleRef = useLatestRef(onLifecycle);
780
949
  const onErrorRef = useLatestRef(onError);
781
950
  const isFlowActiveRef = useRef2(true);
782
951
  const submitGenerationRef = useRef2(0);
952
+ const lastSubmitFailureRef = useRef2(
953
+ null
954
+ );
783
955
  useEffect2(() => {
784
956
  isFlowActiveRef.current = true;
785
957
  return () => {
@@ -803,7 +975,6 @@ function WithdrawFlow({
803
975
  const [recipientInput, setRecipientInput] = useState3(
804
976
  defaultRecipient ?? ""
805
977
  );
806
- const [totalBalanceUsd, setTotalBalanceUsd] = useState3(0);
807
978
  useEffect2(() => {
808
979
  setTargetChain(targetChainProp);
809
980
  }, [targetChainProp]);
@@ -817,11 +988,8 @@ function WithdrawFlow({
817
988
  const registration = useWithdrawStore((s) => s.registration);
818
989
  const flowStep = useWithdrawStore((s) => s.step);
819
990
  const reviewedRecipient = useWithdrawStore((s) => s.recipient);
820
- const publicClient = useMemo2(
821
- () => getPublicClient(sourceChain, rpcUrls),
822
- [sourceChain, rpcUrls]
823
- );
824
- const recipientForRegistration = useMemo2(() => {
991
+ const publicClient = useReadClient(sourceChain, rpcUrls);
992
+ const recipientForRegistration = useMemo3(() => {
825
993
  if (!recipientInput) return void 0;
826
994
  return ADDR_REGEX.test(recipientInput) ? recipientInput : void 0;
827
995
  }, [recipientInput]);
@@ -906,7 +1074,7 @@ function WithdrawFlow({
906
1074
  onLifecycleRef,
907
1075
  onErrorRef
908
1076
  ]);
909
- const asset = useMemo2(() => {
1077
+ const asset = useMemo3(() => {
910
1078
  const symbol = getTokenSymbol(sourceToken, sourceChain);
911
1079
  const decimals = getTokenDecimalsByAddress(sourceToken, sourceChain);
912
1080
  return {
@@ -926,32 +1094,35 @@ function WithdrawFlow({
926
1094
  recipient: reviewedRecipient
927
1095
  } : null;
928
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]);
929
1102
  const reviewBackHandler = useCallback2(() => {
930
1103
  submitGenerationRef.current += 1;
1104
+ analytics.abandon("back");
931
1105
  storeApi.dispatch({ type: "review/back" });
932
- }, [storeApi]);
1106
+ }, [analytics, storeApi]);
933
1107
  useEffect2(() => {
934
1108
  onStepChangeRef.current?.(
935
- flowStep === "review" ? reviewBackHandler : void 0
1109
+ flowStep === "review" ? reviewBackHandler : void 0,
1110
+ flowStep
936
1111
  );
937
1112
  }, [flowStep, reviewBackHandler, onStepChangeRef]);
938
- const lastStepOpenEventKeyRef = useRef2(null);
939
1113
  useEffect2(() => {
940
- if (flowStep !== "form") {
941
- lastStepOpenEventKeyRef.current = null;
942
- 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);
943
1124
  }
944
- if (lastStepOpenEventKeyRef.current === "form") return;
945
- lastStepOpenEventKeyRef.current = "form";
946
- const tokenSymbol = getTokenSymbol(targetToken, targetChain);
947
- const chainName = getChainName(targetChain);
948
- onEventRef.current?.({
949
- type: "withdraw_modal_select_amount_open",
950
- pred_balance: totalBalanceUsd,
951
- default_token: tokenSymbol,
952
- default_chain: chainName
953
- });
954
- }, [flowStep, targetToken, targetChain, totalBalanceUsd, onEventRef]);
1125
+ }, [analytics, registration.kind]);
955
1126
  useEffect2(() => {
956
1127
  logFlow("state:changed", {
957
1128
  step: flowStep,
@@ -972,33 +1143,34 @@ function WithdrawFlow({
972
1143
  ]);
973
1144
  const handleError = useCallback2(
974
1145
  (message, code, cause) => {
1146
+ if (code === "PROCESS_TIMEOUT") {
1147
+ analytics.friction("processor_status_poll_failed", "processing");
1148
+ }
975
1149
  logFlowError("flow:error", cause ?? message, { code });
976
1150
  onErrorRef.current?.({ message, code, cause });
977
1151
  },
978
- [logFlowError, onErrorRef]
1152
+ [analytics, logFlowError, onErrorRef]
979
1153
  );
980
1154
  const handleFormContinue = useCallback2(
981
1155
  (recipient, amountValue) => {
982
- const tokenSymbol = getTokenSymbol(targetToken, targetChain);
983
- const chainName = getChainName(targetChain);
984
- onEventRef.current?.({
985
- type: "withdraw_modal_select_amount_cta_click",
986
- pred_balance: totalBalanceUsd,
987
- selected_token: tokenSymbol,
988
- selected_chain: chainName,
989
- amount: amountValue,
990
- cta_name: "withdraw"
991
- });
1156
+ analytics.completeStep("form");
992
1157
  storeApi.dispatch({
993
1158
  type: "form/continue",
994
1159
  amount: amountValue,
995
1160
  recipient
996
1161
  });
997
1162
  },
998
- [targetToken, targetChain, totalBalanceUsd, onEventRef, storeApi]
1163
+ [analytics, storeApi]
999
1164
  );
1000
1165
  const handleReviewConfirm = useCallback2(
1001
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");
1002
1174
  const state = storeApi.getState();
1003
1175
  const recipient = state.recipient;
1004
1176
  const amountValue = state.amount;
@@ -1016,11 +1188,13 @@ function WithdrawFlow({
1016
1188
  };
1017
1189
  const matchingRegistration = getMatchingRegistration();
1018
1190
  if (!matchingRegistration) {
1191
+ analytics.failure("registration_target_changed", true, "submit");
1019
1192
  throw new Error("Withdrawal target is still being prepared");
1020
1193
  }
1021
1194
  const smartAccount = matchingRegistration.smartAccount;
1022
1195
  const lowerRecipient = recipient.toLowerCase();
1023
1196
  if (lowerRecipient === accountAddress.toLowerCase() || lowerRecipient === smartAccount.toLowerCase()) {
1197
+ analytics.failure("recipient_not_allowed", false, "submit");
1024
1198
  throw new Error(
1025
1199
  "Recipient can't be your own account. Send to a different wallet."
1026
1200
  );
@@ -1058,6 +1232,15 @@ function WithdrawFlow({
1058
1232
  if (!result?.txHash) {
1059
1233
  throw new Error("onSendTransaction did not return a txHash");
1060
1234
  }
1235
+ analytics.handoff(
1236
+ {
1237
+ transactionHash: result.txHash,
1238
+ managedAccount: smartAccount,
1239
+ sameRoute: isSameRoute
1240
+ },
1241
+ "submit"
1242
+ );
1243
+ analytics.completeStep("submit");
1061
1244
  logFlow("withdraw:submitted", {
1062
1245
  txHash: result.txHash,
1063
1246
  smartAccount,
@@ -1084,6 +1267,9 @@ function WithdrawFlow({
1084
1267
  });
1085
1268
  } catch (err) {
1086
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");
1087
1273
  logFlowError("submit:failed", err, {
1088
1274
  sourceChain,
1089
1275
  sourceToken,
@@ -1095,6 +1281,7 @@ function WithdrawFlow({
1095
1281
  }
1096
1282
  },
1097
1283
  [
1284
+ analytics,
1098
1285
  targetChain,
1099
1286
  targetToken,
1100
1287
  asset.decimals,
@@ -1112,6 +1299,8 @@ function WithdrawFlow({
1112
1299
  );
1113
1300
  const handleWithdrawComplete = useCallback2(
1114
1301
  (txHash, destinationTxHash, context) => {
1302
+ analytics.openStep("processing");
1303
+ analytics.outcome("completed");
1115
1304
  logFlow("withdraw:complete", { txHash, destinationTxHash, ...context });
1116
1305
  onLifecycleRef.current?.({
1117
1306
  type: "complete",
@@ -1129,10 +1318,13 @@ function WithdrawFlow({
1129
1318
  });
1130
1319
  storeApi.dispatch({ type: "withdraw/complete" });
1131
1320
  },
1132
- [logFlow, onLifecycleRef, storeApi]
1321
+ [analytics, logFlow, onLifecycleRef, storeApi]
1133
1322
  );
1134
1323
  const handleWithdrawFailed = useCallback2(
1135
1324
  (txHash, error) => {
1325
+ analytics.openStep("processing");
1326
+ analytics.outcome("failed");
1327
+ analytics.failure("processor_failed", false, "processing");
1136
1328
  logFlowError("withdraw:failed", error, { txHash });
1137
1329
  onLifecycleRef.current?.({
1138
1330
  type: "failed",
@@ -1141,11 +1333,11 @@ function WithdrawFlow({
1141
1333
  });
1142
1334
  storeApi.dispatch({ type: "withdraw/failed" });
1143
1335
  },
1144
- [logFlowError, onLifecycleRef, storeApi]
1336
+ [analytics, logFlowError, onLifecycleRef, storeApi]
1145
1337
  );
1146
1338
  const targetChainOptions = useDestinationChains();
1147
1339
  const targetTokensFor = useTargetTokens();
1148
- const targetTokenOptions = useMemo2(
1340
+ const targetTokenOptions = useMemo3(
1149
1341
  () => targetTokensFor(targetChain),
1150
1342
  [targetChain, targetTokensFor]
1151
1343
  );
@@ -1173,10 +1365,16 @@ function WithdrawFlow({
1173
1365
  const handleTargetTokenChange = useCallback2((token) => {
1174
1366
  setTargetToken(token);
1175
1367
  }, []);
1368
+ const handleFormFriction = useCallback2(
1369
+ (reason) => {
1370
+ analytics.friction(reason, "form");
1371
+ },
1372
+ [analytics]
1373
+ );
1176
1374
  const depositAddress = registration.kind === "ready" || registration.kind === "stale" ? registration.smartAccount : void 0;
1177
1375
  const stepView = deriveStepView(flowStep, registration);
1178
1376
  return /* @__PURE__ */ jsxs3("div", { className: "rs-modal-body", children: [
1179
- stepView === "form" && /* @__PURE__ */ jsx3(
1377
+ stepView === "form" && /* @__PURE__ */ jsx4(
1180
1378
  WithdrawFormStep,
1181
1379
  {
1182
1380
  publicClient,
@@ -1195,10 +1393,10 @@ function WithdrawFlow({
1195
1393
  registrationError: registration.kind === "failed" ? registration.error : null,
1196
1394
  onRecipientChange: setRecipientInput,
1197
1395
  onSubmit: async (r, a) => handleFormContinue(r, a),
1198
- onBalanceUsdChange: setTotalBalanceUsd
1396
+ onFriction: handleFormFriction
1199
1397
  }
1200
1398
  ),
1201
- stepView === "review" && /* @__PURE__ */ jsx3(
1399
+ stepView === "review" && /* @__PURE__ */ jsx4(
1202
1400
  WithdrawReviewStep,
1203
1401
  {
1204
1402
  sourceChain,
@@ -1215,7 +1413,7 @@ function WithdrawFlow({
1215
1413
  onError: handleError
1216
1414
  }
1217
1415
  ),
1218
- stepView === "processing" && registration.kind !== "idle" && /* @__PURE__ */ jsx3(
1416
+ stepView === "processing" && registration.kind !== "idle" && /* @__PURE__ */ jsx4(
1219
1417
  ProcessingStep,
1220
1418
  {
1221
1419
  smartAccount: storeApi.getState().processingSmartAccount ?? (registration.kind === "ready" || registration.kind === "stale" ? registration.smartAccount : "0x0"),
@@ -1231,7 +1429,10 @@ function WithdrawFlow({
1231
1429
  service,
1232
1430
  flowLabel: "withdraw",
1233
1431
  onClose,
1234
- onNewDeposit: () => storeApi.dispatch({ type: "flow/reset" }),
1432
+ onNewDeposit: () => {
1433
+ analytics.resetAttempt();
1434
+ storeApi.dispatch({ type: "flow/reset" });
1435
+ },
1235
1436
  onDepositComplete: handleWithdrawComplete,
1236
1437
  onDepositFailed: handleWithdrawFailed,
1237
1438
  onError: handleError,
@@ -1247,6 +1448,13 @@ function toBaseUnits(amount, decimals) {
1247
1448
  return "0";
1248
1449
  }
1249
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
+ }
1250
1458
  function deriveStepView(step, _registration) {
1251
1459
  if (step === "review") return "review";
1252
1460
  if (step === "processing" || step === "complete" || step === "failed") {
@@ -1256,7 +1464,32 @@ function deriveStepView(step, _registration) {
1256
1464
  }
1257
1465
 
1258
1466
  // src/WithdrawModal.tsx
1259
- 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
+ }
1260
1493
  function WithdrawModal({
1261
1494
  accountAddress,
1262
1495
  sourceChain: sourceChainProp,
@@ -1280,27 +1513,38 @@ function WithdrawModal({
1280
1513
  onLifecycle,
1281
1514
  onEvent,
1282
1515
  onError,
1516
+ enableAnalyticsIngest,
1517
+ analyticsIngestUrl,
1283
1518
  debug,
1284
1519
  // Every prop 0.9.0 removed lands here, since none of them is named above.
1285
1520
  // `uiConfig` is destructured, so its removed nested key isn't scanned — that
1286
1521
  // one only ever applied to the deposit flow.
1522
+ // Before the rest element, or it lands in `removedProps` and gets warned
1523
+ // about as a prop 0.9.0 removed.
1524
+ onUiState,
1287
1525
  ...removedProps
1288
1526
  }) {
1289
1527
  warnRemovedProps("WithdrawModal", removedProps);
1290
1528
  checkBackendUrl("WithdrawModal", backendUrl);
1291
1529
  const modalRef = useRef3(null);
1292
1530
  const onReadyRef = useLatestRef(onReady);
1531
+ const analyticsControllerRef = useRef3(
1532
+ null
1533
+ );
1534
+ const previousOpenRef = useRef3(isOpen);
1293
1535
  const [backHandler, setBackHandler] = useState4(
1294
1536
  void 0
1295
1537
  );
1538
+ const [screen, setScreen] = useState4("form");
1539
+ const onUiStateRef = useLatestRef(onUiState);
1296
1540
  const targetChain = getChainId(targetChainProp ?? sourceChainProp);
1297
1541
  const sourceChain = getChainId(sourceChainProp);
1298
1542
  const rpcUrls = useStableRpcUrls(rpcUrlsProp);
1299
- const service = useMemo3(
1543
+ const service = useMemo4(
1300
1544
  () => createDepositService(backendUrl, { debug, debugScope: "service:withdraw" }),
1301
1545
  [backendUrl, debug]
1302
1546
  );
1303
- const store = useMemo3(() => createWithdrawStore(), []);
1547
+ const store = useMemo4(() => createWithdrawStore(), []);
1304
1548
  const onErrorRef = useLatestRef(onError);
1305
1549
  useEffect3(() => {
1306
1550
  if (isOpen && typeof onSendTransaction !== "function") {
@@ -1308,12 +1552,41 @@ function WithdrawModal({
1308
1552
  message: "WithdrawModal requires an `onSendTransaction` function.",
1309
1553
  code: "WITHDRAW_MISSING_SEND_TRANSACTION"
1310
1554
  });
1555
+ analyticsControllerRef.current?.failure(
1556
+ "submission_handler_missing",
1557
+ false,
1558
+ "form"
1559
+ );
1311
1560
  }
1312
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
+ );
1313
1579
  useEffect3(() => {
1314
- if (isOpen && modalRef.current) {
1315
- applyTheme(modalRef.current, theme);
1580
+ if (previousOpenRef.current && !isOpen) {
1581
+ const analytics = analyticsControllerRef.current;
1582
+ analytics?.abandon("modal_close");
1583
+ analytics?.close("host_controlled");
1316
1584
  }
1585
+ previousOpenRef.current = isOpen;
1586
+ }, [isOpen]);
1587
+ useEffect3(() => {
1588
+ if (!isOpen || !modalRef.current) return;
1589
+ return applyTheme(modalRef.current, theme);
1317
1590
  }, [isOpen, theme]);
1318
1591
  useEffect3(() => {
1319
1592
  if (isOpen) {
@@ -1325,68 +1598,103 @@ function WithdrawModal({
1325
1598
  store.dispatch({ type: "modal/reset" });
1326
1599
  }
1327
1600
  }, [isOpen, store]);
1328
- const handleStepChange = useCallback3((onBack) => {
1601
+ const handleStepChange = useCallback3((onBack, next) => {
1329
1602
  setBackHandler(() => onBack);
1603
+ if (next) setScreen(next);
1330
1604
  }, []);
1605
+ useEffect3(() => {
1606
+ onUiStateRef.current?.({
1607
+ screen,
1608
+ back: backHandler,
1609
+ submitting: false
1610
+ });
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
+ });
1331
1622
  const showBackButton = uiConfig?.showBackButton ?? true;
1332
1623
  const canGoBack = backHandler !== void 0;
1333
- 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(
1334
1625
  Modal,
1335
1626
  {
1336
1627
  isOpen,
1337
- onClose,
1628
+ onClose: handleModalDismiss,
1338
1629
  className,
1339
1630
  inline,
1340
1631
  closeOnOverlayClick,
1341
- children: /* @__PURE__ */ jsxs4("div", { ref: modalRef, className: "rs-modal", children: [
1342
- /* @__PURE__ */ jsxs4("div", { className: "rs-modal-header--redesigned", children: [
1343
- /* @__PURE__ */ jsx4("div", { className: "rs-modal-header-nav-left", children: showBackButton && canGoBack && backHandler && /* @__PURE__ */ jsx4(
1344
- "button",
1345
- {
1346
- type: "button",
1347
- className: "rs-modal-header-back",
1348
- "aria-label": "Go back",
1349
- onClick: backHandler,
1350
- children: /* @__PURE__ */ jsx4(ChevronLeftIcon, {})
1351
- }
1352
- ) }),
1353
- /* @__PURE__ */ jsx4("div", { className: "rs-modal-header-nav-right", children: /* @__PURE__ */ jsx4(
1354
- "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,
1355
1643
  {
1356
- type: "button",
1357
- onClick: onClose,
1358
- className: "rs-modal-close",
1359
- "aria-label": "Close",
1360
- 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
+ ] })
1361
1689
  }
1362
- ) })
1363
- ] }),
1364
- /* @__PURE__ */ jsx4(
1365
- WithdrawFlow,
1366
- {
1367
- accountAddress,
1368
- sourceChain,
1369
- sourceToken,
1370
- targetChain,
1371
- targetToken: targetToken ?? sourceToken,
1372
- recipient,
1373
- amount: defaultAmount,
1374
- service,
1375
- forceRegister,
1376
- onSendTransaction,
1377
- onStepChange: handleStepChange,
1378
- onClose,
1379
- onLifecycle,
1380
- onEvent,
1381
- onError,
1382
- debug
1383
- }
1384
- )
1385
- ] })
1690
+ )
1691
+ }
1692
+ )
1386
1693
  }
1387
1694
  ) }) }) });
1388
1695
  }
1389
1696
 
1390
1697
  export {
1698
+ buildWithdrawAnalyticsSessionProperties,
1391
1699
  WithdrawModal
1392
1700
  };