Skip to content

fix(demo-wallet): loading skeletons, error states, offset pagination, empty-state stubs#486

Open
lesyuk wants to merge 2 commits into
mainfrom
fix/loading-states
Open

fix(demo-wallet): loading skeletons, error states, offset pagination, empty-state stubs#486
lesyuk wants to merge 2 commits into
mainfrom
fix/loading-states

Conversation

@lesyuk

@lesyuk lesyuk commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Mobile-web layout, safe-area, viewport-height and bottom-sheet keyboard/scroll fixes for the wallet demo. Every change is behind a mobile breakpoint or an opt-in prop; desktop and the extension popup are unchanged.

Motivation / Bug

On-device testing (iOS Safari + Android Chrome) surfaced several mobile-only issues:

  • Clipped bottom actions. Continue / Reset Wallet / the welcome terms line sat under the Android 3-button navigation bar. The shared CenteredScreen footer used a fixed pb-6; Android 3-button mode reports env(safe-area-inset-bottom)=0 while its ~48dp bar still overlaps the viewport, and env() didn't resolve at all without viewport-fit=cover.
  • Short viewport on iOS. The dashboard used 100vh, which excludes iOS Safari's dynamic toolbars, leaving a gap.
  • Top nav / back button vanished under sheets. Opening a bottom sheet (TON Connect request, wallet switcher, settings) over a scrolled page made the sticky top bar drop out for the sheet's lifetime and snap back late. Root cause: vaul's iOS scroll-lock sets body { position: fixed; top: -scrollY }; a position: sticky header is anchored to its scroll container, and a fixed <body> is no longer that container, so the header detaches.
  • Fluctuating top gap. The header's top pad used env(safe-area-inset-top), which in a browser tab (not a standalone PWA) tracks the address bar showing/hiding — a large, moving gap on both platforms.
  • On-screen Return key did nothing on the password screens (no <form>, so iOS Safari's Return never submitted).
  • Connect-to-dApp sheet keyboard misbehavior. vaul's repositionInputs mutates the sheet height/offset from visualViewport resize events: the sheet flew off-screen on the first iOS focus and got shoved up on Android; the dashboard could also bleed through above it.

What changed

  • Safe-area & viewportviewport-fit=cover in index.html; the CenteredScreen footer reserves max(3rem, 1.5rem + env(safe-area-inset-bottom)); the dashboard layout switched to min-h-dvh.
  • PinnedHeader (apps/demo-wallet/src/core/components/shared/pinned-header/) — new top bar: position: fixed on mobile with a ResizeObserver-measured flow spacer, md:sticky on desktop; a small fixed top pad instead of the top inset. NewLayout and the onboarding back button use it.
  • Return key (setup-password-screen, unlock-screen) — inputs wrapped in a <form> whose onSubmit mirrors the primary button (same validity guard), enterKeyHint set, plus a visually-hidden submit button (what actually makes iOS Safari's Return submit).
  • Connect-to-dApp sheet (modal.tsx, connect-dapp-modal.tsx, is-ios.ts) — a keyboardSafe opt-in: disable vaul repositionInputs on both platforms and lift the compact sheet with a manual visualViewport-based bottom offset (useKeyboardInset) so the input and Connect button rest directly above the keyboard. Input-less sheets set noBodyStyles so the scroll-lock can't break the now-fixed header; the iOS input sheet keeps the lock and restores scroll on close.

Why this approach

  • Fixed vs sticky header: a viewport-anchored (fixed) header is the only thing immune to vaul's position:fixed body lock; sticky provably detaches. Desktop keeps sticky because Radix Dialog locks scroll via overflow:hidden (not a fixed body), so the problem doesn't occur there and we avoid a fixed-vs-flow reflow on the wide layout.
  • Manual keyboard lift over vaul's repositionInputs: repositionInputs is what caused the fly-off/shove; a bottom offset is orthogonal to vaul's transform-based open/close/drag, so the two don't fight, and the resize stream supplies smooth intermediate heights.
  • Hidden submit button: the minimal, standards-based way to get iOS Safari's Return to submit without duplicating submit logic.
  • Fixed top pad over env(safe-area-inset-top): that inset is address-bar-coupled in a browser tab, which is exactly the fluctuation observed.

How it was verified

  • pnpm --filter demo-wallet typecheck (tsc, clean).
  • Manual verification on desktop (macOS, Chrome) and on the same iOS Safari / Android Chrome devices where the issues were originally reported.

Scope / safety

Every change is mobile-only (breakpoint- or visualViewport/env()-guarded) or opt-in (keyboardSafe). The desktop Dialog path and the extension popup entry are untouched. No store or business logic changed.

Follow-ups

Related: #485 (demo-wallet e2e). It should be rebased on top of these fixes before it merges.

Summary by CodeRabbit

  • New Features

    • Added reusable loading skeletons across assets, NFTs, and transaction views for a smoother loading experience.
    • Expanded asset display to include selected mainnet tokens even when the wallet has no balance.
    • Improved transaction history with pagination and a “Load more” option.
  • Bug Fixes

    • Added clearer empty and error states for assets, NFTs, and transactions.
    • Reduced duplicate event loading and improved handling of partial or repeated fetches.

… empty-state stubs

Sections (History, Assets, NFTs) previously shimmered forever on a failed list
fetch and could flash a false "empty" while still loading. Add explicit
loading / error / empty handling backed by real load state.

Store (wallet-core):
- Add isLoadingEvents / eventsLoaded / eventsError / accountStatus to
  walletManagement, driven from loadEvents.
- Coalesce concurrent loadEvents calls for the same (limit, offset) onto one
  in-flight request (removes the duplicate concurrent /traces requests that the
  StrictMode double-invoke amplified); a different window (Load more) chains
  after and is never dropped.
- Best-effort single retry on a transient toncenter timeout.
- Fetch account status (addressInformation) so the UI can distinguish a
  fresh/uninitialized account (truly no transactions) from a still-loading or
  failed fetch.
- Reset the new flags + in-flight guard on wallet switch / remove / clear.

UI (demo-wallet):
- Add a reusable animated Skeleton primitive (gray shimmer within the app
  palette, respects prefers-reduced-motion) with layout-matched variants for the
  Asset row, NFT tile and History row.
- Show shimmers while loading on both the dashboard preview cards and the full
  Assets / NFTs / History pages.
- Empty-state stubs ("No NFTs yet" / "No transactions yet") show only after a
  successful load returned nothing (or a confirmed fresh account) — never while
  loading or after a failed fetch. On the dashboard a genuinely-empty wallet
  shows the stub instead of the section being hidden.
- Error states: the dashboard preview hides the section on a load error; the
  full page shows a short "Unable to load ..." line instead of an endless shimmer.
- History "Load more" pages by offset -- (25,0) -> (25,25) -> (25,50) --
  appending each page to the accumulated list (deduped by eventId) instead of
  refetching a growing limit; shows a spinner while the next page loads.

Work around a server-side toncenter limitation where
GET /api/v3/traces?limit=N returns 500 ("timeout: context deadline exceeded")
for small N (observed 1..10) but succeeds for 25: default the events batch to 25
everywhere. Because the dashboard preview and History now request the same first
page (limit 25, offset 0), the store coalesces them into one /traces request.

Assets: surface the mainnet base tokens (native Gram + USDT + XAUt) at a zero
balance when not held, on BOTH the dashboard preview and the full Assets page,
via the shared useAssetRows hook so both render the exact same list (the preview
shows its first N); held jettons are merged in and win on any address collision.
Mainnet-only; testnet and held-token display unchanged.
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
appkit-minter Ready Ready Preview, Comment Jul 4, 2026 4:29pm
appkit-template Ready Ready Preview, Comment Jul 4, 2026 4:29pm
kit-demo-wallet Ready Ready Preview, Comment Jul 4, 2026 4:29pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@lesyuk, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5c473dd-3a41-4c88-b0f0-72422ac5abe6

📥 Commits

Reviewing files that changed from the base of the PR and between 98d0859 and 38739b5.

📒 Files selected for processing (2)
  • apps/demo-wallet/src/features/transactions/components/transaction-history/transaction-history.tsx
  • apps/demo-wallet/src/features/transactions/hooks/use-transaction-rows.ts
📝 Walkthrough

Walkthrough

Adds a reusable Skeleton UI component and shimmer CSS, applying it to asset, NFT, and transaction row placeholders with explicit loading/empty/error state branching. Extends useAssetRows with default mainnet jettons. Reworks wallet-core's transaction events loading with pagination, request coalescing, account-status tracking, and richer hook state.

Changes

Loading Skeleton UI Components

Layer / File(s) Summary
Skeleton primitive and shimmer CSS
apps/demo-wallet/src/App.css, apps/demo-wallet/src/core/components/ui/skeleton/index.ts, apps/demo-wallet/src/core/components/ui/skeleton/skeleton.tsx
Adds shimmer keyframes/CSS class with reduced-motion handling, and a reusable Skeleton component rendering an aria-hidden placeholder div.
Asset rows and dashboard integration
apps/demo-wallet/src/features/assets/components/asset-row/asset-row.tsx, .../assets-screen/assets-screen.tsx, .../hooks/use-asset-rows.ts, .../dashboard-assets/dashboard-assets.tsx
Uses Skeleton in asset row placeholders, adds an "Unable to load assets" error message, fetches/merges default mainnet jettons in useAssetRows, and simplifies DashboardAssets to slice jettonRows with error hiding.
NFT skeleton states
.../nft-screen/nft-screen.tsx, .../nft-tile/nft-tile.tsx, .../nfts-card/nfts-card.tsx
Adds NftTileSkeleton and branches NFT rendering between grid, "Unable to load NFTs" error, "No NFTs yet" empty state, and skeleton placeholders.
Transaction skeleton and pagination UI
.../history-screen/history-screen.tsx, .../transaction-history/transaction-history.tsx, .../transaction-row/transaction-row.tsx
Adds TransactionRowSkeleton and updates screens to consume the extended useTransactionRows hook, rendering rows, error/empty text, skeletons, and a loadMore-driven pagination button.

Estimated code review effort: 4 (Complex) | ~60 minutes

Transaction Events Pagination and State

Layer / File(s) Summary
Store types
demo/wallet-core/src/types/store.ts
Adds AccountStatus import and optional accountStatus field to WalletManagementSlice.walletManagement.
useTransactionRows hook
apps/demo-wallet/src/features/transactions/hooks/use-transaction-rows.ts
Adds EVENTS_PAGE_SIZE, pageSize parameter, and derived isLoading/showEmpty/isError/loadMore state.
walletManagementSlice loadEvents rework
demo/wallet-core/src/store/slices/walletManagementSlice.ts
Adds request coalescing keyed by (limit, offset), timeout retry, offset dedupe, confirmed-trace/hash tracking, pending-transaction filtering, and event/account-status resets on wallet switch/remove/clear.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HistoryScreen
  participant useTransactionRows
  participant walletManagementSlice
  participant apiClient

  HistoryScreen->>useTransactionRows: mount / pageSize change
  useTransactionRows->>walletManagementSlice: loadEvents(pageSize, 0)
  walletManagementSlice->>apiClient: getEvents(limit, offset)
  apiClient-->>walletManagementSlice: events or timeout
  walletManagementSlice-->>useTransactionRows: rows, isLoadingEvents, eventsError
  useTransactionRows-->>HistoryScreen: rows, hasMore, isError, showEmpty, loadMore
  HistoryScreen-->>HistoryScreen: render rows, error, empty, or skeleton
  HistoryScreen->>useTransactionRows: loadMore() on click
  useTransactionRows->>walletManagementSlice: loadEvents(pageSize, events.length)
Loading

Suggested reviewers: TrueCarry

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main demo-wallet changes: skeleton loading states, error/empty states, and offset-based pagination.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/loading-states

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.

prettier/prettier fixes flagged by CI lint (formatting only, no behavior change):
transaction-history skeleton map and use-transaction-rows early-return.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
apps/demo-wallet/src/features/nft/components/nft-screen/nft-screen.tsx (1)

44-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider announcing the loading state to assistive tech.

Skeleton renders aria-hidden placeholders, so screen-reader users get no indication the grid is loading. A role="status"/visually-hidden "Loading NFTs…" text on this branch would close that gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/demo-wallet/src/features/nft/components/nft-screen/nft-screen.tsx`
around lines 44 - 50, The loading branch in NftScreen currently only renders
aria-hidden skeleton tiles, so assistive tech gets no loading cue. Update the
conditional fallback in nft-screen.tsx to announce the loading state by adding a
status region (for example, a role="status" wrapper with visually hidden
“Loading NFTs…” text) alongside the existing NftTileSkeleton grid so
screen-reader users are informed when NFTs are loading.
apps/demo-wallet/src/features/assets/components/assets-screen/assets-screen.tsx (1)

26-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate isError logic — hoist into useAssetRows.

This exact expression is duplicated in dashboard-assets.tsx (Line 38). Since useAssetRows already exists as the single source of truth shared by both surfaces, computing isError there once (alongside assetsReady) would prevent the two call sites from silently diverging if the rule changes.

♻️ Suggested consolidation
 interface AssetRows {
     tonRow: AssetRowData | null;
     jettonRows: AssetRowData[];
     assetsReady: boolean;
+    isError: boolean;
 }

 export const useAssetRows = (): AssetRows => {
-    const { userJettons, lastJettonsUpdate } = useJettons();
+    const { userJettons, lastJettonsUpdate, isLoadingJettons, error } = useJettons();
     ...
+    const isError = lastJettonsUpdate === 0 && userJettons.length === 0 && error !== null && !isLoadingJettons;
     ...
-    return { tonRow, jettonRows, assetsReady };
+    return { tonRow, jettonRows, assetsReady, isError };
 };

Then both AssetsScreen and DashboardAssets consume isError from the hook instead of recomputing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/demo-wallet/src/features/assets/components/assets-screen/assets-screen.tsx`
around lines 26 - 31, Hoist the duplicated `isError` computation out of
`AssetsScreen` and into `useAssetRows`, since that hook is already the shared
source of truth for asset state. Update `useAssetRows` to derive and return
`isError` alongside `assetsReady`, using the same jettons/error conditions
currently computed in `assets-screen.tsx`, then change both `AssetsScreen` and
`DashboardAssets` to consume `isError` from the hook instead of recalculating it
so the rule stays consistent in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/demo-wallet/src/features/nft/components/nft-screen/nft-screen.tsx`:
- Around line 23-30: Extract the duplicated NFT view-state derivation into a
shared helper so `nft-screen.tsx` and `nfts-card.tsx` do not drift apart. Move
the `hasNfts`, `showEmpty`, and `isError` logic currently computed from
`useNfts()` into a reusable hook or nearby utility such as `useNftViewState`,
and have both components consume that shared result instead of inlining the same
conditions and comments. Keep the behavior unchanged while centralizing the
loading/empty/error semantics in one place.

In
`@apps/demo-wallet/src/features/transactions/components/history-screen/history-screen.tsx`:
- Around line 28-52: The first-page error state in HistoryScreen has no recovery
path, since loadMore is blocked by hasMore and the empty/error branch only
renders text. Update useTransactionRows to expose a retry or reload action for
the initial fetch, and wire that into the isError branch of HistoryScreen as a
button. Keep the existing loadMore behavior for pagination, but ensure the new
retry path can re-run the zero-offset fetch even when hasMore is false.

In
`@apps/demo-wallet/src/features/transactions/components/transaction-history/transaction-history.tsx`:
- Around line 57-59: Prettier is failing on the skeleton row rendering in
transaction-history because the Array.from map body is wrapped in unnecessary
parentheses. Update the TransactionHistory component’s SKELETON_ROWS mapping so
the TransactionRowSkeleton JSX is inlined directly in the map callback body,
matching the formatter’s preferred style and removing the wrapping parentheses.

In `@apps/demo-wallet/src/features/transactions/hooks/use-transaction-rows.ts`:
- Around line 142-145: The showEmpty boolean expression in useTransactionRows
needs to be reformatted to satisfy Prettier and unblock the unit check. Update
the layout of the conditional in use-transaction-rows.ts so the expression is
wrapped consistently with the formatter’s style, keeping the logic in showEmpty
unchanged and ensuring the file matches the output of the project formatter.

In `@demo/wallet-core/src/store/slices/walletManagementSlice.ts`:
- Around line 868-905: The events update block in the walletManagementSlice
loadEvents flow is missing the wallet-address guard, so a stale request can
write previous-wallet data after switchWallet. Add the same
s.walletManagement.address === address check used for accountStatus around the
set(...) logic that mutates events, confirmedTraceIds, confirmedExternalHashes,
pendingTransactions, and eventsError, and only apply the response when the
active wallet still matches the request address.

---

Nitpick comments:
In
`@apps/demo-wallet/src/features/assets/components/assets-screen/assets-screen.tsx`:
- Around line 26-31: Hoist the duplicated `isError` computation out of
`AssetsScreen` and into `useAssetRows`, since that hook is already the shared
source of truth for asset state. Update `useAssetRows` to derive and return
`isError` alongside `assetsReady`, using the same jettons/error conditions
currently computed in `assets-screen.tsx`, then change both `AssetsScreen` and
`DashboardAssets` to consume `isError` from the hook instead of recalculating it
so the rule stays consistent in one place.

In `@apps/demo-wallet/src/features/nft/components/nft-screen/nft-screen.tsx`:
- Around line 44-50: The loading branch in NftScreen currently only renders
aria-hidden skeleton tiles, so assistive tech gets no loading cue. Update the
conditional fallback in nft-screen.tsx to announce the loading state by adding a
status region (for example, a role="status" wrapper with visually hidden
“Loading NFTs…” text) alongside the existing NftTileSkeleton grid so
screen-reader users are informed when NFTs are loading.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a739034-1770-41aa-9d7f-73001d018b43

📥 Commits

Reviewing files that changed from the base of the PR and between b2686a6 and 98d0859.

📒 Files selected for processing (16)
  • apps/demo-wallet/src/App.css
  • apps/demo-wallet/src/core/components/ui/skeleton/index.ts
  • apps/demo-wallet/src/core/components/ui/skeleton/skeleton.tsx
  • apps/demo-wallet/src/features/assets/components/asset-row/asset-row.tsx
  • apps/demo-wallet/src/features/assets/components/assets-screen/assets-screen.tsx
  • apps/demo-wallet/src/features/assets/hooks/use-asset-rows.ts
  • apps/demo-wallet/src/features/dashboard/components/dashboard-assets/dashboard-assets.tsx
  • apps/demo-wallet/src/features/nft/components/nft-screen/nft-screen.tsx
  • apps/demo-wallet/src/features/nft/components/nft-tile/nft-tile.tsx
  • apps/demo-wallet/src/features/nft/components/nfts-card/nfts-card.tsx
  • apps/demo-wallet/src/features/transactions/components/history-screen/history-screen.tsx
  • apps/demo-wallet/src/features/transactions/components/transaction-history/transaction-history.tsx
  • apps/demo-wallet/src/features/transactions/components/transaction-row/transaction-row.tsx
  • apps/demo-wallet/src/features/transactions/hooks/use-transaction-rows.ts
  • demo/wallet-core/src/store/slices/walletManagementSlice.ts
  • demo/wallet-core/src/types/store.ts

Comment on lines +23 to +30
const { userNfts, formatNftIndex, isLoadingNfts, lastNftsUpdate, error } = useNfts();

const hasNfts = userNfts.length > 0;
// "No NFTs yet" is truthful once a load has succeeded and returned nothing (even if a later
// background refresh then errored). "Couldn't load" is only when no load ever succeeded and
// the fetch failed — otherwise the first load is still running (shimmer).
const showEmpty = !hasNfts && lastNftsUpdate > 0;
const isError = !hasNfts && lastNftsUpdate === 0 && error !== null && !isLoadingNfts;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate state-derivation logic — extract to a shared hook/utility.

The hasNfts/showEmpty/isError derivation (with identical inline comments) is copy-pasted verbatim in nfts-card.tsx. Extracting this into a shared hook (e.g. useNftViewState(useNfts())) would prevent the two call sites from silently diverging as the loading semantics evolve.

As per coding guidelines, "Extract utility functions to a nearby utils file and types to a types file when it helps."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/demo-wallet/src/features/nft/components/nft-screen/nft-screen.tsx`
around lines 23 - 30, Extract the duplicated NFT view-state derivation into a
shared helper so `nft-screen.tsx` and `nfts-card.tsx` do not drift apart. Move
the `hasNfts`, `showEmpty`, and `isError` logic currently computed from
`useNfts()` into a reusable hook or nearby utility such as `useNftViewState`,
and have both components consume that shared result instead of inlining the same
conditions and comments. Keep the behavior unchanged while centralizing the
loading/empty/error semantics in one place.

Source: Coding guidelines

Comment on lines +28 to 52
const { rows, hasMore, isLoading, showEmpty, isError, loadMore } = useTransactionRows();

// With rows already on screen, `isLoading` means the next offset page is being fetched.
const isLoadingMore = isLoading && rows.length > 0;

return (
<NewLayout header={<ScreenHeader title="History" onBack={() => navigate('/wallet')} />}>
{rows.length === 0 ? (
<p className="py-12 text-center text-sm text-gray-400">No transactions yet</p>
) : (
{rows.length > 0 ? (
<div className="space-y-1">
{rows.map((row) => (
<TransactionRow key={row.id} {...row} />
))}
</div>
) : isError ? (
<p className="py-12 text-center text-sm text-gray-400">Unable to load transactions</p>
) : showEmpty ? (
<p className="py-12 text-center text-sm text-gray-400">No transactions yet</p>
) : (
// Loading first page: show a shimmer, never a false "empty".
<div className="space-y-1">
{Array.from({ length: SKELETON_ROWS }).map((_, index) => (
<TransactionRowSkeleton key={index} />
))}
</div>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No retry path when the first-page fetch fails.

When isError is true, the screen shows "Unable to load transactions" with no way to recover short of leaving and re-entering the page. loadMore can't be reused for this: its guard requires hasMore, which is still false (the initial default) since a failed first-page fetch never reaches the hasNextEvents assignment in loadEvents. The "Load more" button itself is also hidden here since it's gated by rows.length > 0 (line 54), which is false in the error state.

Consider exposing a retry (or reusing a zero-offset reload) from useTransactionRows and wiring a "Retry" button into the isError branch.

💡 Suggested addition (sketch)
             ) : isError ? (
-                <p className="py-12 text-center text-sm text-gray-400">Unable to load transactions</p>
+                <div className="py-12 text-center">
+                    <p className="text-sm text-gray-400">Unable to load transactions</p>
+                    <Button variant="secondary" size="sm" className="mt-3" onClick={retry}>
+                        Retry
+                    </Button>
+                </div>
             ) : showEmpty ? (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/demo-wallet/src/features/transactions/components/history-screen/history-screen.tsx`
around lines 28 - 52, The first-page error state in HistoryScreen has no
recovery path, since loadMore is blocked by hasMore and the empty/error branch
only renders text. Update useTransactionRows to expose a retry or reload action
for the initial fetch, and wire that into the isError branch of HistoryScreen as
a button. Keep the existing loadMore behavior for pagination, but ensure the new
retry path can re-run the zero-offset fetch even when hasMore is false.

Comment thread apps/demo-wallet/src/features/transactions/hooks/use-transaction-rows.ts Outdated
Comment on lines +868 to 905
set((s) => {
// offset 0 = fresh first page (replace). offset > 0 = "Load more" page:
// append to what's already loaded and dedupe by eventId so a page overlap
// (e.g. a new tx shifting the window) can't produce duplicate rows.
if (offset > 0) {
const existing = s.walletManagement.events as Array<{ eventId?: string }>;
const seenEventIds = new Set<string>(
existing.map((ev) => ev.eventId).filter((id): id is string => !!id),
);
const appended = (response.events as Array<{ eventId?: string }>).filter(
(ev) => !ev.eventId || !seenEventIds.has(ev.eventId),
);
s.walletManagement.events = [...existing, ...appended];
} else {
s.walletManagement.events = response.events;
}
s.walletManagement.hasNextEvents = response.hasNext;
const eventTraceIds = new Set<string>();
const eventExtHashes = new Set<string>();
for (const ev of response.events as Array<{ eventId?: string; traceExternalHash?: string }>) {
if (ev.eventId) eventTraceIds.add(ev.eventId);
if (ev.traceExternalHash) eventExtHashes.add(Base64ToHex(ev.traceExternalHash));
}
state.walletManagement.confirmedTraceIds = [
...state.walletManagement.confirmedTraceIds,
s.walletManagement.confirmedTraceIds = [
...s.walletManagement.confirmedTraceIds,
...eventTraceIds,
].slice(-50);
state.walletManagement.confirmedExternalHashes = [
...state.walletManagement.confirmedExternalHashes,
s.walletManagement.confirmedExternalHashes = [
...s.walletManagement.confirmedExternalHashes,
...eventExtHashes,
].slice(-50);
state.walletManagement.pendingTransactions = state.walletManagement.pendingTransactions.filter(
s.walletManagement.pendingTransactions = s.walletManagement.pendingTransactions.filter(
(p) =>
!(p.traceId && eventTraceIds.has(p.traceId)) &&
!(p.externalHash && eventExtHashes.has(p.externalHash)),
);
s.walletManagement.eventsError = false;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and inspect the relevant region
ast-grep outline demo/wallet-core/src/store/slices/walletManagementSlice.ts --view expanded || true
wc -l demo/wallet-core/src/store/slices/walletManagementSlice.ts
sed -n '800,940p' demo/wallet-core/src/store/slices/walletManagementSlice.ts

# Find all address guards and wallet-switch related logic in the slice
rg -n "address !== address|inFlightEventsLoad|switchWallet|loadEvents|getEvents|confirmedTraceIds|confirmedExternalHashes|pendingTransactions" demo/wallet-core/src/store/slices/walletManagementSlice.ts

Repository: ton-org/kit

Length of output: 10825


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '295,385p' demo/wallet-core/src/store/slices/walletManagementSlice.ts
sed -n '520,590p' demo/wallet-core/src/store/slices/walletManagementSlice.ts
sed -n '680,730p' demo/wallet-core/src/store/slices/walletManagementSlice.ts

Repository: ton-org/kit

Length of output: 9605


Guard the events write against wallet switches. switchWallet clears inFlightEventsLoad, but an already-running loadEvents can still resolve after the new wallet is active and repopulate events, confirmedTraceIds, confirmedExternalHashes, and pendingTransactions with the previous wallet’s data. Add the same s.walletManagement.address === address check used for accountStatus.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@demo/wallet-core/src/store/slices/walletManagementSlice.ts` around lines 868
- 905, The events update block in the walletManagementSlice loadEvents flow is
missing the wallet-address guard, so a stale request can write previous-wallet
data after switchWallet. Add the same s.walletManagement.address === address
check used for accountStatus around the set(...) logic that mutates events,
confirmedTraceIds, confirmedExternalHashes, pendingTransactions, and
eventsError, and only apply the response when the active wallet still matches
the request address.

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.

1 participant