Skip to content

feat(widget): filter eligible tokens by yield category#541

Open
petar-omni wants to merge 2 commits into
mainfrom
feat/eligible-tokens-only
Open

feat(widget): filter eligible tokens by yield category#541
petar-omni wants to merge 2 commits into
mainfrom
feat/eligible-tokens-only

Conversation

@petar-omni

@petar-omni petar-omni commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

Release Notes

  • New Features

    • Added pagination support for token selection on the earn page, enabling users to load more tokens on demand.
    • Enhanced default-token loading to support yield-category filtering.
    • Added support for withdrawal request and instant withdrawal action flows.
  • Bug Fixes

    • Improved KYC URL resolution to use the correct authorize URL field when available.
  • Localization

    • Updated English and French translations for the new withdrawal request/instant withdrawal states and labels.

@changeset-bot

changeset-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 208fc99

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replaces single-page legacy token fetching with an infinite-paginated useDefaultTokens hook driven by DashboardYieldCategory, threads the category through the earn-page hook chain and context, adds a tokenBalance/select action with load-more pagination controls to the token selector UI, migrates KYC URL resolution from kycUrl to authorizeUrl, and adds WITHDRAW_REQUEST/INSTANT_WITHDRAW action types with English and French translations.

Changes

Paginated Yield-Category Token Fetching

Layer / File(s) Summary
Token balance type, action contract, and API client binding
src/domain/types/token-balance.ts, src/providers/api/api-client.ts, src/pages/details/earn-page/state/types.ts
TokenBalanceScanResponseDto overrides the legacy token field with TokenDto; TokenBalanceWithYieldsSelectAction and load-more fields (hasMoreTokens, isLoadingMoreTokens, onLoadMoreTokens) are added to EarnPageContextType/Actions; TokensControllerGetTokens is bound on the yield API client.
Paginated useDefaultTokens and yield-category helpers
src/hooks/api/use-default-tokens.ts
Scaffolding adds infinite-query types and keys; helpers add getYieldTypesForDashboardCategory, getNextOffset, shouldUseYieldTokensApi, and toTokenBalanceScanResponse; fetchDefaultTokens conditionally routes yield vs. legacy API and fetches remaining pages concurrently; useDefaultTokens switches to useInfiniteQuery accepting yieldCategory; getDefaultTokens seeds the infinite cache and returns flat token list.
Earn-page hook chain threaded with DashboardYieldCategory
src/pages/details/earn-page/state/use-token-balances-map.ts, use-token-balance.ts, use-get-init-yield.ts, use-init-yield.ts, src/common/get-token-balances.ts
useTokenBalancesMap, useTokenBalance, useGetInitYield, useInitYield, and getTokenBalances each accept and forward selectedDashboardYieldCategory down to paginated token fetching and init-yield derivation.
Earn-page state context: reducer refactor and category-aware init
src/pages/details/earn-page/state/earn-page-state-context.tsx
Extracts onTokenSelectState helper handling both token/select and tokenBalance/select reducer cases via getInitYield with optional availableYields; derives selectedDashboardYieldCategory from dashboardYieldCategoryGroupingEnabled; reorders useYieldOpportunity earlier; passes category into useTokenBalance.
Earn-page context: category-scoped token balances and pagination
src/pages/details/earn-page/state/earn-page-context.tsx
Passes yieldCategory into useDefaultTokens, filters tokenBalancesScan by category when grouping is enabled, dispatches tokenBalance/select with the full object, and exposes hasMoreTokens/isLoadingMoreTokens/onLoadMoreTokens in the context value.
SelectToken UI infinite scroll wiring
src/pages/details/earn-page/components/select-token-section/select-token.tsx
Reads hasMoreTokens, isLoadingMoreTokens, onLoadMoreTokens from context and wires them into VirtualList as hasNextPage/isFetchingNextPage/fetchNextPage.
Tests and mocks for paginated token fetching
tests/hooks/default-tokens.test.ts, tests/mocks/yield-api-handlers.ts, tests/providers/api-client.test.tsx, tests/pages-dashboard/earn-details-model.test.tsx
Tests cover fetchDefaultTokens endpoint routing and parameter shapes, getDefaultTokens offset sequencing and symbol aggregation, and concurrent page fetching; MSW adds paginated GET /v1/tokens handler; api-client test asserts TokensControllerGetTokens is present; earn-details fixture gains subsequentMinimum.

KYC authorizeUrl Migration

