Skip to content

Comments

Transaction Status for Buy flow on feat/PRO-3681/transaction-status-pulse#411

Merged
RanaBug merged 14 commits intostagingfrom
feat/PRO-3681/transaction-status-pulse
Oct 1, 2025
Merged

Transaction Status for Buy flow on feat/PRO-3681/transaction-status-pulse#411
RanaBug merged 14 commits intostagingfrom
feat/PRO-3681/transaction-status-pulse

Conversation

@RanaBug
Copy link
Collaborator

@RanaBug RanaBug commented Sep 25, 2025

Description

  • FE implementation of Transaction Status for Buy flow

How Has This Been Tested?

  • Unit tests and Manual testing

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Summary by CodeRabbit

  • New Features

    • Unified live transaction status UI (compact + expanded) with multi-step progress, timestamps, multiple hashes, explorer links, copy-to-clipboard, animated status icons, and keyboard/escape & outside-click accessibility.
  • Changes

    • Previews now invoke the status flow; Buy flow auto-calculates token amounts from USD/paying tokens and surfaces real-time polling updates and background reopen behavior.
  • Removed

    • Old sell-only status components replaced by the unified transaction status experience.
  • Style

    • Simplified technical-details error styling.
  • Tests

    • Extensive unit tests for status UI, details, info, progress steps, hooks, and utilities.

@RanaBug RanaBug requested a review from IAmKio September 25, 2025 16:02
@RanaBug RanaBug self-assigned this Sep 25, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 25, 2025

Walkthrough

Adds a unified client-side transaction status system: HomeScreen now manages polling for Buy (bid + resource lock) and Sell (user-op) flows, maps external statuses to a shared TransactionStatus UI, and introduces new transaction components, hooks, types, utils, tests, and a CloseButton.

Changes

Cohort / File(s) Summary
HomeScreen polling & wiring
src/apps/pulse/components/App/HomeScreen.tsx
Adds buy/sell polling lifecycle, status mappings/types, expanded transaction state (hashes, chain IDs, timestamps, resource-lock fields, flags), token/amount auto-calculation, and show/stop/close transaction status APIs.
Buy preview trigger
src/apps/pulse/components/Buy/PreviewBuy.tsx, src/apps/pulse/components/Buy/tests/PreviewBuy.test.tsx
Replaces internal IntentTracker with a new showTransactionStatus callback prop invoked on shortlist success; tests updated to assert the callback.
Sell previews & tests
src/apps/pulse/components/Sell/tests/PreviewSell.test.tsx, src/apps/pulse/components/Sell/tests/SellButton.test.tsx
Injects showTransactionStatus into mocks; expands SellOffer test shape with minimumReceive and slippageTolerance.
Removed Sell-only components
src/apps/pulse/components/Sell/TransactionDetails.tsx (deleted), src/apps/pulse/components/Sell/TransactionInfo.tsx (deleted), src/apps/pulse/components/Sell/TransactionStatus.tsx (deleted)
Deletes legacy Sell-specific transaction UI and exported types.
Unified Transaction UI (added)
src/apps/pulse/components/Transaction/TransactionStatus.tsx, .../TransactionDetails.tsx, .../TransactionInfo.tsx, .../TransactionErrorBox.tsx, .../TransactionStatusContainer.tsx, .../TransactionStatusButton.tsx, .../TransactionStatusIcon.tsx
Adds shared transaction UI primitives (compact + details), animated icons/buttons, multi-chain links, copy-to-clipboard, technical error details, and props for resource-lock/completed hashes and timestamps.
Progress step & visuals
src/apps/pulse/components/Transaction/ProgressStep.tsx, src/apps/pulse/components/Transaction/tests/ProgressStep.test.tsx
Adds ResourceLock and inactive states, adjusts spinner/icon/timestamp handling and styling; includes tests.
Transaction tests
src/apps/pulse/components/Transaction/tests/*
Adds comprehensive tests for TransactionDetails, TransactionInfo, TransactionErrorBox, TransactionStatus, TransactionStatusContainer covering rendering, interactions, multi-chain, and error cases.
Hooks & hook tests
src/apps/pulse/hooks/useClickOutside.ts, src/apps/pulse/hooks/useKeyboardNavigation.ts, src/apps/pulse/hooks/useTechnicalDetails.ts, src/apps/pulse/hooks/useTransactionStatus.ts, src/apps/pulse/hooks/tests/*
New hooks for outside-click, keyboard navigation, technical-details assembly, transaction-status helpers; corresponding unit tests added.
Types, utils & constants
src/apps/pulse/types/types.ts, src/apps/pulse/utils/utils.tsx, src/apps/pulse/utils/tests/utils.test.tsx, src/apps/pulse/constants/constants.ts
Adds transaction-related TypeScript types, constants, utility functions (hash truncation, mappings, sanitization, timestamp formatting, step-status logic) and tests.
Close button utility
src/apps/pulse/components/Misc/CloseButton.tsx
Adds accessible CloseButton component with Escape key handling and click-to-close behavior.
Minor typing fix
src/apps/pillarx-app/components/PillarXLogo/PillarXLogo.tsx
Changes timer type to `ReturnType

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Home as HomeScreen
  participant UI as TransactionStatus (UI)
  participant API as Backend

  rect rgba(240,248,255,1)
  note right of User: Buy flow (shortlist -> bidHash)
  User->>Home: Confirm Buy (shortlist)
  Home-->>User: bidHash
  Home->>UI: showTransactionStatus(bidHash)
  loop Poll Bid & Resource Lock
    Home->>API: getBidStatus(bidHash)
    API-->>Home: BidStatus
    Home->>API: getResourceLockInfo(bidHash)
    API-->>Home: ResourceLockStatus
    Home->>Home: map statuses -> TransactionStatusState
    Home->>UI: update props (status, hashes, timestamps)
  end
  Home-->>UI: Final status (Complete/Failed)
  UI->>UI: auto-open details or user opens
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Home as HomeScreen
  participant UI as TransactionStatus (UI)
  participant API as Backend

  rect rgba(240,248,255,1)
  note right of User: Sell flow (confirm -> userOpHash)
  User->>Home: Confirm Sell
  Home-->>User: userOpHash
  Home->>UI: showTransactionStatus(userOpHash)
  loop Poll UserOp
    Home->>API: getUserOperationStatus(userOpHash)
    API-->>Home: UserOpStatus (+txHash?)
    Home->>Home: map status -> TransactionStatusState
    Home->>UI: update props (status, txHash, timestamps)
  end
  Home-->>UI: Final status (Complete/Failed)
  UI->>UI: auto-open details or user opens
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • IAmKio
  • nikhilkumar1612

Poem

Thump-thump, I hop through fields of code tonight,
Polling bids and hashes till the statuses light.
I tuck errors safe and make the UI sing,
Progress bars hop and the tests take wing.
Merge me a carrot — a green check delights! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title accurately indicates the introduction of transaction status for the Buy flow but includes the branch name and ticket reference which adds noise and does not belong in a concise PR title. It also repeats information that is already clear from the branch metadata rather than focusing solely on the change itself. Please remove the branch name and ticket reference from the title and shorten it to something like “Add transaction status UI for Buy flow” to improve clarity and conciseness.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed The pull request description follows the repository’s template by providing a summary of changes, testing details, and type of change, with the screenshots section correctly left empty since no visuals are included. It clearly labels the change as a new feature and describes the testing approach, satisfying the required template sections.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/PRO-3681/transaction-status-pulse

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8a1cbc and 9a07b8b.

📒 Files selected for processing (1)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 25, 2025 16:04 Inactive
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Sep 25, 2025

Deploying x with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9a07b8b
Status: ✅  Deploy successful!
Preview URL: https://a7308e00.x-e62.pages.dev
Branch Preview URL: https://feat-pro-3681-transaction-st.x-e62.pages.dev

View logs

@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 26, 2025 15:30 Inactive
@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 29, 2025 10:59 Inactive
@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 29, 2025 14:23 Inactive
@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 29, 2025 16:19 Inactive
@RanaBug RanaBug marked this pull request as ready for review September 29, 2025 16:19
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/apps/pulse/components/Buy/PreviewBuy.tsx (1)

171-179: Crash risk: unsafe access to bids[0].bidHash and optional intentSdk.

If bids is empty or intentSdk is undefined, this throws or silently skips shortlist but still calls showTransactionStatus. Guard and derive bidHash first.

   try {
-      await intentSdk?.shortlistBid(
-        expressIntentResponse?.intentHash!,
-        expressIntentResponse?.bids[0].bidHash!
-      );
-      showTransactionStatus(expressIntentResponse?.bids[0].bidHash!);
+      const bidHash = expressIntentResponse.bids?.[0]?.bidHash;
+      const intentHash = expressIntentResponse.intentHash;
+      if (!intentSdk || !bidHash || !intentHash) {
+        // Abort gracefully
+        setIsWaitingForSignature(false);
+        setIsLoading(false);
+        return;
+      }
+      await intentSdk.shortlistBid(intentHash, bidHash);
+      showTransactionStatus(bidHash);
src/apps/pulse/components/Transaction/TransactionErrorBox.tsx (1)

2-2: Use named import for CopyToClipboard throughout
Align every import to import { CopyToClipboard } from 'react-copy-to-clipboard', and update any test mocks (replace default: with CopyToClipboard:) to match. You can locate remaining occurrences with:

rg -n "react-copy-to-clipboard" -g '*.ts' -g '*.tsx'
src/apps/pulse/components/Buy/tests/PreviewBuy.test.tsx (1)

193-221: Contradictory assertions in shortlist-failure test; wait for removal instead of immediate absence

You first await the tracker text to appear, then immediately assert it’s not present. This will be flaky at best. Wait for the element to be removed and also assert that showTransactionStatus was not called.

Apply this diff:

-    it('handles shortlist bid failure', async () => {
+    it('handles shortlist bid failure', async () => {
       (useIntentSdk as any).mockReturnValue({
         intentSdk: {
           shortlistBid: vi
             .fn()
             .mockRejectedValue(new Error('Shortlist failed')),
         },
       });

       render(<PreviewBuy {...mockProps} />);

       const confirmButton = screen.getByText('Confirm');
       fireEvent.click(confirmButton);

-      await waitFor(() => {
-        expect(
-          screen.getByText(
-            'Please open your wallet and confirm the transaction.'
-          )
-        ).toBeInTheDocument();
-      });
-
-      // Should not show tracker on error
-      expect(
-        screen.queryByText(
-          'Please open your wallet and confirm the transaction.'
-        )
-      ).not.toBeInTheDocument();
+      // Should hide the tracker once the shortlist fails
+      // and must not trigger transaction status
+      await waitForElementToBeRemoved(() =>
+        screen.queryByText('Please open your wallet and confirm the transaction.')
+      );
+      expect(mockProps.showTransactionStatus).not.toHaveBeenCalled();
     });

Don’t forget to import the helper:

- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+ import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
🧹 Nitpick comments (39)
src/apps/pulse/components/Sell/tests/PreviewSell.test.tsx (1)

119-119: Add expectations for the transaction status callback.

showTransactionStatus is newly wired into the component but the tests never assert that it fires after a successful sell. If PreviewSell stops invoking this callback, the test suite would still pass, so we lose coverage on the key behaviour introduced in this PR. Please extend the “executes sell transaction” spec to verify the callback is called with the expected payload.

       await waitFor(() => {
         expect(
           screen.getByText(
             'Please open your wallet and confirm the transaction.'
           )
         ).toBeInTheDocument();
       });

       expect(mockExecuteSell).toHaveBeenCalledWith(
         mockToken,
         '10.5',
         undefined
       );
+
+      expect(mockProps.showTransactionStatus).toHaveBeenCalledWith(
+        '0xTransactionHash123456789'
+      );
src/apps/pulse/components/Transaction/TransactionInfo.tsx (3)

50-61: Show “copied” per row instead of globally

A single isCopied flag will flip both copy icons in Buy mode. Track the copied hash to scope feedback per-row.

-  const [isCopied, setIsCopied] = useState(false);
+  const [copiedHash, setCopiedHash] = useState<string | null>(null);
...
-  useEffect(() => {
-    if (isCopied) {
-      const timer = setTimeout(() => setIsCopied(false), 2000);
+  useEffect(() => {
+    if (copiedHash) {
+      const timer = setTimeout(() => setCopiedHash(null), 2000);
       return () => clearTimeout(timer);
     }
     return undefined;
-  }, [isCopied]);
+  }, [copiedHash]);
-          {showCopy && isValidHash && (
+          {showCopy && isValidHash && (
             <CopyToClipboard
-              text={transactionHash}
-              onCopy={() => setIsCopied(true)}
+              text={transactionHash!}
+              onCopy={() => setCopiedHash(transactionHash!)}
             >
               <div className="flex items-center ml-1 cursor-pointer">
-                {isCopied ? (
+                {copiedHash === transactionHash ? (
                   <MdCheck className="w-[10px] h-3 text-white" />
                 ) : (
                   <img
                     src={CopyIcon}
                     alt="copy-address-icon"
                     className="w-[10px] h-3"
                   />
                 )}
               </div>
             </CopyToClipboard>
           )}

Also applies to: 140-157, 171-181, 182-192, 195-205


171-181: Optional: add aria-labels for copy buttons

Improve a11y by labeling the copy actions.

-          {detailsEntry(
+          {detailsEntry(
             'Resource Lock',
             ...
           )}
...
-          {detailsEntry(
+          {detailsEntry(
             'Completed',
             ...
           )}

Within detailsEntry’s CopyToClipboard wrapper:

-              <div className="flex items-center ml-1 cursor-pointer">
+              <div className="flex items-center ml-1 cursor-pointer" aria-label={`Copy ${lhs} hash`}>

Also applies to: 182-192


15-15: Heads-up: Arbitrum explorer URL uses http in getBlockScan

The referenced helper returns 'http://arbiscan.io' for chainId 42161. Prefer https. This is in src/utils/blockchain.ts, not this file, but affects these links. Recommend fixing there.

src/apps/pulse/components/App/HomeScreen.tsx (3)

228-235: Clear pending failure timeout when stopping polling

Prevent stray state updates after closing by clearing the scheduled timeout.

-  const stopTransactionPolling = useCallback(() => {
+  const stopTransactionPolling = useCallback(() => {
     setIsPollingActive(false);
     setIsBackgroundPolling(false);
     setShouldAutoReopen(false);
+    if (failureTimeoutId) {
+      clearTimeout(failureTimeoutId);
+      setFailureTimeoutId(null);
+    }
     setTransactionData(null);
-  }, []);
+  }, [failureTimeoutId]);

656-664: Route errors through delayed failure path

Directly setting “Transaction Failed” bypasses the 10s confirmation delay in updateStatusWithDelay. Use the same path for consistency and to avoid flapping on transient errors.

-      } catch (error) {
+      } catch (error) {
         console.error('Failed to get transaction status:', error);
-        setCurrentTransactionStatus('Transaction Failed');
-        setErrorDetails(
+        updateStatusWithDelay('Transaction Failed');
+        setErrorDetails(
           error instanceof Error
             ? error.message
             : 'Failed to get transaction status'
         );
       }

351-355: Optional: track and clear all setTimeouts

updateStatusWithDelay schedules timeouts that aren’t cleared on unmount. Consider tracking them in a ref and clearing in a cleanup/useEffect to avoid stray updates.

Also applies to: 444-455

src/apps/pulse/components/Transaction/ProgressStep.tsx (2)

139-143: Verify Tailwind breakpoint “xs”

Tailwind’s default breakpoints start at sm. If “xs” isn’t configured, these classes won’t work. Validate tailwind.config.js or switch to sm.


33-46: Minor: consolidate pending step logic

The two pending cases return identical classes/icons. You can merge to reduce branches.

-    if (
-      status === 'pending' &&
-      (step === 'Pending' || step === 'ResourceLock')
-    ) {
-      return `${baseClasses} bg-[#8A77FF]`;
-    }
-    if (status === 'pending' && step === 'Completed') {
-      return `${baseClasses} bg-[#8A77FF]`;
-    }
+    if (status === 'pending') {
+      return `${baseClasses} bg-[#8A77FF]`;
+    }
-    if (
-      status === 'pending' &&
-      (step === 'Pending' || step === 'ResourceLock')
-    ) {
-      return (
-        <TailSpin color="#FFFFFF" height={10} width={10} strokeWidth={8} />
-      );
-    }
-    if (status === 'pending' && step === 'Completed') {
+    if (status === 'pending') {
       return (
         <TailSpin color="#FFFFFF" height={10} width={10} strokeWidth={8} />
       );
     }

Also applies to: 108-120

src/apps/pulse/components/Transaction/TransactionErrorBox.tsx (1)

43-55: Add aria semantics to the “Technical Details” toggle and panel.

Improve accessibility and testability by wiring aria-expanded/aria-controls on the button and an id/role on the panel.

-        <button
+        <button
           type="button"
           onClick={() => setIsExpanded(!isExpanded)}
-          className="px-2 py-1.5 rounded-[6px] bg-[#FF366C]/30 text-white text-[13px] font-normal flex items-center gap-2"
+          className="px-2 py-1.5 rounded-[6px] bg-[#FF366C]/30 text-white text-[13px] font-normal flex items-center gap-2"
+          aria-expanded={isExpanded}
+          aria-controls="technical-details"
         >
@@
-      {isExpanded && (
-        <div className="mt-2">
+      {isExpanded && (
+        <div id="technical-details" className="mt-2" role="region" aria-label="Technical details">
           <p className="text-white/70 text-[12px] font-mono whitespace-pre-wrap break-all">
             {technicalDetails}
           </p>
         </div>
       )}

Also applies to: 71-76

src/apps/pulse/components/Misc/CloseButton.tsx (1)

23-45: Minor a11y polish: declare keyboard shortcut and visible focus styles.

Add aria-keyshortcuts and a focus-visible ring to aid keyboard users; behavior unchanged.

     <button
-      className="flex items-center justify-center w-[36px] h-[34px] bg-[#1E1D24] rounded-[8px]"
+      className="flex items-center justify-center w-[36px] h-[34px] bg-[#1E1D24] rounded-[8px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
       onClick={onClose}
       type="button"
       aria-label="Close"
+      aria-keyshortcuts="Esc"
       data-testid="close-button"
     >
src/apps/pulse/components/Buy/PreviewBuy.tsx (4)

1-1: Prefer removing the global eslint-disable by fixing non-null assertions.

With the guard above, the non-null assertion rule can stay enabled.

-/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */

