@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,12 +1,13 @@
1
1
  import {
2
2
  RecoveryError,
3
3
  submitSignedRecovery
4
- } from "./chunk-YREVFB64.mjs";
4
+ } from "./chunk-ZEO6ADM3.mjs";
5
5
  import {
6
6
  ListRow
7
- } from "./chunk-XYJFEN5D.mjs";
7
+ } from "./chunk-WUJIKCFU.mjs";
8
8
  import {
9
9
  AlertTriangleIcon,
10
+ AnalyticsSession,
10
11
  BodyHeader,
11
12
  Button,
12
13
  Callout,
@@ -27,10 +28,11 @@ import {
27
28
  createDepositService,
28
29
  formatTokenAmount,
29
30
  formatUserError,
31
+ useClaimAnalyticsEmitter,
30
32
  useExplorerTxUrl,
31
33
  useLatestRef,
32
34
  useStableRpcUrls
33
- } from "./chunk-EOMT5LKX.mjs";
35
+ } from "./chunk-IN77XSWO.mjs";
34
36
  import {
35
37
  getChainIcon,
36
38
  getChainName,
@@ -43,7 +45,7 @@ import {
43
45
  import {
44
46
  useCallback,
45
47
  useEffect,
46
- useMemo,
48
+ useMemo as useMemo2,
47
49
  useReducer,
48
50
  useRef,
49
51
  useState
@@ -75,8 +77,193 @@ var Input = forwardRef(
75
77
  );
76
78
  Input.displayName = "Input";
77
79
 
80
+ // src/core/claim-analytics.tsx
81
+ import {
82
+ createContext,
83
+ useContext,
84
+ useMemo
85
+ } from "react";
86
+ import { jsx as jsx2 } from "react/jsx-runtime";
87
+ function claimFailureReasonForError(error) {
88
+ if (!(error instanceof RecoveryError)) return "refund_failed";
89
+ switch (error.code) {
90
+ case "SIGNATURE_INVALID":
91
+ return "signature_invalid";
92
+ case "DEPOSIT_NOT_RECOVERABLE":
93
+ return "recovery_deposit_not_recoverable";
94
+ case "RECOVERY_UNSUPPORTED":
95
+ return "recovery_unsupported";
96
+ case "VERIFICATION_UNAVAILABLE":
97
+ return "signature_verification_unavailable";
98
+ case "REFUND_RECONCILIATION_REQUIRED":
99
+ return "refund_reconciliation_required";
100
+ case "REFUND_FAILED":
101
+ return "refund_failed";
102
+ default:
103
+ if (error.phase === "signature") return "signature_rejected";
104
+ if (error.phase === "request") return "refund_service_unreachable";
105
+ return "refund_failed";
106
+ }
107
+ }
108
+ function createClaimAnalyticsController(emit) {
109
+ let step = null;
110
+ let afterHandoff = false;
111
+ let opened = false;
112
+ let closed = false;
113
+ const dedupe = /* @__PURE__ */ new Set();
114
+ const send = (payload) => {
115
+ controller.openSession();
116
+ emit(payload);
117
+ };
118
+ const emitOnce = (key, payload) => {
119
+ if (dedupe.has(key)) return;
120
+ dedupe.add(key);
121
+ send(payload);
122
+ };
123
+ const rearmStep = (rearmed) => {
124
+ for (const key of [...dedupe]) {
125
+ if (key.includes(`:${rearmed}:`)) dedupe.delete(key);
126
+ }
127
+ };
128
+ const controller = {
129
+ openSession() {
130
+ if (opened) return;
131
+ opened = true;
132
+ emit({ type: "claim_modal_open" });
133
+ },
134
+ openStep(nextStep) {
135
+ controller.openSession();
136
+ if (step === nextStep) return;
137
+ step = nextStep;
138
+ emitOnce(`step-open:${nextStep}`, {
139
+ type: "claim_modal_step_open",
140
+ step: nextStep
141
+ });
142
+ },
143
+ completeStep(completedStep = step ?? void 0) {
144
+ if (!completedStep) return;
145
+ emitOnce(`step-complete:${completedStep}`, {
146
+ type: "claim_modal_step_complete",
147
+ step: completedStep
148
+ });
149
+ },
150
+ rearmStep(rearmed) {
151
+ rearmStep(rearmed);
152
+ },
153
+ // One per submitted search, never deduplicated: the searched hash is
154
+ // deliberately not in the stream, so collapsing two searches that returned
155
+ // the same counts would make the second one unrecoverable.
156
+ lookupResult(matches, eligible) {
157
+ send({
158
+ type: "claim_modal_lookup_result",
159
+ matches,
160
+ eligible
161
+ });
162
+ },
163
+ selectDeposit({ depositId, transactionHash, autoSelected }) {
164
+ emitOnce(`deposit-selected:${depositId}`, {
165
+ type: "claim_modal_deposit_selected",
166
+ deposit_id: depositId,
167
+ transaction_hash: transactionHash,
168
+ auto_selected: autoSelected
169
+ });
170
+ },
171
+ friction(reason, eventStep = step ?? void 0) {
172
+ if (!eventStep) return;
173
+ emitOnce(`friction:${eventStep}:${reason}`, {
174
+ type: "claim_modal_friction",
175
+ step: eventStep,
176
+ reason
177
+ });
178
+ },
179
+ failure(reason, retryable, eventStep = step ?? void 0) {
180
+ if (!eventStep) return;
181
+ emitOnce(`failure:${eventStep}:${reason}`, {
182
+ type: "claim_modal_failure",
183
+ step: eventStep,
184
+ reason,
185
+ retryable
186
+ });
187
+ },
188
+ retry(reason, eventStep = step ?? void 0) {
189
+ if (!eventStep) return;
190
+ send({ type: "claim_modal_retry", step: eventStep, reason });
191
+ rearmStep(eventStep);
192
+ },
193
+ handoff({ depositId, transactionHash, refundTransactionHash }, eventStep) {
194
+ const handoffStep = eventStep ?? step ?? "submit";
195
+ if (step !== handoffStep) controller.openStep(handoffStep);
196
+ afterHandoff = true;
197
+ emitOnce(`handoff:${refundTransactionHash}`, {
198
+ type: "claim_modal_handoff",
199
+ step: handoffStep,
200
+ deposit_id: depositId,
201
+ transaction_hash: transactionHash,
202
+ refund_transaction_hash: refundTransactionHash
203
+ });
204
+ },
205
+ // Keyed by step so a retry re-arms it: a second failed submit is a second
206
+ // terminal outcome, not a duplicate of the first.
207
+ outcome(outcome) {
208
+ if (!step) return;
209
+ emitOnce(`outcome:${step}:${outcome}`, {
210
+ type: "claim_modal_ui_outcome",
211
+ step,
212
+ outcome
213
+ });
214
+ },
215
+ abandon(reason) {
216
+ if (!step || afterHandoff) return;
217
+ emitOnce(`abandon:${step}:${reason}`, {
218
+ type: "claim_modal_abandoned",
219
+ step,
220
+ reason
221
+ });
222
+ },
223
+ close(source) {
224
+ if (closed) return;
225
+ closed = true;
226
+ send({
227
+ type: "claim_modal_close",
228
+ source,
229
+ step,
230
+ after_handoff: afterHandoff
231
+ });
232
+ },
233
+ /** "Claim another deposit" on the success screen: a fresh attempt in the
234
+ * same session, so the envelope and its session id stay. */
235
+ resetAttempt() {
236
+ step = null;
237
+ afterHandoff = false;
238
+ dedupe.clear();
239
+ },
240
+ get step() {
241
+ return step;
242
+ },
243
+ get afterHandoff() {
244
+ return afterHandoff;
245
+ }
246
+ };
247
+ return controller;
248
+ }
249
+ var ClaimAnalyticsControllerContext = createContext(null);
250
+ var noopController = createClaimAnalyticsController(() => {
251
+ });
252
+ function ClaimAnalyticsControllerProvider({
253
+ children,
254
+ controllerRef
255
+ }) {
256
+ const emit = useClaimAnalyticsEmitter();
257
+ const controller = useMemo(() => createClaimAnalyticsController(emit), [emit]);
258
+ if (controllerRef) controllerRef.current = controller;
259
+ return /* @__PURE__ */ jsx2(ClaimAnalyticsControllerContext.Provider, { value: controller, children });
260
+ }
261
+ function useClaimAnalyticsController() {
262
+ return useContext(ClaimAnalyticsControllerContext) ?? noopController;
263
+ }
264
+
78
265
  // src/ClaimModal.tsx
79
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
266
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
80
267
  function evmChainIdFromCaip2(chain) {
81
268
  if (!chain?.startsWith("eip155:")) return null;
82
269
  const id = Number.parseInt(chain.slice("eip155:".length), 10);
@@ -169,6 +356,27 @@ function claimReducer(state, action) {
169
356
  return state;
170
357
  }
171
358
  }
359
+ function buildClaimAnalyticsSessionProperties({
360
+ inline = false,
361
+ closeOnOverlayClick = false,
362
+ defaultTxHash,
363
+ defaultRefundDestination
364
+ }) {
365
+ return {
366
+ presentation: inline ? "inline" : "overlay",
367
+ overlay_close_enabled: !inline && closeOnOverlayClick,
368
+ prefills: {
369
+ transaction_hash: defaultTxHash !== void 0,
370
+ refund_destination: defaultRefundDestination !== void 0
371
+ }
372
+ };
373
+ }
374
+ function analyticsStepForClaimStep(step) {
375
+ if (step === "lookup") return "lookup";
376
+ if (step === "select") return "select";
377
+ if (step === "review") return "review";
378
+ return "submit";
379
+ }
172
380
  function ClaimModal({
173
381
  signRecovery,
174
382
  isOpen,
@@ -186,6 +394,8 @@ function ClaimModal({
186
394
  onLifecycle,
187
395
  onEvent,
188
396
  onError,
397
+ enableAnalyticsIngest,
398
+ analyticsIngestUrl,
189
399
  debug,
190
400
  onUiState
191
401
  }) {
@@ -193,10 +403,12 @@ function ClaimModal({
193
403
  const modalRef = useRef(null);
194
404
  const onReadyRef = useLatestRef(onReady);
195
405
  const onLifecycleRef = useLatestRef(onLifecycle);
196
- const onEventRef = useLatestRef(onEvent);
197
406
  const onErrorRef = useLatestRef(onError);
407
+ const analyticsControllerRef = useRef(null);
408
+ const lastSubmitFailureRef = useRef(null);
409
+ const previousOpenRef = useRef(isOpen);
198
410
  const rpcUrls = useStableRpcUrls(rpcUrlsProp);
199
- const service = useMemo(
411
+ const service = useMemo2(
200
412
  () => createDepositService(backendUrl, { debug, debugScope: "service:claim" }),
201
413
  [backendUrl, debug]
202
414
  );
@@ -211,83 +423,138 @@ function ClaimModal({
211
423
  useEffect(() => {
212
424
  if (isOpen) {
213
425
  onReadyRef.current?.();
214
- onEventRef.current?.({ type: "claim_modal_open" });
215
426
  }
216
- }, [isOpen, onReadyRef, onEventRef]);
427
+ }, [isOpen, onReadyRef]);
428
+ useEffect(() => {
429
+ if (previousOpenRef.current && !isOpen) {
430
+ const analytics = analyticsControllerRef.current;
431
+ analytics?.abandon("modal_close");
432
+ analytics?.close("host_controlled");
433
+ }
434
+ previousOpenRef.current = isOpen;
435
+ }, [isOpen]);
217
436
  useEffect(() => {
218
437
  if (!isOpen) {
219
438
  operationEpochRef.current += 1;
220
439
  dispatch({ type: "reset" });
221
440
  }
222
441
  }, [isOpen]);
223
- const handleClose = useCallback(() => {
224
- if (state.step !== "processing") {
442
+ const closeWithSource = useCallback(
443
+ (source) => {
444
+ const analytics = analyticsControllerRef.current;
445
+ if (state.step === "processing") {
446
+ analytics?.friction("modal_close_refused_in_flight", "submit");
447
+ return;
448
+ }
449
+ analytics?.abandon("modal_close");
450
+ analytics?.close(source);
225
451
  operationEpochRef.current += 1;
226
452
  onClose();
453
+ },
454
+ [state.step, onClose]
455
+ );
456
+ const handleModalDismiss = useCallback(
457
+ (reason) => {
458
+ closeWithSource(
459
+ reason === "escape" ? "escape" : reason === "overlay" ? "overlay" : "header_button"
460
+ );
461
+ },
462
+ [closeWithSource]
463
+ );
464
+ const goBack = useCallback(() => {
465
+ const analytics = analyticsControllerRef.current;
466
+ const previousFailure = lastSubmitFailureRef.current;
467
+ if (state.step === "failed" && previousFailure?.retryable) {
468
+ lastSubmitFailureRef.current = null;
469
+ analytics?.retry(previousFailure.reason, "submit");
470
+ } else {
471
+ if (state.step === "failed") {
472
+ lastSubmitFailureRef.current = null;
473
+ analytics?.rearmStep("submit");
474
+ }
475
+ analytics?.abandon("back");
227
476
  }
228
- }, [state.step, onClose]);
477
+ dispatch({ type: "back" });
478
+ }, [state.step]);
229
479
  const canGoBack = state.step === "select" || state.step === "review" || state.step === "failed";
230
480
  const onUiStateRef = useLatestRef(onUiState);
231
481
  useEffect(() => {
232
482
  onUiStateRef.current?.({
233
483
  screen: state.step,
234
- back: canGoBack ? () => dispatch({ type: "back" }) : void 0,
484
+ back: canGoBack ? goBack : void 0,
235
485
  submitting: state.step === "processing"
236
486
  });
237
- }, [state.step, canGoBack, onUiStateRef]);
487
+ }, [state.step, canGoBack, goBack, onUiStateRef]);
238
488
  const showBackButton = uiConfig?.showBackButton ?? true;
239
- return /* @__PURE__ */ jsx2(RpcUrlsProvider, { value: rpcUrls, children: /* @__PURE__ */ jsx2(ChainCatalogProvider, { service, rpcUrls, children: /* @__PURE__ */ jsx2(
489
+ return /* @__PURE__ */ jsx3(RpcUrlsProvider, { value: rpcUrls, children: /* @__PURE__ */ jsx3(ChainCatalogProvider, { service, rpcUrls, children: /* @__PURE__ */ jsx3(
240
490
  Modal,
241
491
  {
242
492
  isOpen,
243
- onClose: handleClose,
493
+ onClose: handleModalDismiss,
244
494
  className,
245
495
  inline,
246
496
  closeOnOverlayClick,
247
- children: /* @__PURE__ */ jsxs2("div", { ref: modalRef, className: "rs-modal", children: [
248
- /* @__PURE__ */ jsxs2("div", { className: "rs-modal-header--redesigned", children: [
249
- /* @__PURE__ */ jsx2("div", { className: "rs-modal-header-nav-left", children: showBackButton && canGoBack && /* @__PURE__ */ jsx2(
250
- "button",
251
- {
252
- type: "button",
253
- className: "rs-modal-header-back",
254
- "aria-label": "Go back",
255
- onClick: () => dispatch({ type: "back" }),
256
- children: /* @__PURE__ */ jsx2(ChevronLeftIcon, {})
257
- }
258
- ) }),
259
- /* @__PURE__ */ jsx2("div", { className: "rs-modal-header-nav-right", children: /* @__PURE__ */ jsx2(
260
- "button",
261
- {
262
- type: "button",
263
- onClick: handleClose,
264
- disabled: state.step === "processing",
265
- className: "rs-modal-close",
266
- "aria-label": "Close",
267
- children: /* @__PURE__ */ jsx2(CloseIcon, {})
268
- }
269
- ) })
270
- ] }),
271
- /* @__PURE__ */ jsx2("div", { className: "rs-modal-body", children: /* @__PURE__ */ jsx2(
272
- ClaimBody,
273
- {
274
- state,
275
- dispatch,
276
- service,
277
- signRecovery,
278
- backendUrl,
279
- debug,
497
+ children: /* @__PURE__ */ jsx3(
498
+ AnalyticsSession,
499
+ {
500
+ modal: "claim",
501
+ sessionProperties: buildClaimAnalyticsSessionProperties({
502
+ inline,
503
+ closeOnOverlayClick,
280
504
  defaultTxHash,
281
- defaultRefundDestination,
282
- onClose: handleClose,
283
- operationEpochRef,
284
- isOpenRef,
285
- onLifecycleRef,
286
- onEventRef,
287
- onErrorRef
288
- }
289
- ) })
290
- ] })
505
+ defaultRefundDestination
506
+ }),
507
+ onEvent,
508
+ backendUrl,
509
+ enableIngest: enableAnalyticsIngest,
510
+ ingestUrl: analyticsIngestUrl,
511
+ children: /* @__PURE__ */ jsx3(ClaimAnalyticsControllerProvider, { controllerRef: analyticsControllerRef, children: /* @__PURE__ */ jsxs2("div", { ref: modalRef, className: "rs-modal", children: [
512
+ /* @__PURE__ */ jsxs2("div", { className: "rs-modal-header--redesigned", children: [
513
+ /* @__PURE__ */ jsx3("div", { className: "rs-modal-header-nav-left", children: showBackButton && canGoBack && /* @__PURE__ */ jsx3(
514
+ "button",
515
+ {
516
+ type: "button",
517
+ className: "rs-modal-header-back",
518
+ "aria-label": "Go back",
519
+ onClick: goBack,
520
+ children: /* @__PURE__ */ jsx3(ChevronLeftIcon, {})
521
+ }
522
+ ) }),
523
+ /* @__PURE__ */ jsx3("div", { className: "rs-modal-header-nav-right", children: /* @__PURE__ */ jsx3(
524
+ "button",
525
+ {
526
+ type: "button",
527
+ onClick: () => closeWithSource("header_button"),
528
+ disabled: state.step === "processing",
529
+ className: "rs-modal-close",
530
+ "aria-label": "Close",
531
+ children: /* @__PURE__ */ jsx3(CloseIcon, {})
532
+ }
533
+ ) })
534
+ ] }),
535
+ /* @__PURE__ */ jsx3("div", { className: "rs-modal-body", children: /* @__PURE__ */ jsx3(
536
+ ClaimBody,
537
+ {
538
+ state,
539
+ dispatch,
540
+ service,
541
+ signRecovery,
542
+ backendUrl,
543
+ debug,
544
+ defaultTxHash,
545
+ defaultRefundDestination,
546
+ onClose: closeWithSource,
547
+ onBack: goBack,
548
+ operationEpochRef,
549
+ isOpenRef,
550
+ lastSubmitFailureRef,
551
+ onLifecycleRef,
552
+ onErrorRef
553
+ }
554
+ ) })
555
+ ] }) })
556
+ }
557
+ )
291
558
  }
292
559
  ) }) });
293
560
  }
@@ -301,15 +568,21 @@ function ClaimBody({
301
568
  defaultTxHash,
302
569
  defaultRefundDestination,
303
570
  onClose,
571
+ onBack,
304
572
  operationEpochRef,
305
573
  isOpenRef,
574
+ lastSubmitFailureRef,
306
575
  onLifecycleRef,
307
- onEventRef,
308
576
  onErrorRef
309
577
  }) {
578
+ const analytics = useClaimAnalyticsController();
579
+ useEffect(() => {
580
+ analytics.openSession();
581
+ analytics.openStep(analyticsStepForClaimStep(state.step));
582
+ }, [analytics, state.step]);
310
583
  switch (state.step) {
311
584
  case "lookup":
312
- return /* @__PURE__ */ jsx2(
585
+ return /* @__PURE__ */ jsx3(
313
586
  LookupStep,
314
587
  {
315
588
  service,
@@ -319,24 +592,29 @@ function ClaimBody({
319
592
  onResults: (deposits) => {
320
593
  const eligible = deposits.filter(isEligible);
321
594
  const autoSelect = deposits.length === 1 && eligible.length === 1 ? deposits[0] : null;
595
+ analytics.completeStep("lookup");
596
+ if (autoSelect) reportSelection(autoSelect, true);
322
597
  dispatch({ type: "lookup/results", deposits, autoSelect });
323
598
  },
324
599
  onLifecycleRef,
325
- onEventRef,
326
600
  onErrorRef
327
601
  }
328
602
  );
329
603
  case "select":
330
- return /* @__PURE__ */ jsx2(
604
+ return /* @__PURE__ */ jsx3(
331
605
  SelectStep,
332
606
  {
333
607
  deposits: state.deposits,
334
- onSelect: (deposit) => dispatch({ type: "select", deposit })
608
+ onSelect: (deposit) => {
609
+ analytics.completeStep("select");
610
+ reportSelection(deposit, false);
611
+ dispatch({ type: "select", deposit });
612
+ }
335
613
  }
336
614
  );
337
615
  case "review":
338
616
  case "processing":
339
- return /* @__PURE__ */ jsx2(
617
+ return /* @__PURE__ */ jsx3(
340
618
  ReviewStep,
341
619
  {
342
620
  deposit: state.selected,
@@ -346,27 +624,51 @@ function ClaimBody({
346
624
  }
347
625
  );
348
626
  case "complete":
349
- return /* @__PURE__ */ jsx2(ResultStep, { result: state.result, onClose, onReset: () => dispatch({ type: "reset" }) });
627
+ return /* @__PURE__ */ jsx3(
628
+ ResultStep,
629
+ {
630
+ result: state.result,
631
+ onClose: () => onClose("success_done"),
632
+ onReset: () => {
633
+ analytics.resetAttempt();
634
+ dispatch({ type: "reset" });
635
+ }
636
+ }
637
+ );
350
638
  case "failed":
351
- return /* @__PURE__ */ jsx2(
639
+ return /* @__PURE__ */ jsx3(
352
640
  ResultStep,
353
641
  {
354
642
  error: state.error,
355
- onClose,
356
- onRetry: state.retryable ? () => dispatch({ type: "back" }) : void 0
643
+ onClose: () => onClose("failure_cancel"),
644
+ onRetry: state.retryable ? onBack : void 0
357
645
  }
358
646
  );
359
647
  default:
360
648
  return null;
361
649
  }
650
+ function reportSelection(deposit, autoSelected) {
651
+ if (!deposit.id) return;
652
+ analytics.selectDeposit({
653
+ depositId: deposit.id,
654
+ transactionHash: deposit.txHash ?? null,
655
+ autoSelected
656
+ });
657
+ }
362
658
  async function submitRecoveryRequest(destination) {
363
659
  const deposit = state.selected;
364
660
  if (!deposit) return;
365
661
  const sourceChainId = evmChainIdFromCaip2(deposit.chain);
366
662
  if (sourceChainId == null || !deposit.id || !deposit.recipient || !deposit.depositAddress || !deposit.token || !deposit.chain) {
663
+ analytics.failure("recovery_deposit_data_incomplete", false, "review");
664
+ analytics.outcome("failed");
367
665
  dispatch({
368
666
  type: "failed",
369
- error: "Deposit is missing data required to recover it."
667
+ error: "Deposit is missing data required to recover it.",
668
+ // The missing fields come off the backend row already in memory, so a
669
+ // retry re-reads nothing and fails identically. Offering one would be a
670
+ // dead end, and would also contradict the `retryable` reported above.
671
+ retryable: false
370
672
  });
371
673
  return;
372
674
  }
@@ -375,11 +677,9 @@ function ClaimBody({
375
677
  const token = deposit.token;
376
678
  const operationEpoch = operationEpochRef.current;
377
679
  const isCurrentOperation = () => isOpenRef.current && operationEpochRef.current === operationEpoch;
680
+ analytics.completeStep("review");
681
+ analytics.openStep("submit");
378
682
  dispatch({ type: "submit" });
379
- onEventRef.current?.({
380
- type: "claim_modal_refund_click",
381
- chain: `eip155:${sourceChainId}`
382
- });
383
683
  onLifecycleRef.current?.({
384
684
  type: "refund_requested",
385
685
  account,
@@ -413,10 +713,16 @@ function ClaimBody({
413
713
  destination: paidTo,
414
714
  chain: sourceChainId
415
715
  });
416
- onEventRef.current?.({
417
- type: "claim_modal_complete",
418
- chain: `eip155:${sourceChainId}`
419
- });
716
+ analytics.handoff(
717
+ {
718
+ depositId: deposit.id,
719
+ transactionHash: deposit.txHash ?? null,
720
+ refundTransactionHash: result.transactionHash
721
+ },
722
+ "submit"
723
+ );
724
+ analytics.completeStep("submit");
725
+ analytics.outcome("completed");
420
726
  onLifecycleRef.current?.({
421
727
  type: "complete",
422
728
  txHash: result.transactionHash,
@@ -433,16 +739,16 @@ function ClaimBody({
433
739
  const message = recoveryError?.message ?? formatUserError(
434
740
  err instanceof Error ? err.message : "Recovery failed"
435
741
  );
742
+ const retryable = recoveryError?.retryable ?? true;
436
743
  dispatch({
437
744
  type: "failed",
438
745
  error: message,
439
- retryable: recoveryError?.retryable ?? true
440
- });
441
- onEventRef.current?.({
442
- type: "claim_modal_failed",
443
- chain: `eip155:${sourceChainId}`,
444
- status
746
+ retryable
445
747
  });
748
+ const reason = claimFailureReasonForError(err);
749
+ lastSubmitFailureRef.current = { reason, retryable };
750
+ analytics.failure(reason, retryable, "submit");
751
+ analytics.outcome("failed");
446
752
  onLifecycleRef.current?.({ type: "failed", status, error: message });
447
753
  onErrorRef.current?.({ message, code: "CLAIM_FAILED" });
448
754
  }
@@ -455,19 +761,25 @@ function LookupStep({
455
761
  isOpenRef,
456
762
  onResults,
457
763
  onLifecycleRef,
458
- onEventRef,
459
764
  onErrorRef
460
765
  }) {
766
+ const analytics = useClaimAnalyticsController();
461
767
  const [txHash, setTxHash] = useState(defaultTxHash ?? "");
462
768
  const [loading, setLoading] = useState(false);
463
769
  const [error, setError] = useState(null);
464
770
  const trimmed = txHash.trim();
465
771
  const validTxHash = /^0x[a-fA-F0-9]{64}$/.test(trimmed);
772
+ useEffect(() => {
773
+ if (trimmed.length > 0 && !validTxHash) {
774
+ analytics.friction("lookup_transaction_hash_invalid", "lookup");
775
+ }
776
+ }, [analytics, trimmed, validTxHash]);
466
777
  const handleLookup = useCallback(async () => {
467
778
  const operationEpoch = operationEpochRef.current;
468
779
  const isCurrentOperation = () => isOpenRef.current && operationEpochRef.current === operationEpoch;
469
780
  setError(null);
470
781
  setLoading(true);
782
+ analytics.rearmStep("lookup");
471
783
  try {
472
784
  const deposits = await service.lookupRecoverableDeposits({
473
785
  txHash: trimmed
@@ -478,15 +790,15 @@ function LookupStep({
478
790
  txHash: trimmed,
479
791
  matches: deposits.length
480
792
  });
481
- onEventRef.current?.({
482
- type: "claim_modal_lookup",
483
- matches: deposits.length
484
- });
793
+ const eligible = deposits.filter(isEligible);
794
+ analytics.lookupResult(deposits.length, eligible.length);
485
795
  if (deposits.length === 0) {
796
+ analytics.friction("lookup_no_deposits_found", "lookup");
486
797
  setError("No deposits found for this transaction.");
487
798
  return;
488
799
  }
489
- if (!deposits.some(isEligible)) {
800
+ if (eligible.length === 0) {
801
+ analytics.friction("lookup_no_eligible_deposits", "lookup");
490
802
  setError(
491
803
  "This transaction has no failed or rejected deposits available to recover."
492
804
  );
@@ -498,32 +810,33 @@ function LookupStep({
498
810
  const message = formatUserError(
499
811
  err instanceof Error ? err.message : "Lookup failed"
500
812
  );
813
+ analytics.failure("lookup_failed", true, "lookup");
501
814
  setError(message);
502
815
  onErrorRef.current?.({ message, code: "CLAIM_LOOKUP_FAILED" });
503
816
  } finally {
504
817
  if (isCurrentOperation()) setLoading(false);
505
818
  }
506
819
  }, [
820
+ analytics,
507
821
  trimmed,
508
822
  service,
509
823
  operationEpochRef,
510
824
  isOpenRef,
511
825
  onResults,
512
826
  onLifecycleRef,
513
- onEventRef,
514
827
  onErrorRef
515
828
  ]);
516
829
  return /* @__PURE__ */ jsxs2("div", { className: "rs-screen", children: [
517
830
  /* @__PURE__ */ jsxs2("div", { className: "rs-screen-body rs-screen-body--gap-32", children: [
518
- /* @__PURE__ */ jsx2(
831
+ /* @__PURE__ */ jsx3(
519
832
  BodyHeader,
520
833
  {
521
- icon: /* @__PURE__ */ jsx2(HandCoinsIcon, {}),
834
+ icon: /* @__PURE__ */ jsx3(HandCoinsIcon, {}),
522
835
  title: "Claim a deposit",
523
836
  subtitle: "Get funds from a failed deposit sent back to you on the source chain."
524
837
  }
525
838
  ),
526
- /* @__PURE__ */ jsx2(
839
+ /* @__PURE__ */ jsx3(
527
840
  Input,
528
841
  {
529
842
  label: "Transaction hash",
@@ -533,8 +846,8 @@ function LookupStep({
533
846
  error: trimmed.length > 0 && !validTxHash ? "Enter a valid transaction hash" : void 0
534
847
  }
535
848
  ),
536
- error && /* @__PURE__ */ jsx2(Callout, { variant: "error", children: error }),
537
- /* @__PURE__ */ jsx2(
849
+ error && /* @__PURE__ */ jsx3(Callout, { variant: "error", children: error }),
850
+ /* @__PURE__ */ jsx3(
538
851
  Button,
539
852
  {
540
853
  onClick: handleLookup,
@@ -546,7 +859,7 @@ function LookupStep({
546
859
  }
547
860
  )
548
861
  ] }),
549
- /* @__PURE__ */ jsx2(PoweredBy, {})
862
+ /* @__PURE__ */ jsx3(PoweredBy, {})
550
863
  ] });
551
864
  }
552
865
  function SelectStep({
@@ -555,18 +868,18 @@ function SelectStep({
555
868
  }) {
556
869
  return /* @__PURE__ */ jsxs2("div", { className: "rs-screen", children: [
557
870
  /* @__PURE__ */ jsxs2("div", { className: "rs-screen-body rs-screen-body--gap-32", children: [
558
- /* @__PURE__ */ jsx2(
871
+ /* @__PURE__ */ jsx3(
559
872
  BodyHeader,
560
873
  {
561
- icon: /* @__PURE__ */ jsx2(CoinsIcon, {}),
874
+ icon: /* @__PURE__ */ jsx3(CoinsIcon, {}),
562
875
  title: "Select a deposit",
563
876
  subtitle: "This transaction produced multiple deposits. Choose the one to claim."
564
877
  }
565
878
  ),
566
- /* @__PURE__ */ jsx2("div", { className: "rs-list", children: deposits.map((row) => {
879
+ /* @__PURE__ */ jsx3("div", { className: "rs-list", children: deposits.map((row) => {
567
880
  const { symbol, amount, chainName } = describeDeposit(row);
568
881
  const eligible = isEligible(row);
569
- return /* @__PURE__ */ jsx2(
882
+ return /* @__PURE__ */ jsx3(
570
883
  ListRow,
571
884
  {
572
885
  leadingMedia: tokenIconUrl(symbol),
@@ -580,7 +893,7 @@ function SelectStep({
580
893
  );
581
894
  }) })
582
895
  ] }),
583
- /* @__PURE__ */ jsx2(PoweredBy, {})
896
+ /* @__PURE__ */ jsx3(PoweredBy, {})
584
897
  ] });
585
898
  }
586
899
  function ReviewStep({
@@ -589,41 +902,47 @@ function ReviewStep({
589
902
  defaultRefundDestination,
590
903
  onSubmit
591
904
  }) {
905
+ const analytics = useClaimAnalyticsController();
592
906
  const { chainId, symbol, amount, chainName, chainIcon } = describeDeposit(deposit);
593
907
  const explorerTxUrl = useExplorerTxUrl();
594
908
  const [destination, setDestination] = useState(defaultRefundDestination ?? "");
595
909
  const destinationValid = isAddress(destination);
910
+ useEffect(() => {
911
+ if (destination.length > 0 && !destinationValid) {
912
+ analytics.friction("refund_destination_invalid", "review");
913
+ }
914
+ }, [analytics, destination, destinationValid]);
596
915
  return /* @__PURE__ */ jsxs2("div", { className: "rs-screen", children: [
597
916
  /* @__PURE__ */ jsxs2("div", { className: "rs-screen-body rs-screen-body--gap-32", children: [
598
- /* @__PURE__ */ jsx2(
917
+ /* @__PURE__ */ jsx3(
599
918
  BodyHeader,
600
919
  {
601
- icon: /* @__PURE__ */ jsx2(HandCoinsIcon, {}),
920
+ icon: /* @__PURE__ */ jsx3(HandCoinsIcon, {}),
602
921
  title: "Review refund",
603
922
  subtitle: "This returns the funds on the source chain. It does not retry the original bridge."
604
923
  }
605
924
  ),
606
925
  /* @__PURE__ */ jsxs2("div", { className: "rs-review-details", children: [
607
926
  /* @__PURE__ */ jsxs2("div", { className: "rs-review-detail-row", children: [
608
- /* @__PURE__ */ jsx2("span", { children: "Amount" }),
927
+ /* @__PURE__ */ jsx3("span", { children: "Amount" }),
609
928
  /* @__PURE__ */ jsxs2("span", { className: "rs-review-detail-value", children: [
610
929
  /* @__PURE__ */ jsxs2("span", { children: [
611
930
  amount,
612
931
  " ",
613
932
  symbol
614
933
  ] }),
615
- /* @__PURE__ */ jsx2("span", { className: "rs-review-detail-icon", children: /* @__PURE__ */ jsx2(TokenIcon, { symbol }) })
934
+ /* @__PURE__ */ jsx3("span", { className: "rs-review-detail-icon", children: /* @__PURE__ */ jsx3(TokenIcon, { symbol }) })
616
935
  ] })
617
936
  ] }),
618
937
  /* @__PURE__ */ jsxs2("div", { className: "rs-review-detail-row", children: [
619
- /* @__PURE__ */ jsx2("span", { children: "Source chain" }),
938
+ /* @__PURE__ */ jsx3("span", { children: "Source chain" }),
620
939
  /* @__PURE__ */ jsxs2("span", { className: "rs-review-detail-value", children: [
621
- /* @__PURE__ */ jsx2("span", { children: chainName }),
622
- chainIcon && /* @__PURE__ */ jsx2("span", { className: "rs-review-detail-icon", children: /* @__PURE__ */ jsx2(CdnIcon, { src: chainIcon }) })
940
+ /* @__PURE__ */ jsx3("span", { children: chainName }),
941
+ chainIcon && /* @__PURE__ */ jsx3("span", { className: "rs-review-detail-icon", children: /* @__PURE__ */ jsx3(CdnIcon, { src: chainIcon }) })
623
942
  ] })
624
943
  ] }),
625
944
  /* @__PURE__ */ jsxs2("div", { className: "rs-review-detail-row", children: [
626
- /* @__PURE__ */ jsx2("span", { children: "Deposit transaction" }),
945
+ /* @__PURE__ */ jsx3("span", { children: "Deposit transaction" }),
627
946
  (() => {
628
947
  const url = deposit.txHash ? explorerTxUrl(chainId, deposit.txHash) : null;
629
948
  const short = deposit.txHash ? shortenHash(deposit.txHash) : "\u2014";
@@ -636,14 +955,14 @@ function ReviewStep({
636
955
  rel: "noreferrer",
637
956
  children: [
638
957
  short,
639
- /* @__PURE__ */ jsx2(ExternalLinkIcon, {})
958
+ /* @__PURE__ */ jsx3(ExternalLinkIcon, {})
640
959
  ]
641
960
  }
642
- ) : /* @__PURE__ */ jsx2("span", { className: "rs-review-detail-value", children: short });
961
+ ) : /* @__PURE__ */ jsx3("span", { className: "rs-review-detail-value", children: short });
643
962
  })()
644
963
  ] })
645
964
  ] }),
646
- /* @__PURE__ */ jsx2(
965
+ /* @__PURE__ */ jsx3(
647
966
  Input,
648
967
  {
649
968
  label: "Send to",
@@ -653,7 +972,7 @@ function ReviewStep({
653
972
  error: destination.length > 0 && !destinationValid ? "Enter a valid address" : void 0
654
973
  }
655
974
  ),
656
- /* @__PURE__ */ jsx2(
975
+ /* @__PURE__ */ jsx3(
657
976
  Button,
658
977
  {
659
978
  onClick: () => destinationValid && onSubmit(destination),
@@ -665,7 +984,7 @@ function ReviewStep({
665
984
  }
666
985
  )
667
986
  ] }),
668
- /* @__PURE__ */ jsx2(PoweredBy, {})
987
+ /* @__PURE__ */ jsx3(PoweredBy, {})
669
988
  ] });
670
989
  }
671
990
  function ResultStep({
@@ -679,10 +998,10 @@ function ResultStep({
679
998
  const explorerTxUrl = useExplorerTxUrl();
680
999
  return /* @__PURE__ */ jsxs2("div", { className: "rs-screen", children: [
681
1000
  /* @__PURE__ */ jsxs2("div", { className: "rs-screen-body rs-screen-body--gap-32", children: [
682
- /* @__PURE__ */ jsx2(
1001
+ /* @__PURE__ */ jsx3(
683
1002
  BodyHeader,
684
1003
  {
685
- icon: success ? /* @__PURE__ */ jsx2(CheckIcon, {}) : /* @__PURE__ */ jsx2(AlertTriangleIcon, {}),
1004
+ icon: success ? /* @__PURE__ */ jsx3(CheckIcon, {}) : /* @__PURE__ */ jsx3(AlertTriangleIcon, {}),
686
1005
  variant: success ? "success" : "error",
687
1006
  title: success ? "Refund submitted" : "Refund failed",
688
1007
  subtitle: success ? "Your funds are being returned on the source chain." : void 0
@@ -690,22 +1009,22 @@ function ResultStep({
690
1009
  ),
691
1010
  success ? /* @__PURE__ */ jsxs2("div", { className: "rs-review-details", children: [
692
1011
  /* @__PURE__ */ jsxs2("div", { className: "rs-review-detail-row", children: [
693
- /* @__PURE__ */ jsx2("span", { children: "Amount" }),
1012
+ /* @__PURE__ */ jsx3("span", { children: "Amount" }),
694
1013
  /* @__PURE__ */ jsxs2("span", { className: "rs-review-detail-value", children: [
695
1014
  /* @__PURE__ */ jsxs2("span", { children: [
696
1015
  result.amountDisplay,
697
1016
  " ",
698
1017
  result.symbol
699
1018
  ] }),
700
- /* @__PURE__ */ jsx2("span", { className: "rs-review-detail-icon", children: /* @__PURE__ */ jsx2(TokenIcon, { symbol: result.symbol }) })
1019
+ /* @__PURE__ */ jsx3("span", { className: "rs-review-detail-icon", children: /* @__PURE__ */ jsx3(TokenIcon, { symbol: result.symbol }) })
701
1020
  ] })
702
1021
  ] }),
703
1022
  /* @__PURE__ */ jsxs2("div", { className: "rs-review-detail-row", children: [
704
- /* @__PURE__ */ jsx2("span", { children: "Sent to" }),
705
- /* @__PURE__ */ jsx2("span", { className: "rs-review-detail-value", children: shortenAddress(result.destination) })
1023
+ /* @__PURE__ */ jsx3("span", { children: "Sent to" }),
1024
+ /* @__PURE__ */ jsx3("span", { className: "rs-review-detail-value", children: shortenAddress(result.destination) })
706
1025
  ] }),
707
1026
  /* @__PURE__ */ jsxs2("div", { className: "rs-review-detail-row", children: [
708
- /* @__PURE__ */ jsx2("span", { children: "Transaction" }),
1027
+ /* @__PURE__ */ jsx3("span", { children: "Transaction" }),
709
1028
  (() => {
710
1029
  const url = explorerTxUrl(result.chain, result.txHash);
711
1030
  const short = shortenHash(result.txHash);
@@ -718,25 +1037,26 @@ function ResultStep({
718
1037
  rel: "noreferrer",
719
1038
  children: [
720
1039
  short,
721
- /* @__PURE__ */ jsx2(ExternalLinkIcon, {})
1040
+ /* @__PURE__ */ jsx3(ExternalLinkIcon, {})
722
1041
  ]
723
1042
  }
724
- ) : /* @__PURE__ */ jsx2("span", { className: "rs-review-detail-value", children: short });
1043
+ ) : /* @__PURE__ */ jsx3("span", { className: "rs-review-detail-value", children: short });
725
1044
  })()
726
1045
  ] })
727
- ] }) : /* @__PURE__ */ jsx2(Callout, { variant: "error", children: error }),
728
- /* @__PURE__ */ jsx2("div", { className: "rs-screen-button-row", children: success ? /* @__PURE__ */ jsxs2(Fragment, { children: [
729
- onReset && /* @__PURE__ */ jsx2(Button, { variant: "outline", onClick: onReset, fullWidth: true, children: "Claim another deposit" }),
730
- /* @__PURE__ */ jsx2(Button, { onClick: onClose, fullWidth: true, children: "Done" })
1046
+ ] }) : /* @__PURE__ */ jsx3(Callout, { variant: "error", children: error }),
1047
+ /* @__PURE__ */ jsx3("div", { className: "rs-screen-button-row", children: success ? /* @__PURE__ */ jsxs2(Fragment, { children: [
1048
+ onReset && /* @__PURE__ */ jsx3(Button, { variant: "outline", onClick: onReset, fullWidth: true, children: "Claim another deposit" }),
1049
+ /* @__PURE__ */ jsx3(Button, { onClick: onClose, fullWidth: true, children: "Done" })
731
1050
  ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
732
- /* @__PURE__ */ jsx2(Button, { variant: "outline", onClick: onClose, fullWidth: true, children: "Cancel" }),
733
- onRetry && /* @__PURE__ */ jsx2(Button, { onClick: onRetry, fullWidth: true, children: "Try again" })
1051
+ /* @__PURE__ */ jsx3(Button, { variant: "outline", onClick: onClose, fullWidth: true, children: "Cancel" }),
1052
+ onRetry && /* @__PURE__ */ jsx3(Button, { onClick: onRetry, fullWidth: true, children: "Try again" })
734
1053
  ] }) })
735
1054
  ] }),
736
- /* @__PURE__ */ jsx2(PoweredBy, {})
1055
+ /* @__PURE__ */ jsx3(PoweredBy, {})
737
1056
  ] });
738
1057
  }
739
1058
 
740
1059
  export {
1060
+ buildClaimAnalyticsSessionProperties,
741
1061
  ClaimModal
742
1062
  };