Layer / File(s) Summary
KYC URL source type, getKycUrl fallback chain, and test updates
src/domain/types/kyc.ts, tests/domain/kyc.test.ts, tests/use-cases/rwa-kyc-flow.test.tsx
KycUrlSource.status is retyped to pick authorizeUrl; getKycUrl reads status?.authorizeUrl. Tests add a shared kycEligibility fixture and update the pending-status case to supply authorizeUrl; the RWA KYC fixture gains full eligibility configuration.

Withdrawal Action Types and Translations

Layer / File(s) Summary
Withdrawal action constants and i18n strings
src/domain/types/action.ts, src/translation/English/translations.json, src/translation/French/translations.json
ActionTypes gains WITHDRAW_REQUEST and INSTANT_WITHDRAW. Both English and French translations add withdraw_request/instant_withdraw entries under complete.pending_action, position_details.pending_action, pending_action_button, and pending_action_review.pending_action_type.

Sequence Diagram(s)

sequenceDiagram
  participant SelectToken
  participant EarnPageContext
  participant useDefaultTokens
  participant fetchDefaultTokens
  participant YieldAPI as GET /v1/tokens (yield)
  participant LegacyAPI as GET /v1/tokens (legacy)

  SelectToken->>EarnPageContext: onLoadMoreTokens()
  EarnPageContext->>useDefaultTokens: fetchNextPage()
  useDefaultTokens->>fetchDefaultTokens: offset, limit, yieldTypes
  alt yieldTypes present (DashboardYieldCategory set)
    fetchDefaultTokens->>YieldAPI: GET /v1/tokens?yieldTypes=...&offset=N
    YieldAPI-->>fetchDefaultTokens: { items[], nextOffset }
  else no yield filter
    fetchDefaultTokens->>LegacyAPI: GET /v1/tokens?enabledYieldsOnly
    LegacyAPI-->>fetchDefaultTokens: single page
  end
  fetchDefaultTokens-->>useDefaultTokens: DefaultTokensPage + nextOffset
  useDefaultTokens-->>EarnPageContext: flattened pages[].tokens
  EarnPageContext-->>SelectToken: updated hasMoreTokens / isLoadingMoreTokens
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • stakekit/widget#516: Modifies the same getTokenBalances/use-default-tokens fetching path that this PR rewrites for paginated DashboardYieldCategory-scoped token loading.
  • stakekit/widget#525: Both PRs directly modify packages/widget/src/domain/types/kyc.ts and the getKycUrl/status-based URL resolution logic.
  • stakekit/widget#527: Both PRs implement selectedDashboardYieldCategory scoping in the earn-page context and token-balance/default-token fetching flow.

Suggested reviewers

  • Philippoes
  • dnehl
  • xhakti

🐇 Hop, hop — pages flip one by one,
Each token fetched 'til all yields are done.
The category threads from hook to hook,
KYC finds its URL — go look!
Withdraw and instant, now translated too,
A bunny-approved infinite queue! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided; the template requires 'Added' and 'Changed' sections to document the modifications. Add a description following the template with 'Added' section explaining new features and 'Changed' section detailing modifications to existing functionality.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding filtering of tokens by yield category throughout the codebase.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eligible-tokens-only

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

@aws-amplify-eu-central-1

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-541.d2ribjy8evqo6h.amplifyapp.com

@aws-amplify-eu-central-1

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-541.df4xyoi0xyeak.amplifyapp.com

@petar-omni petar-omni force-pushed the feat/eligible-tokens-only branch from 7ae18ad to f0cde9c Compare June 17, 2026 14:23
Philippoes
Philippoes previously approved these changes Jun 17, 2026
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/widget/tests/use-cases/rwa-kyc-flow.test.tsx (1)

48-52: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use authorizeUrl in the KYC status mock response.

mockKycStatus still returns kycUrl (Line 51), but the production mapping now reads status.authorizeUrl. This mock no longer matches the API contract and can hide regressions in status-URL handling.

Suggested fix
       return HttpResponse.json({
         kycStatus:
           calls > 1 && statusAfterRefresh ? statusAfterRefresh : status,
-        kycUrl: "https://issuer.example/verify",
+        authorizeUrl: "https://issuer.example/verify",
       });
🤖 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 `@packages/widget/tests/use-cases/rwa-kyc-flow.test.tsx` around lines 48 - 52,
The mock response in the rwa-kyc-flow.test.tsx file is using an outdated field
name that no longer matches the production API contract. In the
HttpResponse.json return statement within the mockKycStatus mock handler, change
the field name from kycUrl to authorizeUrl to align with the current status
mapping in production code. This ensures the mock accurately reflects the actual
API response structure and prevents hiding regressions related to status-URL
handling.
🤖 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 `@packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx`:
- Around line 227-236: The categoryTokenSet is being derived from the paginated
defTb data, which only includes currently loaded pages and filters out scanned
balances for tokens from not-yet-loaded pages. Instead of using val.defTb to
determine which tokens belong to the selected dashboard yield category, retrieve
the full category membership information separately from the paginated subset.
Then use this complete category membership set (not just the currently loaded
page subset) to filter val.tb, ensuring tokens from all pages are properly
evaluated regardless of pagination state.

---

Outside diff comments:
In `@packages/widget/tests/use-cases/rwa-kyc-flow.test.tsx`:
- Around line 48-52: The mock response in the rwa-kyc-flow.test.tsx file is
using an outdated field name that no longer matches the production API contract.
In the HttpResponse.json return statement within the mockKycStatus mock handler,
change the field name from kycUrl to authorizeUrl to align with the current
status mapping in production code. This ensures the mock accurately reflects the
actual API response structure and prevents hiding regressions related to
status-URL handling.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 70785469-c32f-48f8-b413-f05e48e10f52

📥 Commits

Reviewing files that changed from the base of the PR and between dcf1c01 and f0cde9c.

⛔ Files ignored due to path filters (2)
  • packages/widget/src/generated/api/legacy.ts is excluded by !**/generated/**
  • packages/widget/src/generated/api/yield.ts is excluded by !**/generated/**
📒 Files selected for processing (22)
  • packages/widget/src/common/get-token-balances.ts
  • packages/widget/src/domain/types/action.ts
  • packages/widget/src/domain/types/kyc.ts
  • packages/widget/src/domain/types/token-balance.ts
  • packages/widget/src/hooks/api/use-default-tokens.ts
  • packages/widget/src/pages/details/earn-page/components/select-token-section/select-token.tsx
  • packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx
  • packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx
  • packages/widget/src/pages/details/earn-page/state/types.ts
  • packages/widget/src/pages/details/earn-page/state/use-get-init-yield.ts
  • packages/widget/src/pages/details/earn-page/state/use-init-yield.ts
  • packages/widget/src/pages/details/earn-page/state/use-token-balance.ts
  • packages/widget/src/pages/details/earn-page/state/use-token-balances-map.ts
  • packages/widget/src/providers/api/api-client.ts
  • packages/widget/src/translation/English/translations.json
  • packages/widget/src/translation/French/translations.json
  • packages/widget/tests/domain/kyc.test.ts
  • packages/widget/tests/hooks/default-tokens.test.ts
  • packages/widget/tests/mocks/yield-api-handlers.ts
  • packages/widget/tests/pages-dashboard/earn-details-model.test.tsx
  • packages/widget/tests/providers/api-client.test.tsx
  • packages/widget/tests/use-cases/rwa-kyc-flow.test.tsx

Comment on lines +227 to +236
const categoryTokenSet =
dashboardYieldCategoryGroupingEnabled &&
selectedDashboardYieldCategory
? new Set(val.defTb.map((item) => tokenString(item.token)))
: null;
const tokenBalancesScanData = categoryTokenSet
? val.tb.filter((item) =>
categoryTokenSet.has(tokenString(item.token))
)
: val.tb;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Paginated default-token set is being used as a hard filter for scanned balances.

On Line 227–236, categoryTokenSet is derived from defaultTokens.data (incremental pages). While more pages exist, scanned balances for not-yet-loaded category tokens are filtered out, so users can miss/selectably lose eligible tokens until they manually load further pages. Please decouple category membership from the currently loaded page subset before filtering val.tb.

🤖 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 `@packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx`
around lines 227 - 236, The categoryTokenSet is being derived from the paginated
defTb data, which only includes currently loaded pages and filters out scanned
balances for tokens from not-yet-loaded pages. Instead of using val.defTb to
determine which tokens belong to the selected dashboard yield category, retrieve
the full category membership information separately from the paginated subset.
Then use this complete category membership set (not just the currently loaded
page subset) to filter val.tb, ensuring tokens from all pages are properly
evaluated regardless of pagination state.

@raiseerco raiseerco 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.

comments!


return useCallback(
({
availableYields,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: availableYields are threaded into the cached path here, but on a cache miss we still fall back to useInitYield ---> getTokenBalances, which re-fetches token discovery to rebuild the same yieldIds...could we carry availableYields through the async path too?

? val.tb.filter((item) =>
categoryTokenSet.has(tokenString(item.token))
)
: val.tb;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

same as bot: it looks like category membership is derived from the currently loaded defaultTokens pages, so eligible balance tokens from later pages can be filtered out and become unsearchable...

@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.

🧹 Nitpick comments (2)
packages/widget/src/hooks/api/use-default-tokens.ts (2)

145-152: 💤 Low value

Non-null assertions on potentially undefined pagination fields.

The fields offset, limit, and total are optional in DefaultTokensPage type (lines 37-41), yet this code uses non-null assertions. While the yield API should always return these values, if the API response is malformed or the contract changes, this will throw at runtime.

Consider using default values or early validation to improve robustness:

💡 Suggested defensive approach
+    const offset = firstPage.offset ?? firstOffset;
+    const limit = firstPage.limit ?? DEFAULT_TOKENS_PAGE_LIMIT;
+    const total = firstPage.total ?? 0;
+
     const remainingOffsets: number[] = [];
     for (
-      let offset = firstPage.offset! + firstPage.limit!;
-      offset < firstPage.total!;
-      offset += firstPage.limit!
+      let nextOff = offset + limit;
+      nextOff < total;
+      nextOff += limit
     ) {
-      remainingOffsets.push(offset);
+      remainingOffsets.push(nextOff);
     }
🤖 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 `@packages/widget/src/hooks/api/use-default-tokens.ts` around lines 145 - 152,
The loop in the remainingOffsets section uses non-null assertions on the
optional fields offset, limit, and total from firstPage, which will throw at
runtime if these fields are undefined due to a malformed API response. Instead
of relying on non-null assertions, add defensive validation before the loop to
ensure these fields have values (either by checking they exist and returning
early, or by providing sensible defaults), then use the validated values in the
loop condition and increment expression without the non-null assertion
operators.

226-251: ⚖️ Poor tradeoff

Duplicated yield API call construction.

The yield API request construction and response mapping (params building, TokensControllerGetTokens call, response transformation to DefaultTokensPage) is duplicated between fetchDefaultTokens (lines 109-136) and this infiniteQuery.queryFn. This increases maintenance burden if the API contract changes.

Consider extracting a shared helper for the yield API page fetch:

💡 Possible extraction approach

Extract fetchYieldTokensPage from fetchDefaultTokens to module scope and reuse it here:

// At module scope
const fetchYieldTokensPage = async (
  apiClient: ApiClient,
  params: { networks?: ...; yieldTypes?: ...; offset: number; limit: number },
  signal?: AbortSignal
): Promise<DefaultTokensPage> => {
  const page = await apiClient
    .withOptions({ signal })
    .yield.TokensControllerGetTokens({ params });
  // ... mapping logic
};

Then reuse in both fetchDefaultTokens and infiniteQuery.queryFn.

🤖 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 `@packages/widget/src/hooks/api/use-default-tokens.ts` around lines 226 - 251,
The yield API request construction and response mapping logic is duplicated in
both fetchDefaultTokens and the infiniteQuery.queryFn where
TokensControllerGetTokens is called and responses are transformed to
DefaultTokensPage. Extract this shared logic into a new module-scope helper
function called fetchYieldTokensPage that accepts the apiClient, params object
containing networks, yieldTypes, offset, and limit, and an optional AbortSignal.
Move the API call, response mapping via toTokenBalanceScanResponse, and the
return value construction into this helper, then replace both duplicate
implementations with calls to this new helper function.
🤖 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.

Nitpick comments:
In `@packages/widget/src/hooks/api/use-default-tokens.ts`:
- Around line 145-152: The loop in the remainingOffsets section uses non-null
assertions on the optional fields offset, limit, and total from firstPage, which
will throw at runtime if these fields are undefined due to a malformed API
response. Instead of relying on non-null assertions, add defensive validation
before the loop to ensure these fields have values (either by checking they
exist and returning early, or by providing sensible defaults), then use the
validated values in the loop condition and increment expression without the
non-null assertion operators.
- Around line 226-251: The yield API request construction and response mapping
logic is duplicated in both fetchDefaultTokens and the infiniteQuery.queryFn
where TokensControllerGetTokens is called and responses are transformed to
DefaultTokensPage. Extract this shared logic into a new module-scope helper
function called fetchYieldTokensPage that accepts the apiClient, params object
containing networks, yieldTypes, offset, and limit, and an optional AbortSignal.
Move the API call, response mapping via toTokenBalanceScanResponse, and the
return value construction into this helper, then replace both duplicate
implementations with calls to this new helper function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84a527d4-d8df-4550-8f4b-8f4f0bcf6fee

📥 Commits

Reviewing files that changed from the base of the PR and between f0cde9c and 208fc99.

📒 Files selected for processing (2)
  • packages/widget/src/hooks/api/use-default-tokens.ts
  • packages/widget/tests/hooks/default-tokens.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/widget/tests/hooks/default-tokens.test.ts

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.

3 participants