497-504: Surface actual gas fee if available (optional).

You already support gasFee in showTransactionStatus; consider plumbed-estimate here instead of a hardcoded “≈ $0.00”.


266-296: Auto-refresh considerations.

15s polling is fine, but add jitter/backoff on repeated failures to avoid synchronized bursts and rate limits.


42-51: Unify showTransactionStatus parameter naming
The first argument is declared as userOperationHash in Buy (but passed a bidHash) and txHash in Sell—rename it to a generic hash (or split into separate callbacks) for clarity.

src/apps/pulse/components/Transaction/tests/TransactionErrorBox.test.tsx (2)

65-71: Add assertions for aria-expanded and the details region (after a11y update).

Once aria attributes are added, assert toggle semantics to prevent regressions.

-  it('expands when technical details button is clicked', () => {
+  it('expands when technical details button is clicked', () => {
     render(<TransactionErrorBox {...baseProps} />);

-    const expandButton = screen.getByText('Technical Details');
+    const expandButton = screen.getByText('Technical Details');
     fireEvent.click(expandButton);

+    expect(expandButton).toHaveAttribute('aria-expanded', 'true');
+    const region = screen.getByRole('region', { name: /technical details/i });
+    expect(region).toHaveAttribute('id', 'technical-details');
     expect(screen.getByText(baseProps.technicalDetails)).toBeInTheDocument();
   });

Also applies to: 73-90


10-22: Mock both default and named exports for react-copy-to-clipboard (import consistency).

If you normalize imports, keep the mock compatible with both.

-vi.mock('react-copy-to-clipboard', () => ({
-  default: ({ children, onCopy, text }: any) => (
+vi.mock('react-copy-to-clipboard', () => ({
+  __esModule: true,
+  default: ({ children, onCopy, text }: any) => (
     <button type="button" onClick={() => onCopy(text)} data-testid="copy-button">
       {children}
     </button>
   ),
+  CopyToClipboard: ({ children, onCopy, text }: any) => (
+    <button type="button" onClick={() => onCopy(text)} data-testid="copy-button">
+      {children}
+    </button>
+  ),
 }));
src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1)

636-651: Good redaction test; consider covering buy-mode redaction too.

You validate API key redaction for sell; add the same assertion for buy flow to guard both paths.

src/apps/pulse/components/Buy/tests/PreviewBuy.test.tsx (3)

3-3: Use a type‑only import (or local type) to avoid runtime deep import coupling

The current import targets a deep CJS path and may leak into runtime if TS config changes. Make it explicit type‑only (safe), or inline a minimal local type.

Option A — keep path but mark type-only:

-import { ExpressIntentResponse } from '@etherspot/intent-sdk/dist/cjs/sdk/types/user-intent-types';
+import type { ExpressIntentResponse } from '@etherspot/intent-sdk/dist/cjs/sdk/types/user-intent-types';

And drop the redundant cast:

-} as any;
+};

Option B — avoid the import entirely:

type ExpressIntentResponse = { intentHash: string; bids: { bidHash: string }[] };

Also applies to: 48-51


169-189: Strengthen assertion around showTransactionStatus call

Add a precondition to ensure it isn’t called before user action, keeping the test intent crisp.

Apply this diff:

   it('shows transaction status after successful shortlist', async () => {
     render(<PreviewBuy {...mockProps} />);

+    expect(mockProps.showTransactionStatus).not.toHaveBeenCalled();
     const confirmButton = screen.getByText('Confirm');
     fireEvent.click(confirmButton);

78-81: Consider replacing the broad snapshot with targeted assertions

Snapshots for complex, highly‑styled components tend to churn. Prefer focused assertions on visible text/roles.

Based on learnings

src/apps/pulse/components/Transaction/tests/ProgressStep.test.tsx (2)

114-126: Line visibility isn’t actually asserted

Both tests only check the label text; they don’t verify the line’s presence/absence. Add a stable test target and assert it.

Test changes (assuming the component exposes data-testid="progress-line" when showLine is true and !isLast):

 it('shows line when not last step', () => {
   render(<ProgressStep {...baseProps} isLast={false} />);
-
-  expect(screen.getByText('Transaction Submitted')).toBeInTheDocument();
+  expect(screen.getByText('Transaction Submitted')).toBeInTheDocument();
+  expect(screen.getByTestId('progress-line')).toBeInTheDocument();
 });

 it('does not show line when is last step', () => {
   render(<ProgressStep {...baseProps} isLast />);
-
-  expect(screen.getByText('Transaction Submitted')).toBeInTheDocument();
+  expect(screen.getByText('Transaction Submitted')).toBeInTheDocument();
+  expect(screen.queryByTestId('progress-line')).not.toBeInTheDocument();
 });

If the component lacks such a hook, add one in ProgressStep.tsx where the vertical line is rendered:

<div data-testid="progress-line" /* existing props/styles */ />

29-34: Avoid snapshot for purely presentational component

Snapshots tend to be noisy here; rely on the behavior/state assertions you already have.

Based on learnings

src/apps/pulse/components/Transaction/tests/TransactionInfo.test.tsx (3)

94-126: Reduce brittleness: assert semantics, not Tailwind class names

Asserting specific classes like text-white/50, text-[#FFAB36], etc., couples tests to styling. Prefer role/text plus a semantic hook (e.g., data-status="pending|success|failed"), then assert on that.

Example:

expect(screen.getByTestId('status')).toHaveAttribute('data-status', 'pending');

Add data-testid="status" and data-status on the status element in TransactionInfo.tsx.


350-360: Short‐hash truncation expectation looks off

Expecting 0x123...x123 for a 5‑char hash is unusual. Either we shouldn’t truncate very short inputs or should display them verbatim.

If the intended behavior is “no truncation below threshold,” update the expectation:

- expect(screen.getByText('0x123...x123')).toBeInTheDocument();
+ expect(screen.getByText('0x123')).toBeInTheDocument();

88-92: Snapshot can be dropped in favor of explicit assertions

Given the breadth of behavior checks below, the snapshot adds little value and can churn on layout-only changes.

Based on learnings

src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (5)

167-171: Prefer userEvent over fireEvent for realistic interactions

Switch to @testing-library/user-event (click/keyboard) for closer-to-user behavior and fewer act()/timing pitfalls.

-import { fireEvent, render, screen } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';

-const viewStatusButton = screen.getByText('View Status');
-fireEvent.click(viewStatusButton);
+await userEvent.click(screen.getByText('View Status'));

-const successButton = screen.getByText('Success');
-fireEvent.click(successButton);
+await userEvent.click(screen.getByText('Success'));

Also applies to: 181-185, 192-196


217-229: Strengthen outside-click test to avoid false positives

Also assert that clicking inside the container does NOT close, to guard regressions in outside-click logic.

-// Click outside the modal
-fireEvent.mouseDown(document.body);
-expect(baseProps.closeTransactionStatus).toHaveBeenCalled();
+// Click outside the modal
+await userEvent.click(document.body);
+expect(baseProps.closeTransactionStatus).toHaveBeenCalled();
+
+// Negative case: clicking inside should NOT close
+const container = screen.getByText('Starting Transaction').closest('div');
+await userEvent.click(container as HTMLElement);
+expect(baseProps.closeTransactionStatus).toHaveBeenCalledTimes(1);

108-111: Snapshot test is fine; consider adding a lightweight a11y assertion

Add a small check for dialog semantics (role/aria) once implemented to make tests more resilient than snapshot-only.

-const tree = renderer.create(<TransactionStatus {...baseProps} />).toJSON();
-expect(tree).toMatchSnapshot();
+const tree = renderer.create(<TransactionStatus {...baseProps} />).toJSON();
+expect(tree).toMatchSnapshot();
+// Assert basic a11y hooks (after component adds them)
+render(<TransactionStatus {...baseProps} />);
+expect(screen.getByRole('dialog', { name: /transaction status/i })).toBeInTheDocument();

300-337: ClassName equality assertions are brittle

Asserting the full Tailwind class list will cause frequent churn. Prefer subset checks or role-based queries.

- expect(iconContainer).toHaveClass(
-   'w-[90px]','h-[90px]','rounded-full','border-[3px]','border-white/10','bg-[#8A77FF]'
- );
+ expect(iconContainer).toHaveClass('rounded-full');
+ expect(iconContainer?.className).toMatch(/bg-\[#8A77FF\]/);

Also applies to: 339-375


26-35: Mock heavy spinners to speed up and stabilize tests

Mock react-loader-spinner to a simple stub to avoid SVG/animation overhead in JSDOM.

+vi.mock('react-loader-spinner', () => ({
+  TailSpin: (props: any) => <div data-testid="tail-spin" {...props} />,
+}));
src/apps/pulse/components/Transaction/TransactionStatus.tsx (1)

96-111: Duplicate global listeners (here and in TransactionDetails/CloseButton/Esc)

Both this component and its child add Escape/outside-click listeners. Centralize to one place to avoid multiple handlers firing.

  • Option A: Keep listeners here; pass a simple onClose to children; remove listeners from TransactionDetails.
  • Option B: Remove listeners here and rely on CloseButton/Esc + a wrapper Modal that owns outside-click.

If keeping here, consider pointerdown to better catch drag cases and add a guard to ignore clicks originating from within portals.

Also applies to: 113-126

src/apps/pulse/components/Transaction/TransactionDetails.tsx (7)

119-131: Triple Escape handling; dedupe to a single source of truth

This component adds its own ESC listener while also rendering Esc/CloseButton (each adds ESC listeners) and the parent adds one too. This can trigger multiple onDone calls.

  • Remove this component’s ESC listener and rely on Esc/CloseButton (or vice‑versa). If you keep a listener here, make Esc/CloseButton dumb (no listeners).
-  useEffect(() => {
-    const handleKeyDown = (event: KeyboardEvent) => {
-      if (event.key === 'Escape') {
-        onDone();
-      }
-    };
-    document.addEventListener('keydown', handleKeyDown);
-    return () => {
-      document.removeEventListener('keydown', handleKeyDown);
-    };
-  }, [onDone]);

Also applies to: 383-392


382-392: Add dialog semantics to the details panel

Mark details as a nested dialog/region and focus the container on mount for better keyboard navigation.

-  return (
-    <div ref={detailsRef} className="flex flex-col gap-4 h-full w-full">
+  return (
+    <div
+      ref={detailsRef}
+      role="dialog"
+      aria-modal="true"
+      aria-labelledby="tx-details-title"
+      tabIndex={-1}
+      className="flex flex-col gap-4 h-full w-full"
+    >
       <div className="flex justify-between items-center">
-        <p className="text-xl text-white font-normal">Transaction Details</p>
+        <p id="tx-details-title" className="text-xl text-white font-normal">
+          Transaction Details
+        </p>

491-504: completedAt likely wrong for Buy

You pass pendingCompletedAt to TransactionInfo as completedAt. For Buy, the “final” step completion may be resourceLockCompletedAt. Consider conditional wiring.

-  completedAt={pendingCompletedAt}
+  completedAt={isBuy ? (resourceLockCompletedAt || pendingCompletedAt) : pendingCompletedAt}

264-271: Broaden secret redaction

The regex only catches api-key=...; consider covering apiKey, x-api-key, and Authorization headers to avoid accidental leakage.

-    return errorThrown.replace(
-      /api-key=[A-Za-z0-9+/=]+/g,
-      'api-key=***REDACTED***'
-    );
+    return errorThrown
+      .replace(/(?:api[-_]?key|x-api-key)=([A-Za-z0-9._\-+/=]+)/gi, '$1=***REDACTED***')
+      .replace(/Authorization:\s*Bearer\s+[A-Za-z0-9._\-+/=]+/gi, 'Authorization: Bearer ***REDACTED***');

394-422: Guard against missing token logo/source errors

When logo is missing, empty src attributes can cause 404s/noise. Provide a placeholder or hide the img when no logo is available.

-  <img
-    className="w-4 h-4 rounded"
-    src={isBuy ? buyToken?.logo || '' : sellToken?.logo || ''}
-    alt={isBuy ? buyToken?.symbol || 'Token' : sellToken?.symbol || 'Token'}
-  />
+  {(isBuy ? buyToken?.logo : sellToken?.logo) ? (
+    <img
+      className="w-4 h-4 rounded"
+      src={isBuy ? (buyToken!.logo as string) : (sellToken!.logo as string)}
+      alt={isBuy ? buyToken?.symbol || 'Token' : sellToken?.symbol || 'Token'}
+    />
+  ) : null}

321-339: Minor: transactionHash assignment comment is confusing

transactionHash sets to userOpHash for both branches; the comment implies bidHash vs userOpHash. Consider using distinct field names to avoid ambiguity.

- transactionHash: isBuy ? userOpHash : userOpHash, // For Buy: bidHash, For Sell: userOpHash
- hashType: isBuy ? 'bidHash' : 'userOpHash',
+ transactionHash: userOpHash,
+ hashType: isBuy ? 'bidHash' : 'userOpHash', // clarify that userOpHash carries bidHash in buy mode

102-117: Outside-click handler: consider pointerdown and portal-aware check

If details/status ever render via portals, contains() can misfire. Use pointerdown and a composedPath() check when available.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5acb28e and a192218.

⛔ Files ignored due to path filters (5)
  • src/apps/pulse/components/Transaction/tests/__snapshots__/ProgressStep.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/Transaction/tests/__snapshots__/TransactionDetails.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/Transaction/tests/__snapshots__/TransactionErrorBox.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/Transaction/tests/__snapshots__/TransactionInfo.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/Transaction/tests/__snapshots__/TransactionStatus.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (19)
  • src/apps/pulse/components/App/HomeScreen.tsx (7 hunks)
  • src/apps/pulse/components/Buy/PreviewBuy.tsx (3 hunks)
  • src/apps/pulse/components/Buy/tests/PreviewBuy.test.tsx (3 hunks)
  • src/apps/pulse/components/Misc/CloseButton.tsx (1 hunks)
  • src/apps/pulse/components/Sell/TransactionDetails.tsx (0 hunks)
  • src/apps/pulse/components/Sell/TransactionInfo.tsx (0 hunks)
  • src/apps/pulse/components/Sell/TransactionStatus.tsx (0 hunks)
  • src/apps/pulse/components/Sell/tests/PreviewSell.test.tsx (1 hunks)
  • src/apps/pulse/components/Sell/tests/SellButton.test.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/ProgressStep.tsx (5 hunks)
  • src/apps/pulse/components/Transaction/TransactionDetails.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionErrorBox.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionInfo.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionStatus.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/ProgressStep.test.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/TransactionErrorBox.test.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/TransactionInfo.test.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (1 hunks)
💤 Files with no reviewable changes (3)
  • src/apps/pulse/components/Sell/TransactionDetails.tsx
  • src/apps/pulse/components/Sell/TransactionStatus.tsx
  • src/apps/pulse/components/Sell/TransactionInfo.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1)
src/apps/pulse/types/tokens.ts (1)
  • SelectedToken (1-10)
src/apps/pulse/components/Transaction/TransactionDetails.tsx (4)
src/apps/pulse/components/Misc/Esc.tsx (1)
  • Esc (7-36)
src/apps/pulse/components/Misc/CloseButton.tsx (1)
  • CloseButton (7-46)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
src/apps/pulse/components/Sell/TransactionDetails.tsx (1)
  • TransactionDetailsProps (37-211)
src/apps/pulse/components/Transaction/TransactionInfo.tsx (2)
src/utils/blockchain.ts (1)
  • getBlockScan (225-246)
src/apps/pulse/components/Sell/TransactionInfo.tsx (1)
  • TransactionInfoProps (29-167)
src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (1)
src/apps/pulse/types/tokens.ts (1)
  • SelectedToken (1-10)
src/apps/pulse/components/App/HomeScreen.tsx (4)
src/apps/pulse/hooks/useIntentSdk.ts (1)
  • useIntentSdk (9-53)
src/apps/pulse/types/tokens.ts (1)
  • SelectedToken (1-10)
src/apps/pulse/hooks/useRelaySell.ts (1)
  • SellOffer (37-43)
src/services/userOpStatus.ts (1)
  • getUserOperationStatus (3-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (6)
src/apps/pulse/components/Sell/tests/SellButton.test.tsx (1)

27-30: Mock update aligns with SellOffer changes. Adding minimumReceive and slippageTolerance keeps the test fixtures in sync with the expanded SellOffer interface, so everything compiles cleanly. Nicely done.

src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1)

151-186: Solid coverage of status/interaction paths.

Snapshot + behavioral checks across statuses and close actions look good.

Also applies to: 187-235

src/apps/pulse/components/Transaction/TransactionErrorBox.tsx (1)

12-17: Verify upstream sanitization in generateTechnicalDetails
Only usage of TransactionErrorBox is in TransactionDetails.tsx (<TransactionErrorBox technicalDetails={generateTechnicalDetails()} />). Ensure generateTechnicalDetails redacts PII, API keys or tokens before passing its output here—tests cover redaction in TransactionDetails but double-check this upstream sanitization.

src/apps/pulse/components/Transaction/tests/TransactionInfo.test.tsx (1)

11-21: No change needed: mock aligns with default import

TransactionInfo imports CopyToClipboard as the default export; the test’s vi.mock correctly mocks default. No update required.

Likely an incorrect or invalid review comment.

src/apps/pulse/components/Transaction/TransactionStatus.tsx (1)

261-270: Confirm txHash plumbing for Buy vs Sell

For Buy you pass completedTxHash and omit txHash; for Sell you pass blockchainTxHash as txHash. Ensure TransactionInfo expects completedAt aligned with the final step (see comment in TransactionDetails).

src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (1)

1-6: jest-dom matchers are already set up globally
The global test setup in src/test-utils/setupTests.ts imports @testing-library/jest-dom and is loaded via setupFiles in vite.config.js, so matchers like toBeInTheDocument and toHaveClass are available.

Copy link
Collaborator

@IAmKio IAmKio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't mind checking the one CodeRabbit issue but other than that - no showstoppers!

@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 30, 2025 09:49 Inactive
@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 30, 2025 09:55 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/apps/pulse/components/Transaction/TransactionStatus.tsx (1)

265-319: Add dialog role and ARIA attributes for accessibility.

The past review comment correctly identifies that this modal-like component should have role="dialog", aria-modal="true", and an associated label for screen reader users. Implementing these attributes will improve accessibility.

   return (
     <div
       ref={transactionStatusRef}
+      role="dialog"
+      aria-modal="true"
+      aria-labelledby="transaction-status-title"
       className={`flex flex-col w-full max-w-[446px] ${!showDetails ? 'h-[calc(100vh-94px)] max-h-[600px]' : 'h-min'} bg-[#1E1D24] border border-white/5 rounded-[10px] p-6`}
     >
       {showDetails ? (
         <TransactionDetails
           onDone={
             currentStatus === 'Transaction Pending'
               ? () => setShowDetails(false)
               : closeTransactionStatus
           }
           {.../* other props */}
         />
       ) : (
         <div className="flex flex-col items-center justify-center h-full">
           {statusIcon && (
             <div className={statusIcon.containerClasses}>
               <img
                 src={statusIcon.icon}
                 alt={currentStatus}
                 className={statusIcon.iconClasses}
               />
             </div>
           )}
           <div
+            id="transaction-status-title"
             className={`text-white font-normal text-[20px] ${statusIcon ? 'mt-3' : ''}`}
           >
             {currentStatus}
           </div>
           {getThirdElement()}
         </div>
       )}
     </div>
   );
🧹 Nitpick comments (2)
src/apps/pulse/components/App/HomeScreen.tsx (2)

249-292: Verify dependency array completeness.

The showTransactionStatus callback includes buyToken, payingTokens, and usdAmount in its closure but not in the dependency array (Line 291). While the ESLint rule is disabled, this could lead to stale closures where old values are captured.

Consider whether these should be added to the dependency array:

     [sellToken, tokenAmount, sellOffer, isBuy]
+    [sellToken, buyToken, tokenAmount, sellOffer, payingTokens, usdAmount, isBuy]

Or, if the current behavior is intentional (capturing values at callback creation time), add a comment explaining why these dependencies are omitted.


559-561: Verify chainId type from API.

Line 559 converts lock.chainId to a number. If the API consistently returns a number, this conversion is unnecessary. However, if the API might return a string, the conversion is appropriate.

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a192218 and fa5dc2d.

📒 Files selected for processing (6)
  • src/apps/pillarx-app/components/PillarXLogo/PillarXLogo.tsx (1 hunks)
  • src/apps/pulse/components/App/HomeScreen.tsx (7 hunks)
  • src/apps/pulse/components/Transaction/TransactionDetails.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionStatus.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/apps/pillarx-app/components/PillarXLogo/PillarXLogo.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
src/apps/pulse/components/App/HomeScreen.tsx (5)
src/apps/pulse/hooks/useIntentSdk.ts (1)
  • useIntentSdk (9-53)
src/apps/pulse/types/tokens.ts (1)
  • SelectedToken (1-10)
src/apps/pulse/hooks/useRelaySell.ts (1)
  • SellOffer (37-43)
src/apps/pulse/components/Buy/PayingToken.tsx (1)
  • PayingToken (18-93)
src/services/userOpStatus.ts (1)
  • getUserOperationStatus (3-44)
src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1)
src/apps/pulse/types/tokens.ts (1)
  • SelectedToken (1-10)
src/apps/pulse/components/Transaction/TransactionDetails.tsx (4)
src/apps/pulse/components/Misc/Esc.tsx (1)
  • Esc (7-36)
src/apps/pulse/components/Misc/CloseButton.tsx (1)
  • CloseButton (7-46)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
src/apps/pulse/components/Sell/TransactionDetails.tsx (1)
  • TransactionDetailsProps (37-211)
src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (1)
src/apps/pulse/types/tokens.ts (1)
  • SelectedToken (1-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
🔇 Additional comments (19)
src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (4)

14-71: LGTM! Mock setup follows testing best practices.

The mock implementations are appropriate for unit testing and use data-testid attributes effectively for querying elements.


73-137: LGTM! Mock data is comprehensive and realistic.

The mock tokens, paying tokens, and base props provide good coverage for testing various scenarios.


649-663: Excellent security test for API key sanitization.

This test ensures that sensitive API keys are redacted from error details, which is critical for preventing credential leaks in logs and error reports.


1-694: Comprehensive test coverage with good practices.

This test suite demonstrates strong coverage across multiple dimensions: status variations, user interactions, buy/sell modes, error handling, edge cases, and security (API key sanitization). The use of mocks is appropriate for unit testing, and the tests effectively validate component behavior without over-reliance on implementation details.

src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (3)

14-101: LGTM! Mock setup is clean and appropriate.

The simplified TransactionDetails mock is acceptable for unit testing TransactionStatus in isolation.


198-214: Good test coverage of context-dependent done button behavior.

This test correctly verifies that clicking done in the pending status returns to the main view rather than closing the transaction status entirely, which is the expected UX flow.


1-530: Comprehensive and well-organized test suite.

The tests provide strong coverage of the TransactionStatus component, including status variations, conditional interactions (ESC/outside-click behavior based on status), buy/sell modes, styling, and edge cases. The interaction tests particularly shine in verifying context-dependent behavior.

src/apps/pulse/components/Transaction/TransactionStatus.tsx (1)

12-66: LGTM! Well-typed interface with clear documentation.

The TransactionStatusProps interface is comprehensive and the comment on Line 14 usefully clarifies that userOpHash serves dual purposes (UserOp hash for Sell, bidHash for Buy).

src/apps/pulse/components/App/HomeScreen.tsx (2)

294-307: LGTM! Smart background polling logic.

The closeTransactionStatus function correctly enables background polling when the transaction is still in progress, allowing the user to close the modal while preserving transaction state awareness. The auto-reopen flag ensures the modal reappears when the transaction completes.


524-689: LGTM! Comprehensive polling logic for Buy and Sell flows.

The polling effect correctly handles both Buy (bid + resource lock) and Sell (UserOp) transaction flows with appropriate status mapping, error handling, and hash extraction. The 2-second polling interval is reasonable, and the chainId resolution now correctly uses the buyToken chain for Buy transactions (past issue resolved).

src/apps/pulse/components/Transaction/TransactionDetails.tsx (9)

1-16: LGTM!

All imports are used appropriately throughout the component. The conditional usage of Esc and CloseButton based on transaction status is intentional.


18-73: LGTM!

The interface is comprehensive and correctly models both Buy and Sell transaction flows with appropriate optional fields.


141-199: Past review concern remains: Pending step cannot indicate failure.

The previous review correctly identified that getStepStatus never returns 'failed' for the 'Pending' step (line 186 always returns 'completed' when status is 'Transaction Failed'). This prevents determineFailureStep from detecting a pending-step failure via lines 258-260.

Verify whether this is intentional behavior (i.e., pending always completes before failure) or if the mapping should distinguish between "pending completed then failed" vs "failed during pending."

If failures can occur during pending without a transaction hash, consider applying the diff suggested in the past review to mark Pending as 'failed' when the expected hash is missing.

Based on learnings


201-242: LGTM!

The timestamp formatting correctly returns JSX elements for completed steps (excluding the final step) with appropriate date/time formats and styling.


244-269: determineFailureStep depends on getStepStatus fix.

This function's ability to detect pending/submission failures depends on getStepStatus returning 'failed' for those steps, which currently doesn't happen (lines 255-260 will never execute as written). See the earlier comment on getStepStatus.


282-288: Clarify bidHash vs userOpHash usage.

Line 286 uses userOpHash for both Buy and Sell, but the comment on line 286 states "For Buy: bidHash, For Sell: userOpHash". If Buy transactions should use a different hash identifier (bidHash), ensure the correct value is passed or update the comment.

Verify whether userOpHash is the correct identifier for Buy transactions, or if a separate bidHash prop should be added to the interface.


271-279: LGTM!

The API key sanitization regex correctly redacts sensitive data from error messages.


390-430: LGTM!

The header and summary card correctly render transaction details with proper conditional logic for Buy vs Sell flows, and the Esc/CloseButton distinction matches the expected behavior per status.


432-529: LGTM!

The rendering logic correctly handles Buy vs Sell flows with appropriate progress steps, conditional displays, and proper prop passing to child components.

@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 30, 2025 13:14 Inactive
@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 30, 2025 13:18 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (2)
src/apps/pulse/components/Transaction/TransactionInfo.tsx (2)

47-49: Guard explorer link when getBlockScan returns empty URL.

If getBlockScan(cId) returns an empty string, window.open may navigate to a broken relative path. This issue was previously flagged and remains unresolved.

Apply this diff to guard the handler:

 const handleExternalLink = (hash: string, cId: number) => {
+  const base = getBlockScan(cId);
+  if (!base) return;
-  window.open(`${getBlockScan(cId)}${hash}`, '_blank', 'noopener,noreferrer');
+  window.open(`${base}${hash}`, '_blank', 'noopener,noreferrer');
 };

Additionally, only render the external link button when a valid explorer URL exists:

-        {showExternalLink && isHashValid && transactionHash && (
+        {showExternalLink && isHashValid && transactionHash && getBlockScan(effectiveChainId) && (
           <button
             type="button"
+            aria-label="Open in block explorer"
             onClick={() =>
               handleExternalLink(transactionHash, effectiveChainId)
             }

107-109: Avoid boolean-to-string in className.

Using a boolean inside a template literal yields "false" as a class when lhs !== 'Status'. This issue was previously flagged and remains unresolved.

Apply this diff:

-          <span className={`${lhs === 'Status' && statusInfo.color}`}>
+          <span className={lhs === 'Status' ? statusInfo.color : ''}>
             {rhs}
           </span>
🧹 Nitpick comments (17)
src/apps/pulse/hooks/tests/useClickOutside.test.ts (1)

24-36: Ineffective event listener cleanup in afterEach.

Lines 25, 33, and 35 call removeEventListener with vi.fn(), which creates new function instances each time. Event listeners are identified by reference, so these calls won't remove the actual listeners registered by the hook. The querySelectorAll loop (lines 31-34) adds unnecessary overhead and also doesn't remove listeners.

The hook itself handles cleanup in its useEffect return, so explicit afterEach cleanup is redundant here. If you need to verify cleanup, the test at lines 127-146 already validates it.

Apply this diff to remove the ineffective cleanup:

   afterEach(() => {
     vi.clearAllMocks();
-    // Clean up event listeners - remove all mousedown listeners
-    const listeners = document.querySelectorAll('*');
-    listeners.forEach((el) => {
-      el.removeEventListener('mousedown', vi.fn());
-    });
-    document.removeEventListener('mousedown', vi.fn());
   });
src/apps/pulse/utils/utils.tsx (1)

57-64: Consider broader regex pattern for API key redaction.

The current pattern /api-key=[A-Za-z0-9+/=]+/g matches base64-like characters but may miss URL-encoded keys or keys with dashes/underscores. If your API keys can contain -, _, or %-encoded characters, consider a more inclusive pattern:

   return errorThrown.replace(
-    /api-key=[A-Za-z0-9+/=]+/g,
+    /api-key=[A-Za-z0-9+/=_-]+/g,
     'api-key=***REDACTED***'
   );

Alternatively, use a non-whitespace pattern if keys are delimited by whitespace: /api-key=\S+/g.

src/apps/pulse/hooks/useClickOutside.ts (1)

7-29: Stabilize the callback passed to useClickOutside
Wrap the closeTransactionStatus prop and inline () => setShowDetails(false) callback in useCallback (or adopt a ref-based handler inside the hook) so the effect’s dependency on callback doesn’t re-register the listener on every render.

src/apps/pulse/hooks/useKeyboardNavigation.ts (1)

34-34: Consider documenting the need for stable callbacks.

The effect depends on onEscape and onEnter, which will cause the listener to be re-registered on every render if these callbacks are not stable (e.g., inline functions).

Consumers should wrap onEscape and onEnter with useCallback to avoid unnecessary re-registrations. Consider adding a JSDoc comment to document this requirement, or alternatively, wrap the callbacks internally with useCallback (though that would require additional dependencies).

src/apps/pulse/hooks/useTransactionStatus.ts (1)

27-43: Consider simplifying the memoization pattern.

The useMemo wraps a function that delegates to getStepStatus. However, since the consumer will call getStepStatusForStep(step) on each render anyway, memoizing the wrapper function provides limited benefit unless the function is passed as a prop to a memoized child component.

Consider one of the following:

  1. If the function is passed to memoized children, keep the current pattern and document the reason.
  2. If not, simplify by computing all step statuses upfront and returning an object instead of a function:
    const stepStatuses = useMemo(() => ({
      Submitted: getStepStatus('Submitted', status, isBuy, resourceLockTxHash, completedTxHash, isResourceLockFailed),
      Pending: getStepStatus('Pending', status, isBuy, resourceLockTxHash, completedTxHash, isResourceLockFailed),
      ResourceLock: getStepStatus('ResourceLock', status, isBuy, resourceLockTxHash, completedTxHash, isResourceLockFailed),
      Completed: getStepStatus('Completed', status, isBuy, resourceLockTxHash, completedTxHash, isResourceLockFailed),
    }), [status, isBuy, resourceLockTxHash, completedTxHash, isResourceLockFailed]);
src/apps/pulse/components/Transaction/TransactionStatusContainer.tsx (1)

15-26: Consider accessibility and localization.

The component directly renders the status string without any accessibility attributes or localization. Consider the following improvements:

  1. Accessibility: Add an aria-live="polite" attribute to the status text container so screen readers announce status changes.
  2. Localization: If the app supports multiple languages, the status text should be localized. If not, consider documenting that status strings are intentionally user-facing.

Example:

-      <div className="text-white font-normal text-[20px] mt-3">{status}</div>
+      <div 
+        className="text-white font-normal text-[20px] mt-3"
+        aria-live="polite"
+        role="status"
+      >
+        {status}
+      </div>
src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1)

384-436: Consider completing the commented-out timestamp tests.

Two timestamp tests are partially commented out (lines 418 and 432-434) with notes about "complex conditional logic". While this is acceptable for the current PR, these tests should ideally be completed to ensure full coverage of timestamp rendering.

Consider adding these to a follow-up task to improve test completeness.

src/apps/pulse/constants/constants.ts (2)

22-36: Duplication: STARTING and PENDING configs are identical.

Lines 23-29 and 30-36 define identical configurations for STARTING and PENDING statuses (same icon, containerClasses, iconClasses, and color). Consider consolidating these to reduce maintenance burden.

 export const STATUS_CONFIG = {
-  [TRANSACTION_STATUSES.STARTING]: {
+  [TRANSACTION_STATUSES.STARTING]: (() => {
+    const sharedPendingConfig = {
+      icon: 'pending',
+      containerClasses:
+        'w-[90px] h-[90px] rounded-full border-[3px] border-white/10 bg-[#8A77FF] flex items-center justify-center flex-shrink-0',
+      iconClasses: 'w-[60px] h-[60px]',
+      color: '#8A77FF',
+    };
+    return sharedPendingConfig;
+  })(),
-  [TRANSACTION_STATUSES.PENDING]: {
+  [TRANSACTION_STATUSES.PENDING]: STATUS_CONFIG[TRANSACTION_STATUSES.STARTING],
-    icon: 'pending',
-    containerClasses:
-      'w-[90px] h-[90px] rounded-full border-[3px] border-white/10 bg-[#8A77FF] flex items-center justify-center flex-shrink-0',
-    iconClasses: 'w-[60px] h-[60px]',
-    color: '#8A77FF',
-  },

Alternatively, extract the shared config to a constant and reference it in both keys.


15-20: Consider extracting color palette constants.

The hex colors (#8A77FF, #FFAB36, #5CFF93, #FF366C) are repeated across STATUS_COLORS, STATUS_CONFIG, and BUTTON_CONFIG. Extracting these to named palette constants would improve maintainability and ensure consistency.

const COLORS = {
  PURPLE: '#8A77FF',
  ORANGE: '#FFAB36',
  GREEN: '#5CFF93',
  RED: '#FF366C',
} as const;

export const STATUS_COLORS = {
  [TRANSACTION_STATUSES.STARTING]: COLORS.PURPLE,
  [TRANSACTION_STATUSES.PENDING]: COLORS.ORANGE,
  [TRANSACTION_STATUSES.COMPLETE]: COLORS.GREEN,
  [TRANSACTION_STATUSES.FAILED]: COLORS.RED,
};
src/apps/pulse/components/Transaction/TransactionInfo.tsx (1)

53-132: Consider reducing parameter count in detailsEntry.

The detailsEntry helper accepts 7 parameters, which can be hard to track. Consider using an options object for optional parameters.

const detailsEntry = (
  lhs: string,
  rhs: string,
  options?: {
    showExternalLink?: boolean;
    showCopy?: boolean;
    tooltipContent?: string;
    transactionHash?: string;
    txChainId?: number;
  }
) => {
  const {
    showExternalLink = false,
    showCopy = false,
    tooltipContent = '',
    transactionHash,
    txChainId,
  } = options || {};
  // ... rest of implementation
};
src/apps/pulse/hooks/useTechnicalDetails.ts (1)

98-121: Nested ternary is hard to parse.

The triple-nested ternary for the token field is difficult to read and maintain, despite the eslint-disable comment. Consider extracting this logic into a helper function.

const getTokenDetails = (): TokenDetails | null => {
  if (isBuy && buyToken) {
    return {
      symbol: buyToken.symbol,
      name: buyToken.name,
      address: buyToken.address || 'N/A',
      chainId,
      amount: tokenAmount || 'N/A',
      logo: buyToken.logo || 'N/A',
      type: 'BUY_TOKEN' as const,
    } satisfies TokenDetails;
  }
  if (!isBuy && sellToken) {
    return {
      symbol: sellToken.symbol,
      name: sellToken.name,
      address: sellToken.address || 'N/A',
      chainId,
      amount: tokenAmount || 'N/A',
      logo: sellToken.logo || 'N/A',
      type: 'SELL_TOKEN' as const,
    } satisfies TokenDetails;
  }
  return null;
};

const details: TechnicalDetails = {
  // ...
  token: getTokenDetails(),
  // ...
};
src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (4)

1-2: Reconsider broad ESLint disables.

Disabling @typescript-eslint/no-explicit-any and react/jsx-props-no-spreading for the entire file weakens type safety. Limit these to specific lines where necessary, or refactor to avoid any types and object spreading.


18-22: Mock return value lacks type safety.

The mock returns an object without type constraints. Consider typing the return value to match useTransactionKit's actual return type.

-vi.mock('../../../../hooks/useTransactionKit', () => ({
-  default: () => ({
-    walletAddress: '0x1234567890123456789012345678901234567890',
-  }),
-}));
+vi.mock('../../../../hooks/useTransactionKit', () => ({
+  default: (): { walletAddress: string } => ({
+    walletAddress: '0x1234567890123456789012345678901234567890',
+  }),
+}));

309-333: Handle undefined callback more gracefully.

The test uses if (onEscapeCallback) but the callback should always be defined when the hook is called. Consider asserting its presence instead.

       // Simulate ESC key press
-      if (onEscapeCallback) {
-        act(() => {
-          onEscapeCallback!();
-        });
-      }
+      expect(onEscapeCallback).toBeDefined();
+      act(() => {
+        onEscapeCallback!();
+      });

392-416: Same pattern for click-outside callback.

Similar to the ESC handler, assert that the callback is defined rather than conditionally checking it.

       // Simulate click outside
-      if (callback) {
-        act(() => {
-          callback!();
-        });
-      }
+      expect(callback).toBeDefined();
+      act(() => {
+        callback!();
+      });
src/apps/pulse/types/types.ts (1)

167-200: TechnicalDetails interface is comprehensive for debugging.

The nested structure with dedicated sections (transactionHashes, chains, timestamps, error, gas, stepStatus) supports detailed error diagnostics. The [key: string]: string pattern in transactionHashes, timestamps, and stepStatus provides flexibility for dynamic keys.

Minor suggestion: Consider using more specific key types instead of [key: string] for better type safety if the keys are known at compile time.

src/apps/pulse/components/Transaction/TransactionDetails.tsx (1)

264-272: Consider adding keyboard Enter support for the Done button.

The button correctly triggers onDone on click, but keyboard users navigating with Tab+Enter would benefit from the Enter key calling onDone when focused. The useKeyboardNavigation hook supports onEnter but it's not currently configured.

Optional enhancement to improve keyboard accessibility:

   useKeyboardNavigation({
     onEscape: () => {
       if (
         status === 'Transaction Complete' ||
         status === 'Transaction Failed'
       ) {
         onDone();
       } else if (status === 'Transaction Pending') {
         onDone();
       }
     },
+    onEnter: onDone,
   });

Note: This would make Enter trigger Done from anywhere in the modal, not just when the button is focused. If you want Enter only on button focus, the native button behavior already handles that, so this change is optional.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa5dc2d and bfa8546.

⛔ Files ignored due to path filters (3)
  • src/apps/pulse/components/Transaction/tests/__snapshots__/TransactionDetails.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/Transaction/tests/__snapshots__/TransactionStatus.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/Transaction/tests/__snapshots__/TransactionStatusContainer.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (20)
  • src/apps/pulse/components/Transaction/ProgressStep.tsx (5 hunks)
  • src/apps/pulse/components/Transaction/TransactionDetails.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionInfo.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionStatus.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionStatusButton.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionStatusContainer.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionStatusIcon.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/tests/TransactionStatusContainer.test.tsx (1 hunks)
  • src/apps/pulse/constants/constants.ts (1 hunks)
  • src/apps/pulse/hooks/tests/useClickOutside.test.ts (1 hunks)
  • src/apps/pulse/hooks/tests/useKeyboardNavigation.test.ts (1 hunks)
  • src/apps/pulse/hooks/useClickOutside.ts (1 hunks)
  • src/apps/pulse/hooks/useKeyboardNavigation.ts (1 hunks)
  • src/apps/pulse/hooks/useTechnicalDetails.ts (1 hunks)
  • src/apps/pulse/hooks/useTransactionStatus.ts (1 hunks)
  • src/apps/pulse/types/types.ts (1 hunks)
  • src/apps/pulse/utils/tests/utils.test.tsx (1 hunks)
  • src/apps/pulse/utils/utils.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (16)
src/apps/pulse/components/Transaction/TransactionStatusButton.tsx (2)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusState (1-5)
src/apps/pulse/utils/utils.tsx (1)
  • getButtonConfig (43-45)
src/apps/pulse/components/Transaction/TransactionStatusContainer.tsx (1)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusState (1-5)
src/apps/pulse/hooks/useKeyboardNavigation.ts (1)
src/apps/pulse/types/types.ts (1)
  • UseKeyboardNavigationOptions (161-165)
src/apps/pulse/hooks/useClickOutside.ts (1)
src/apps/pulse/types/types.ts (1)
  • UseClickOutsideOptions (155-159)
src/apps/pulse/components/Transaction/TransactionStatusIcon.tsx (2)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusState (1-5)
src/apps/pulse/utils/utils.tsx (1)
  • getStatusConfig (36-38)
src/apps/pulse/components/Transaction/TransactionDetails.tsx (9)
src/apps/pulse/types/types.ts (1)
  • TransactionDetailsProps (100-124)
src/apps/pulse/hooks/useTransactionStatus.ts (1)
  • useTransactionStatus (20-53)
src/apps/pulse/hooks/useTechnicalDetails.ts (1)
  • useTechnicalDetails (49-219)
src/apps/pulse/hooks/useClickOutside.ts (1)
  • useClickOutside (7-29)
src/apps/pulse/hooks/useKeyboardNavigation.ts (1)
  • useKeyboardNavigation (7-35)
src/apps/pulse/utils/utils.tsx (1)
  • formatStepTimestamp (79-94)
src/apps/pulse/components/Misc/Esc.tsx (1)
  • Esc (7-36)
src/apps/pulse/components/Misc/CloseButton.tsx (1)
  • CloseButton (7-46)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
src/apps/pulse/components/Transaction/TransactionStatus.tsx (3)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusProps (73-98)
src/apps/pulse/hooks/useClickOutside.ts (1)
  • useClickOutside (7-29)
src/apps/pulse/hooks/useKeyboardNavigation.ts (1)
  • useKeyboardNavigation (7-35)
src/apps/pulse/utils/tests/utils.test.tsx (1)
src/apps/pulse/utils/utils.tsx (12)
  • truncateHash (21-24)
  • getStatusColor (29-31)
  • getStatusConfig (36-38)
  • getButtonConfig (43-45)
  • isValidHash (50-52)
  • sanitizeErrorDetails (57-64)
  • formatTimestamp (69-74)
  • formatStepTimestamp (79-94)
  • getStatusInfo (99-110)
  • getStepStatus (115-171)
  • determineFailureStep (176-195)
  • canCloseTransaction (200-204)
src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (5)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusState (1-5)
src/apps/pulse/hooks/useClickOutside.ts (1)
  • useClickOutside (7-29)
src/apps/pulse/hooks/useKeyboardNavigation.ts (1)
  • useKeyboardNavigation (7-35)
src/apps/pulse/hooks/useTransactionStatus.ts (1)
  • useTransactionStatus (20-53)
src/apps/pulse/hooks/useTechnicalDetails.ts (1)
  • useTechnicalDetails (49-219)
src/apps/pulse/utils/utils.tsx (2)
src/apps/pulse/types/types.ts (3)
  • TransactionStatusState (1-5)
  • TransactionStep (7-11)
  • StepStatus (13-13)
src/apps/pulse/constants/constants.ts (3)
  • STATUS_COLORS (15-20)
  • STATUS_CONFIG (22-51)
  • BUTTON_CONFIG (53-78)
src/apps/pulse/hooks/tests/useKeyboardNavigation.test.ts (1)
src/apps/pulse/hooks/useKeyboardNavigation.ts (1)
  • useKeyboardNavigation (7-35)
src/apps/pulse/hooks/useTechnicalDetails.ts (2)
src/apps/pulse/types/types.ts (7)
  • TransactionStatusState (1-5)
  • TokenInfo (15-20)
  • SellOffer (22-25)
  • PayingToken (27-36)
  • TechnicalDetails (167-200)
  • TokenDetails (38-46)
  • BuyModeDetails (48-52)
src/apps/pulse/utils/utils.tsx (3)
  • getStepStatus (115-171)
  • sanitizeErrorDetails (57-64)
  • determineFailureStep (176-195)
src/apps/pulse/components/Transaction/TransactionInfo.tsx (4)
src/apps/pulse/types/types.ts (1)
  • TransactionInfoProps (126-139)
src/utils/blockchain.ts (1)
  • getBlockScan (225-246)
src/apps/pulse/utils/utils.tsx (3)
  • getStatusInfo (99-110)
  • isValidHash (50-52)
  • truncateHash (21-24)
src/apps/pulse/components/Sell/TransactionInfo.tsx (1)
  • TransactionInfoProps (29-167)
src/apps/pulse/hooks/tests/useClickOutside.test.ts (1)
src/apps/pulse/hooks/useClickOutside.ts (1)
  • useClickOutside (7-29)
src/apps/pulse/hooks/useTransactionStatus.ts (2)
src/apps/pulse/types/types.ts (2)
  • TransactionStatusState (1-5)
  • TransactionStep (7-11)
src/apps/pulse/utils/utils.tsx (1)
  • getStepStatus (115-171)
src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (3)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusState (1-5)
src/apps/pulse/hooks/useClickOutside.ts (1)
  • useClickOutside (7-29)
src/apps/pulse/hooks/useKeyboardNavigation.ts (1)
  • useKeyboardNavigation (7-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (81)
src/apps/pulse/hooks/tests/useClickOutside.test.ts (5)

38-65: LGTM!

The test correctly validates that the callback is invoked when clicking outside the referenced element with condition true. DOM setup, event dispatch, and cleanup are properly handled.


67-94: LGTM!

Correctly validates that the callback is suppressed when condition is false.


96-125: LGTM!

Properly tests the null ref guard to ensure the callback is not invoked when ref.current is null.


127-146: LGTM!

Validates that the hook cleans up the event listener on unmount.


148-191: LGTM!

Thoroughly tests dynamic condition changes by using rerender and verifying that the callback respects the updated condition.

src/apps/pulse/utils/utils.tsx (6)

29-52: LGTM!

The configuration lookup functions have appropriate fallbacks, and isValidHash correctly validates hash presence.


69-94: LGTM!

Timestamp formatting functions correctly use date-fns and provide appropriate UI-specific formatting for different step contexts.


99-110: LGTM!

Status-to-UI mapping is clear and provides appropriate text and color for each state.


115-171: LGTM! Complex but well-structured.

This function correctly handles the nuanced per-step status logic for Buy vs. Sell flows, including resource lock and completion hash checks. The logic is covered by tests (per the PR context), which is essential given the complexity.


176-195: LGTM!

Correctly determines the failure step by checking resource lock failure and step statuses in order.


200-204: LGTM!

Correctly restricts closing to terminal states (Complete or Failed).

src/apps/pulse/components/Transaction/TransactionStatusButton.tsx (3)

22-52: LGTM!

The renderIcon helper correctly maps each status to its corresponding icon or spinner.


54-60: LGTM!

Correctly renders a non-interactive message for the Starting Transaction state.


62-81: LGTM!

Button structure is correct with proper type attribute, dynamic styling, and memoization.

src/apps/pulse/components/Transaction/tests/TransactionStatusContainer.test.tsx (4)

9-28: LGTM!

Mock implementations are appropriate for isolating the container's behavior.


37-129: LGTM!

Rendering tests provide good coverage across all status states with appropriate assertions.


131-160: LGTM!

Interaction tests correctly verify callback invocation and prop passing.


162-182: LGTM!

Layout test appropriately validates the container's CSS classes.

src/apps/pulse/hooks/useKeyboardNavigation.ts (4)

7-11: LGTM!

The hook signature is clean and follows React conventions. The default value for enabled=true is appropriate for the common case.


12-15: LGTM!

The early return pattern for the disabled state is correct and prevents unnecessary listener registration.


17-27: Verify that Enter preventDefault doesn't interfere with form inputs.

The handler unconditionally calls preventDefault() for both Escape and Enter keys. In contexts where form inputs might be focused (e.g., if transaction UI ever includes text fields), this would prevent form submissions or other native Enter behaviors.

For the current transaction status UI, this is likely acceptable, but please verify that no inputs exist in the same document scope where Enter should retain its default behavior.


29-33: LGTM!

The event listener registration and cleanup pattern is correct. The cleanup function ensures no memory leaks on unmount or dependency changes.

src/apps/pulse/hooks/useTransactionStatus.ts (4)

9-15: LGTM!

The interface definition is clear and complete. Optional properties are appropriately marked.


20-26: LGTM!

The hook signature is clean and follows React conventions. The default value for isResourceLockFailed = false is appropriate.


45-47: LGTM!

The canClose memoization is appropriate and the logic correctly identifies terminal states.


49-52: LGTM!

The return object is clear and provides a clean API for consumers.

src/apps/pulse/components/Transaction/TransactionStatusContainer.tsx (2)

10-13: LGTM!

The interface definition is clear and complete.


28-28: LGTM! Ensure parent provides stable onViewDetails callback.

The component is appropriately memoized. However, the memoization will only be effective if the parent provides a stable onViewDetails callback (e.g., wrapped with useCallback). Without a stable callback, the component will re-render on every parent render.

src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (10)

1-6: LGTM!

The imports are clean and appropriate for the test suite.


19-106: LGTM!

The mocking strategy is comprehensive and appropriate for isolating the component under test. Subcomponents and hooks are properly mocked.


108-146: LGTM!

The baseProps object is well-structured and provides comprehensive default data for tests. This is a good practice for maintainability.


148-214: LGTM!

The beforeEach setup is thorough and ensures clean state for each test. Default mock implementations are reasonable and provide a solid baseline.


216-279: LGTM!

The rendering tests provide good coverage of different transaction statuses and verify the expected UI elements are present or hidden accordingly.


281-302: LGTM!

The buy mode test correctly verifies that the ResourceLock step is rendered for buy transactions.


438-568: LGTM!

The user interaction tests are comprehensive and well-structured. They correctly simulate hook behavior and verify that callbacks are invoked appropriately based on transaction status.


570-590: LGTM!

The error state tests correctly verify that the error box is displayed for failed transactions and that resource lock failures are handled appropriately in buy mode.


592-658: LGTM!

The hook integration tests are thorough and verify that the component correctly passes props to its dependencies. This is valuable for catching integration issues.


660-684: LGTM!

The props passing test correctly verifies that the component forwards props to the TransactionInfo subcomponent.

src/apps/pulse/hooks/tests/useKeyboardNavigation.test.ts (9)

25-43: LGTM!

The test correctly verifies that onEscape is called when the Escape key is pressed.


45-63: LGTM!

The test correctly verifies that onEnter is called when the Enter key is pressed and the callback is provided.


65-82: LGTM!

The test correctly verifies that onEnter is not called when the callback is not provided.


84-102: LGTM!

The test correctly verifies that key handlers are not invoked when the hook is disabled.


123-146: LGTM!

The test correctly verifies that non-target keys are ignored. Good coverage of edge cases.


148-169: LGTM!

The test correctly verifies that preventDefault is called for handled keys.


171-190: LGTM!

The test correctly verifies that the event listener is cleaned up on unmount. This is important for preventing memory leaks.


192-223: LGTM!

The test correctly verifies that the hook responds to changes in the enabled prop across rerenders. Good coverage of dynamic state changes.


225-260: LGTM!

The test correctly verifies that the hook responds to callback changes across rerenders. Good coverage of dynamic callback updates.

src/apps/pulse/components/Transaction/TransactionStatusIcon.tsx (1)

23-33: Clean implementation with good memoization.

The icon mapping logic is clear and the use of React.memo is appropriate for this presentational component.

src/apps/pulse/hooks/useTechnicalDetails.ts (1)

73-192: Well-structured hook with comprehensive memoization.

The hook correctly assembles the technical details payload and includes all relevant dependencies in the useMemo array. The use of satisfies for type safety and helper functions for step status logic is good practice.

src/apps/pulse/utils/tests/utils.test.tsx (3)

263-357: Comprehensive step status test coverage.

The test suite covers all transaction states, Buy/Sell modes, resource lock scenarios, and failure cases. This provides strong confidence in the getStepStatus utility logic.


359-402: Good use of mocking for failure step determination.

The tests properly mock the getStepStatus function to verify each failure path. The sequential mock return values clearly demonstrate the branching logic.


169-175: sanitizeErrorDetails test expectation is correct
The regex /api-key=[A-Za-z0-9+/=]+/g matches only api-key=test, so the test’s output (Error with api-key=***REDACTED***-api-key-123 and other data) aligns with actual behavior.

src/apps/pulse/components/Transaction/tests/TransactionStatus.test.tsx (3)

67-105: LGTM! Comprehensive base props.

The baseProps object provides thorough test data covering all required and optional fields. This enables robust testing across various scenarios.


442-482: LGTM! Hook integration tests are thorough.

These tests validate that hooks are called with correct parameters for different transaction states, ensuring proper integration between the component and its hooks.


183-213: Timer cleanup verified. All three tests that call vi.useFakeTimers() also invoke vi.useRealTimers(), so no further action is needed.

src/apps/pulse/components/Transaction/TransactionStatus.tsx (6)

44-58: Effect cleanup correctly handles timer.

The effect properly cleans up the timeout and returns undefined when conditions aren't met, preventing memory leaks and ensuring correct behavior across re-renders.


60-66: Click-outside only active for terminal states.

Good defensive design—click-outside is only enabled for completed/failed states, preventing accidental dismissal during pending transactions.


68-79: Keyboard navigation logic is status-aware.

The ESC key behavior correctly differentiates between terminal states (close entirely) and pending state with details open (collapse to container). This provides intuitive navigation.


81-122: Component structure is clean and well-organized.

The component logic is straightforward: it manages detail visibility, integrates hooks for interactions, and conditionally renders the appropriate child component with comprehensive props. The structure supports maintainability.


109-109: Confirm sell flow handles missing completedTxHash
Ensure TransactionDetails gracefully handles an undefined completedTxHash (when isBuy is false) without breaking the UI or displaying empty data.


105-105: Conditional txHash assignment is correct
txHash is intentionally undefined for buy transactions; TransactionInfo’s isBuy branch renders completedTxHash instead, so no changes are needed.

src/apps/pulse/components/Transaction/ProgressStep.tsx (7)

2-2: LGTM! Type externalization improves maintainability.

Moving ProgressStepProps to a shared types file promotes reusability and consistency across components.


38-40: Line status logic covers all states.

The line rendering correctly handles active states (completed, pending, failed) with purple and inactive with dark background. This provides clear visual progression.


67-70: Icon sizing increased for better visibility.

Changing from w-2 to w-3 and adding strokeWidth="2" improves visual clarity of the checkmark icon.


98-116: Pending spinner logic extended for ResourceLock and Completed.

The component now shows spinners for both ResourceLock and Completed steps when pending, and correctly renders nothing for inactive status. This aligns with the multi-step buy flow.


126-133: Responsive timestamp display is user-friendly.

The implementation shows full timestamps on larger screens and time-only on small screens using Tailwind's responsive utilities, improving mobile UX.


53-61: Timestamp parsing assumes specific format. This helper extracts the text after a bullet (“•”). Confirm that every timestamp string consistently includes exactly one bullet prefix; otherwise, add validation or a fallback branch to handle missing or multiple bullets.


23-35: No change required. The ResourceLock step correctly renders as pending (with spinner) for buy transactions, per the getStepStatus implementation and existing ProgressStep.test.tsx coverage.

src/apps/pulse/types/types.ts (6)

1-13: LGTM! Core type unions are well-defined.

The union types for TransactionStatusState, TransactionStep, and StepStatus provide clear state machines for the transaction flow. String literal unions are appropriate here for type safety and enable exhaustive pattern matching.


15-46: LGTM! Token-related interfaces are comprehensive.

The token data structures (TokenInfo, SellOffer, PayingToken, TokenDetails) capture all necessary fields for both Buy and Sell flows. The type discriminator in TokenDetails enables proper type narrowing.


48-71: LGTM! Configuration interfaces support UI composition.

BuyModeDetails, TransactionStatusConfig, ButtonConfig, and ProgressStepConfig appropriately separate UI configuration from business logic.


100-124: LGTM! TransactionDetailsProps mirrors TransactionStatusProps appropriately.

The props interface includes all necessary fields for the details view. The optional field pattern is consistent with TransactionStatusProps.


126-165: LGTM! Component and hook props are well-typed.

TransactionInfoProps, ProgressStepProps, TransactionErrorBoxProps, UseClickOutsideOptions, and UseKeyboardNavigationOptions provide clear contracts for their respective consumers.


73-98: No unsafe optional accesses found
All optional props in TransactionStatusProps are accessed with optional chaining or fallback defaults throughout consuming components.

src/apps/pulse/components/Transaction/TransactionDetails.tsx (7)

1-26: LGTM! Imports are well-organized.

Dependencies are grouped logically (components, hooks, types, utils) and all necessary imports are present.


27-60: LGTM! Hook initialization follows best practices.

The component properly extracts account address, initializes transaction status logic, and configures technical details for debugging. Hook dependency patterns are correct.


87-104: LGTM! Event handlers are correctly implemented.

The useClickOutside hook properly guards with the canClose condition, and useKeyboardNavigation correctly differentiates behavior for Pending vs Complete/Failed states. The past review concern about duplicate ESC listeners has been addressed (no component-level useEffect duplicating the behavior).


177-242: LGTM! Progress step rendering correctly handles Buy vs Sell flows.

The conditional rendering (lines 194-238) appropriately shows different step sequences:

  • Buy: Submitted → ResourceLock → Completed
  • Sell: Submitted → Pending → Completed

Dynamic labels for failed states (lines 208-210, 230-232) provide clear user feedback.


244-258: LGTM! TransactionInfo receives all required props.

The component is passed comprehensive transaction data including optional Buy-specific fields for multi-chain transactions.


260-262: LGTM! Error handling is appropriately conditional.

TransactionErrorBox only renders for failed transactions and receives sanitized technical details.


106-133: Verify and cover step‐timestamp logic

  • Confirm with UX/product whether the final “Completed” step should ever render a timestamp (the code currently returns undefined when step === 'Completed').
  • Ensure falling back to pendingCompletedAt for the ‘ResourceLock’ step in buy flows matches business requirements.
  • Add unit tests for getStepTimestamp covering both the “Completed” step and the ‘ResourceLock’ fallback scenario.

@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) September 30, 2025 15:07 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/apps/pulse/hooks/tests/useKeyboardNavigation.test.ts (1)

143-164: Consider verifying preventDefault more reliably.

While this test works, assigning preventDefault after event construction can be fragile. Consider using Object.defineProperty for better cross-environment reliability:

-    escapeEvent.preventDefault = mockPreventDefault;
+    Object.defineProperty(escapeEvent, 'preventDefault', {
+      value: mockPreventDefault,
+      writable: true,
+    });

However, since the current approach works in the test environment, this is an optional improvement.

src/apps/pulse/utils/tests/utils.test.tsx (1)

209-227: LGTM!

The formatStepTimestamp tests validate that JSX is returned for different step types. The current assertions (toBeDefined and typeof 'object') provide basic coverage. For more robust validation, consider using @testing-library/react to render and assert on the JSX structure.

src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (2)

4-4: Migrate away from deprecated react-test-renderer.

The react-test-renderer package is deprecated by the React team and will emit warnings on React 19+. Since this project already uses @testing-library/react, you can replace snapshot testing with asFragment() or container.innerHTML serialization.

Replace the snapshot test on lines 217-222 with:

-import renderer from 'react-test-renderer';

   it('renders correctly and matches snapshot', () => {
-    const tree = renderer
-      .create(<TransactionDetails {...baseProps} />)
-      .toJSON();
-    expect(tree).toMatchSnapshot();
+    const { asFragment } = render(<TransactionDetails {...baseProps} />);
+    expect(asFragment()).toMatchSnapshot();
   });

Based on learnings.


281-302: Consider expanding Buy mode test coverage.

The current Buy mode tests only verify that the resource lock step is present. Consider adding tests for:

  • Buy-specific props (payingTokens, usdAmount)
  • Buy mode with completed resource lock timestamp
  • Buy mode failure scenarios (resource lock failed)

Example additional test:

it('renders buy mode with paying tokens and USD amount', () => {
  const payingTokens = [
    { symbol: 'USDC', amount: 50, totalUsd: 50 },
    { symbol: 'DAI', amount: 50, totalUsd: 50 },
  ];
  
  render(
    <TransactionDetails
      {...baseProps}
      isBuy
      buyToken={{
        name: 'ETH',
        symbol: 'ETH',
        logo: 'eth.png',
        address: '0x...',
      }}
      payingTokens={payingTokens}
      usdAmount="100"
      sellToken={null}
    />
  );

  // Assert TransactionInfo receives correct Buy mode props
  // Assert technical details include payingTokens data
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bfa8546 and 248719b.

📒 Files selected for processing (3)
  • src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1 hunks)
  • src/apps/pulse/hooks/tests/useKeyboardNavigation.test.ts (1 hunks)
  • src/apps/pulse/utils/tests/utils.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/apps/pulse/hooks/tests/useKeyboardNavigation.test.ts (1)
src/apps/pulse/hooks/useKeyboardNavigation.ts (1)
  • useKeyboardNavigation (7-35)
src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (5)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusState (1-5)
src/apps/pulse/hooks/useClickOutside.ts (1)
  • useClickOutside (7-29)
src/apps/pulse/hooks/useKeyboardNavigation.ts (1)
  • useKeyboardNavigation (7-35)
src/apps/pulse/hooks/useTransactionStatus.ts (1)
  • useTransactionStatus (20-53)
src/apps/pulse/hooks/useTechnicalDetails.ts (1)
  • useTechnicalDetails (49-219)
src/apps/pulse/utils/tests/utils.test.tsx (1)
src/apps/pulse/utils/utils.tsx (12)
  • truncateHash (21-24)
  • getStatusColor (29-31)
  • getStatusConfig (36-38)
  • getButtonConfig (43-45)
  • isValidHash (50-52)
  • sanitizeErrorDetails (57-64)
  • formatTimestamp (69-74)
  • formatStepTimestamp (79-94)
  • getStatusInfo (99-110)
  • getStepStatus (115-171)
  • determineFailureStep (176-195)
  • canCloseTransaction (200-204)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (22)
src/apps/pulse/hooks/tests/useKeyboardNavigation.test.ts (10)

1-18: LGTM! Clean test setup.

The imports are appropriate, and the setup/teardown correctly initializes mocks without the previously flagged ineffective cleanup calls.


20-38: LGTM!

Test correctly verifies that the Escape key triggers the onEscape callback when enabled.


40-58: LGTM!

Test correctly verifies that the Enter key triggers the onEnter callback when provided and enabled.


60-77: LGTM!

Test correctly verifies that Enter key does nothing when the onEnter callback is not provided.


79-97: LGTM!

Test correctly verifies that key handlers are inactive when enabled is false.


99-116: LGTM! Default enabled behavior verified.

Test correctly verifies that the hook handles keys when the enabled prop is omitted (defaulting to true). Previous test description issue has been resolved.


118-141: LGTM! Good coverage of ignored keys.

Test correctly verifies that the hook ignores keys other than Escape and Enter, with good coverage of different key types.


166-185: LGTM! Proper cleanup verification.

Test correctly verifies that the hook removes the event listener on unmount and properly restores the spy.


187-218: LGTM! Excellent dynamic behavior test.

Test correctly verifies that the hook properly responds to changes in the enabled prop during rerenders.


220-254: LGTM! Excellent callback update test.

Test correctly verifies that the hook properly responds to callback reference changes during rerenders, ensuring the latest callback is always invoked.

src/apps/pulse/utils/tests/utils.test.tsx (11)

20-43: LGTM!

The truncateHash tests correctly validate both typical hash truncation and edge cases (empty string, dash).


45-61: LGTM!

The getStatusColor tests correctly validate all status-to-color mappings.


63-107: LGTM!

The getStatusConfig tests comprehensively validate icon, styling, and color configurations for all transaction statuses.


109-149: LGTM!

The getButtonConfig tests correctly verify button styling and labels for all transaction statuses.


151-166: LGTM!

The isValidHash tests correctly validate the function's logic (non-empty and not '-'). The test description accurately reflects the behavior following the previous fix.


168-193: LGTM!

The sanitizeErrorDetails tests correctly validate API key redaction, including multiple keys, absence of keys, and empty input.


195-207: LGTM!

The formatTimestamp tests correctly validate both successful formatting and invalid date handling.


229-261: LGTM!

The getStatusInfo tests correctly validate status text and color mappings for all transaction statuses.


263-357: LGTM!

The getStepStatus tests comprehensively validate step status determination across transaction states, buy/sell modes, and resource lock scenarios. The critical paths are well covered.


359-402: LGTM!

The determineFailureStep tests correctly validate failure step identification using mocked step status functions, covering all major failure scenarios.


404-420: LGTM!

The canCloseTransaction tests correctly validate closability logic for all transaction statuses.

src/apps/pulse/components/Transaction/tests/TransactionDetails.test.tsx (1)

148-684: LGTM! Comprehensive test coverage.

The test suite provides excellent coverage of the TransactionDetails component:

✅ Status variations (Starting, Pending, Complete, Failed)
✅ Buy vs Sell modes
✅ User interactions (keyboard, mouse, buttons)
✅ Hook integration with parameter validation
✅ Error states and resource lock failures
✅ Props passing to child components

The test structure is well-organized with clear describe blocks and meaningful test names.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
src/apps/pulse/components/Transaction/TransactionStatusIcon.tsx (2)

33-36: Initial spring config includes unused rotate property.

The initial spring configuration defines rotate: '0deg' but this property is only used in the second useEffect (lines 97-113) for completion/failure states. The heartbeat animation (lines 38-94) only uses scale and filter.

Consider splitting into two separate springs for clarity:

-  const [springs, api] = useSpring(() => ({
-    from: { scale: 1, filter: 'blur(0px)', rotate: '0deg' },
-    to: { scale: 1, filter: 'blur(0px)', rotate: '0deg' },
-  }));
+  const [heartbeatSprings, heartbeatApi] = useSpring(() => ({
+    from: { scale: 1, filter: 'blur(0px)' },
+    to: { scale: 1, filter: 'blur(0px)' },
+  }));
+  
+  const [rotationSprings, rotationApi] = useSpring(() => ({
+    from: { rotate: '0deg', scale: 1 },
+    to: { rotate: '0deg', scale: 1 },
+  }));

Then combine both in the animated.div style prop.


38-94: Deeply nested onRest callbacks create complex animation chain.

The heartbeat animation uses four levels of nested onRest callbacks (lines 46-82), making the control flow difficult to follow and maintain. Each level also checks shouldAnimateRef.current before proceeding.

Consider using async/await with react-spring's promise-based API or extract the sequence into a separate function:

+  const runHeartbeatSequence = async () => {
+    if (!shouldAnimateRef.current) return;
+    
+    // Step 1: bigger + blurry
+    await api.start({ to: { scale: 1.1, filter: 'blur(1px)' }, config: { duration: 100 } });
+    if (!shouldAnimateRef.current) return;
+    
+    // Step 2: back to normal
+    await api.start({ to: { scale: 1, filter: 'blur(0px)' }, config: { duration: 100 } });
+    if (!shouldAnimateRef.current) return;
+    
+    // Step 3: bigger + blurry
+    await api.start({ to: { scale: 1.1, filter: 'blur(1px)' }, config: { duration: 100 } });
+    if (!shouldAnimateRef.current) return;
+    
+    // Step 4: back to normal with longer duration
+    await api.start({ to: { scale: 1, filter: 'blur(0px)' }, config: { duration: 700 } });
+    
+    // Restart if still animating
+    if (shouldAnimateRef.current) {
+      runHeartbeatSequence();
+    }
+  };
+
   useEffect(() => {
     if (shouldAnimate) {
-      const startHeartbeat = () => { /* nested callbacks */ };
-      startHeartbeat();
+      runHeartbeatSequence();
     } else {
       api.stop();
       api.start({ to: { scale: 1, filter: 'blur(0px)' }, config: { duration: 200 } });
     }
   }, [shouldAnimate, api]);
src/apps/pulse/components/Transaction/TransactionStatusButton.tsx (2)

62-92: Extract and deduplicate icon container styling.

The renderIcon function contains three nearly identical circular container divs (lines 65-68, 73-80, 85-88) with only the colors differing. This duplication makes maintenance harder and increases bundle size.

Extract a shared component or helper function:

+const IconContainer: React.FC<{ 
+  borderColor: string; 
+  bgColor: string; 
+  children: React.ReactNode;
+}> = ({ borderColor, bgColor, children }) => (
+  <div 
+    className={`w-[14px] h-[14px] rounded-full border flex items-center justify-center flex-shrink-0`}
+    style={{ 
+      borderColor, 
+      backgroundColor: bgColor 
+    }}
+  >
+    {children}
+  </div>
+);

 const renderIcon = () => {
   if (status === 'Transaction Pending') {
     return (
-      <div className="w-[14px] h-[14px] rounded-full border-[2px] border-[#FFAB36]/[.23] bg-[#FFAB36]/30 flex items-center justify-center flex-shrink-0">
+      <IconContainer borderColor="rgba(255, 171, 54, 0.23)" bgColor="rgba(255, 171, 54, 0.3)">
         <TailSpin color="#FFAB36" height={14} width={14} strokeWidth={6} />
-      </div>
+      </IconContainer>
     );
   }
   // Similar for Complete and Failed cases
 };

30-60: Document linear status transition flow above zoom animation effect.

Add a brief comment before the useEffect in TransactionStatusButton.tsx stating that the status prop is guaranteed to follow the finite state machine
“Starting Transaction → Transaction Pending → Transaction Complete or Transaction Failed,”
so the zoom animation only fires on the two intended transitions.

src/apps/pulse/components/Transaction/TransactionStatus.tsx (4)

92-105: Cleanup return is correct but undefined is unnecessary.

The effect properly cleans up the timeout (line 102), and the explicit return undefined at line 104 is unnecessary since implicit return is equivalent in this context.

Simplify the effect:

   useEffect(() => {
     if (
       (currentStatus === 'Transaction Complete' ||
         currentStatus === 'Transaction Failed') &&
       !showDetails
     ) {
       const timer = setTimeout(() => {
         setShowDetails(true);
       }, 1000);

       return () => clearTimeout(timer);
     }
-    return undefined;
   }, [currentStatus, showDetails]);

60-89: Bounce animation triggers on every showDetails toggle.

The bounce animation effect (lines 60-89) triggers whenever showDetails changes, including both opening and closing the details panel. This may cause unnecessary visual feedback when collapsing details (e.g., pressing Escape while in pending state).

Consider animating only when expanding details:

   useEffect(() => {
+    if (!showDetails) return;
+    
     // Quick bounce effect when size changes
     api.start({
       to: {
         scale: 1.04,
         // ...
       },
       // ...
     });
   }, [showDetails, api]);

Or use a previous value ref to detect the direction of the toggle:

+  const prevShowDetails = useRef(showDetails);
+  
   useEffect(() => {
+    if (showDetails && !prevShowDetails.current) {
       // Bounce animation only when expanding
       api.start({ /* ... */ });
+    }
+    prevShowDetails.current = showDetails;
   }, [showDetails, api]);

153-153: Clarify conditional hash forwarding logic.

The component forwards txHash and completedTxHash conditionally based on isBuy (lines 153, 157):

  • txHash={isBuy ? undefined : blockchainTxHash} (sell flow only)
  • completedTxHash={isBuy ? completedTxHash : undefined} (buy flow only)

This logic implies that buy transactions use completedTxHash while sell transactions use txHash (from blockchainTxHash). However, the reasoning isn't immediately clear from the code.

Add a comment explaining the distinction:

+  // Buy flow: resource lock + completed tx (two-phase)
+  // Sell flow: single blockchain tx (user-op)
   <TransactionDetails
     onDone={/* ... */}
     userOpHash={userOpHash}
     chainId={chainId}
     status={currentStatus}
     isBuy={isBuy}
     sellToken={sellToken}
     buyToken={buyToken}
     tokenAmount={tokenAmount}
     sellOffer={sellOffer}
     payingTokens={payingTokens}
     usdAmount={usdAmount}
     submittedAt={submittedAt}
     pendingCompletedAt={pendingCompletedAt}
-    txHash={isBuy ? undefined : blockchainTxHash}
+    txHash={isBuy ? undefined : blockchainTxHash} // Sell: single tx
     gasFee={gasFee}
     errorDetails={errorDetails}
     resourceLockTxHash={resourceLockTxHash}
-    completedTxHash={isBuy ? completedTxHash : undefined}
+    completedTxHash={isBuy ? completedTxHash : undefined} // Buy: second phase
     resourceLockChainId={resourceLockChainId}
     completedChainId={completedChainId}
     resourceLockCompletedAt={resourceLockCompletedAt}
     isResourceLockFailed={isResourceLockFailed}
   />

Also applies to: 157-157


115-126: Keyboard navigation might not handle all edge cases.

The useKeyboardNavigation hook (lines 115-126) handles Escape key behavior, but doesn't account for Enter key (which the hook supports via the optional onEnter prop). Additionally, focus management isn't explicitly handled when the modal opens or closes.

Consider:

  1. Adding Enter key support to confirm/proceed actions when appropriate
  2. Managing focus trap within the dialog when it's open
  3. Restoring focus to the trigger element when closing

Example enhancement:

   useKeyboardNavigation({
     onEscape: () => {
       if (
         currentStatus === 'Transaction Complete' ||
         currentStatus === 'Transaction Failed'
       ) {
         closeTransactionStatus();
       } else if (currentStatus === 'Transaction Pending' && showDetails) {
         setShowDetails(false);
       }
     },
+    onEnter: () => {
+      // Allow Enter to expand details when in compact view
+      if (!showDetails) {
+        setShowDetails(true);
+      }
+    },
   });

For focus management, consider using a library like react-focus-lock or implementing manual focus trap logic.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 248719b and 3cc7645.

⛔ Files ignored due to path filters (1)
  • src/apps/pulse/components/Transaction/tests/__snapshots__/TransactionStatus.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (3)
  • src/apps/pulse/components/Transaction/TransactionStatus.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionStatusButton.tsx (1 hunks)
  • src/apps/pulse/components/Transaction/TransactionStatusIcon.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/apps/pulse/components/Transaction/TransactionStatusButton.tsx (2)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusState (1-5)
src/apps/pulse/utils/utils.tsx (1)
  • getButtonConfig (43-45)
src/apps/pulse/components/Transaction/TransactionStatusIcon.tsx (2)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusState (1-5)
src/apps/pulse/utils/utils.tsx (1)
  • getStatusConfig (36-38)
src/apps/pulse/components/Transaction/TransactionStatus.tsx (3)
src/apps/pulse/types/types.ts (1)
  • TransactionStatusProps (73-98)
src/apps/pulse/hooks/useClickOutside.ts (1)
  • useClickOutside (7-29)
src/apps/pulse/hooks/useKeyboardNavigation.ts (1)
  • useKeyboardNavigation (7-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
🔇 Additional comments (2)
src/apps/pulse/components/Transaction/TransactionStatusButton.tsx (1)

65-67: Verify TailSpin dimensions match container.

The TailSpin spinner is configured with height={14} width={14} (line 66) to match the 14×14px container, but strokeWidth={6} seems quite thick for such a small spinner and may cause clipping or rendering issues.

Test the spinner visually at this size, or check the react-loader-spinner documentation for recommended strokeWidth values at 14px dimensions. Typical strokeWidth for small spinners is 1-3.

src/apps/pulse/components/Transaction/TransactionStatusIcon.tsx (1)

94-94: Verify react-spring api stability in useEffect deps
Confirm that the api object returned from react-spring is referentially stable before including it in your useEffect dependency arrays (lines 94 & 113). If stability is guaranteed, disable the ESLint warning with an inline comment explaining why; otherwise memoize the API or remove it from the deps.

Comment on lines +115 to +125
const getIcon = () => {
switch (config.icon) {
case 'confirmed':
return ConfirmedIcon;
case 'failed':
return FailedIcon;
case 'pending':
default:
return PendingIcon;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Duplicate icon selection logic with utility function.

The getIcon function (lines 115-125) duplicates icon mapping logic. According to the relevant code snippets, src/apps/pulse/utils/utils.tsx exports a getIcon utility that's used elsewhere.

Import and use the shared getIcon utility from utils instead:

 // utils
-import { getStatusConfig } from '../../utils/utils';
+import { getStatusConfig, getIcon } from '../../utils/utils';

 const TransactionStatusIcon: React.FC<TransactionStatusIconProps> = ({
   status,
 }) => {
   const config = getStatusConfig(status);
+  const iconSrc = getIcon(config.icon);
   // ...
-
-  const getIcon = () => {
-    switch (config.icon) {
-      case 'confirmed':
-        return ConfirmedIcon;
-      case 'failed':
-        return FailedIcon;
-      case 'pending':
-      default:
-        return PendingIcon;
-    }
-  };

   return (
     <animated.div className={config.containerClasses} style={springs}>
-      <img src={getIcon()} alt={status} className={config.iconClasses} />
+      <img src={iconSrc} alt={status} className={config.iconClasses} />
     </animated.div>
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getIcon = () => {
switch (config.icon) {
case 'confirmed':
return ConfirmedIcon;
case 'failed':
return FailedIcon;
case 'pending':
default:
return PendingIcon;
}
};
// src/apps/pulse/components/Transaction/TransactionStatusIcon.tsx
import React from 'react';
import ConfirmedIcon from '../../assets/confirm.svg';
import FailedIcon from '../../assets/fail.svg';
import PendingIcon from '../../assets/pending.svg';
// utils
import { getStatusConfig, getIcon } from '../../utils/utils';
const TransactionStatusIcon: React.FC<TransactionStatusIconProps> = ({
status,
}) => {
const config = getStatusConfig(status);
const iconSrc = getIcon(config.icon);
- const getIcon = () => {
- switch (config.icon) {
- case 'confirmed':
- return ConfirmedIcon;
- case 'failed':
- return FailedIcon;
- case 'pending':
- default:
- return PendingIcon;
- }
- };
return (
<animated.div className={config.containerClasses} style={springs}>
<img src={iconSrc} alt={status} className={config.iconClasses} />
</animated.div>
);
};
export default TransactionStatusIcon;
🤖 Prompt for AI Agents
In src/apps/pulse/components/Transaction/TransactionStatusIcon.tsx around lines
115 to 125, the local getIcon function duplicates logic that already exists in
src/apps/pulse/utils/utils.tsx; remove the local getIcon implementation, add an
import for the shared getIcon utility from src/apps/pulse/utils/utils.tsx, and
replace all uses of the local function with the imported utility (adjust any
naming or typing if there's a conflict). Ensure the imported function signature
matches how this component calls it and update any associated types/props
imports if needed.

@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) October 1, 2025 08:26 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
src/apps/pulse/components/App/HomeScreen.tsx (5)

39-68: Consider extracting types to shared types file.

These transaction status types (TransactionStatusState, BidStatus, ResourceLockStatus, UserOpStatus) appear to be domain types that could be reused across components. According to the AI summary, these types are used via shared utilities and the TransactionStatus component.

Consider moving these to src/apps/pulse/types/types.ts for better reusability and maintainability:

// In src/apps/pulse/types/types.ts
export type TransactionStatusState =
  | 'Starting Transaction'
  | 'Transaction Pending'
  | 'Transaction Complete'
  | 'Transaction Failed';

export type BidStatus =
  | 'PENDING'
  | 'SHORTLISTING_INITIATED'
  | 'SHORTLISTED'
  | 'EXECUTED'
  | 'CLAIMED'
  | 'RESOURCE_LOCK_RELEASED'
  | 'FAILED_EXECUTION'
  | 'SHORTLISTING_FAILED';

export type ResourceLockStatus =
  | 'PENDING_USER_OPS_CREATION'
  | 'USER_OPS_CREATION_INITIATED'
  | 'USER_OPS_EXECUTION_SUCCESSFUL'
  | 'USER_OPS_EXECUTION_FAILED';

export type UserOpStatus =
  | 'New'
  | 'Pending'
  | 'Submitted'
  | 'OnChain'
  | 'Finalized'
  | 'Cancelled'
  | 'Reverted';

Then import them in this file:

+import {
+  TransactionStatusState,
+  BidStatus,
+  ResourceLockStatus,
+  UserOpStatus,
+} from '../../types/types';
-
-// types
-type TransactionStatusState =
-  | 'Starting Transaction'
-  | 'Transaction Pending'
-  | 'Transaction Complete'
-  | 'Transaction Failed';
-
-type BidStatus =
-  | 'PENDING'
-  | 'SHORTLISTING_INITIATED'
-  | 'SHORTLISTED'
-  | 'EXECUTED'
-  | 'CLAIMED'
-  | 'RESOURCE_LOCK_RELEASED'
-  | 'FAILED_EXECUTION'
-  | 'SHORTLISTING_FAILED';
-
-type ResourceLockStatus =
-  | 'PENDING_USER_OPS_CREATION'
-  | 'USER_OPS_CREATION_INITIATED'
-  | 'USER_OPS_EXECUTION_SUCCESSFUL'
-  | 'USER_OPS_EXECUTION_FAILED';
-
-type UserOpStatus =
-  | 'New'
-  | 'Pending'
-  | 'Submitted'
-  | 'OnChain'
-  | 'Finalized'
-  | 'Cancelled'
-  | 'Reverted';

124-161: Consider consolidating transaction status state.

This component manages 17+ transaction status-related state variables. For better maintainability, consider consolidating related state into a single object managed by useReducer or extracting the polling logic into a custom hook (e.g., useTransactionStatusPolling).

Example with useReducer:

type TransactionPollingState = {
  currentStatus: TransactionStatusState;
  errorDetails: string;
  submittedAt?: Date;
  pendingCompletedAt?: Date;
  blockchainTxHash?: string;
  resourceLockTxHash?: string;
  completedTxHash?: string;
  completedChainId?: number;
  resourceLockChainId?: number;
  resourceLockCompletedAt?: Date;
  isResourceLockFailed: boolean;
  isPollingActive: boolean;
  statusStartTime: number;
  hasSeenSuccess: boolean;
  failureTimeoutId: ReturnType<typeof setTimeout> | null;
  isBackgroundPolling: boolean;
  shouldAutoReopen: boolean;
};

const [pollingState, dispatch] = useReducer(pollingReducer, initialState);

Or extract to a custom hook:

const {
  currentStatus,
  errorDetails,
  // ... other values
  stopPolling,
} = useTransactionStatusPolling({
  userOpHash,
  transactionData,
  intentSdk,
});

317-338: Consider extracting status mapping to shared utility.

The mapping logic is correct, but according to the AI summary, mapBidStatusToTransactionStatus is used via shared utilities (src/apps/pulse/utils/utils.tsx). Consider moving this function there for reusability across components.

Move to src/apps/pulse/utils/utils.tsx:

export const mapBidStatusToTransactionStatus = (
  status: BidStatus
): TransactionStatusState => {
  // ... same implementation
};

341-357: Consider extracting status mapping to shared utility.

Similar to mapBidStatusToTransactionStatus, this function should be moved to a shared utility file for reusability, as indicated by the AI summary.

Move to src/apps/pulse/utils/utils.tsx:

export const mapUserOpStatusToTransactionStatus = (
  status: UserOpStatus
): TransactionStatusState => {
  // ... same implementation
};

548-627: Add validation for Intent SDK API responses.

The Buy flow accesses nested properties from Intent SDK responses without validating the structure beyond optional chaining. The type cast at line 570 (as ResourceLockStatus) assumes the API returns valid status strings without validation.

Add validation before type casting:

 const resourceLockStatus: ResourceLockStatus | undefined =
-  resourceLock?.resourceLockInfo?.status as ResourceLockStatus;
+  resourceLock?.resourceLockInfo?.status &&
+  ['PENDING_USER_OPS_CREATION', 'USER_OPS_CREATION_INITIATED', 
+   'USER_OPS_EXECUTION_SUCCESSFUL', 'USER_OPS_EXECUTION_FAILED'].includes(
+    resourceLock.resourceLockInfo.status
+  )
+    ? (resourceLock.resourceLockInfo.status as ResourceLockStatus)
+    : undefined;

Similarly for bidStatus:

 const bidStatus: BidStatus | undefined = bid?.bidStatus;
+// Add validation if bidStatus doesn't already match the BidStatus type
+const validBidStatuses = ['PENDING', 'SHORTLISTING_INITIATED', 'SHORTLISTED', 
+  'EXECUTED', 'CLAIMED', 'RESOURCE_LOCK_RELEASED', 
+  'FAILED_EXECUTION', 'SHORTLISTING_FAILED'];
+if (bidStatus && !validBidStatuses.includes(bidStatus)) {
+  console.warn('Unexpected bid status:', bidStatus);
+  return;
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc7645 and a8a1cbc.

📒 Files selected for processing (1)
  • src/apps/pulse/components/App/HomeScreen.tsx (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/apps/pulse/components/App/HomeScreen.tsx (4)
src/apps/pulse/hooks/useIntentSdk.ts (1)
  • useIntentSdk (9-53)
src/apps/pulse/types/tokens.ts (1)
  • SelectedToken (1-10)
src/apps/pulse/hooks/useRelaySell.ts (1)
  • SellOffer (37-43)
src/services/userOpStatus.ts (1)
  • getUserOperationStatus (3-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
🔇 Additional comments (5)
src/apps/pulse/components/App/HomeScreen.tsx (5)

165-179: Token amount calculation looks correct.

The calculation logic correctly mirrors PreviewBuy's approach (totalPay / tokenUsdValue), and the dependencies are appropriate since payingTokens reference changes trigger the effect when needed.


244-254: LGTM! Preview close handlers correctly stop polling.

Both handlers appropriately call stopTransactionPolling() to clean up polling state when previews are closed.


301-314: Background polling logic looks good.

The function correctly handles both final and intermediate states: stopping polling for final states and enabling background polling with auto-reopen for intermediate states.


496-529: Step completion tracking logic looks correct.

The effect properly tracks completion timestamps for each transaction step, setting them only once using the || operator pattern. Dependencies are appropriate.


763-797: TransactionStatus props wiring looks comprehensive.

All required props are correctly passed, including the fixed chainId logic that uses buyToken.chainId for Buy transactions and sellToken.chainId for Sell transactions.

@github-actions github-actions bot temporarily deployed to Preview (feat/PRO-3681/transaction-status-pulse) October 1, 2025 08:34 Inactive
@RanaBug RanaBug merged commit d855159 into staging Oct 1, 2025
6 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Nov 12, 2025
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants