Skip to content

feat(pos-app): activity filters, date range, and Pay API migration#412

Open
ignaciosantise wants to merge 7 commits intomainfrom
feat/activity-filter-bottomsheets
Open

feat(pos-app): activity filters, date range, and Pay API migration#412
ignaciosantise wants to merge 7 commits intomainfrom
feat/activity-filter-bottomsheets

Conversation

@ignaciosantise
Copy link
Collaborator

@ignaciosantise ignaciosantise commented Mar 10, 2026

Summary

  • Bottom sheet filter modals: Replace inline filter chips with bottom sheet modals for status and date range filtering on the Activity screen
  • Date range filtering: Add date range filter (Today, Last 7 days, This week, This month, All time) with startTs/endTs API params
  • Individual status filters: Add separate Expired and Cancelled status filters (previously grouped under Failed)
  • Pay API migration: Migrate transactions service from old Merchant API (/merchants/{id}/payments with MERCHANT_PORTAL_API_KEY) to unified Pay API (/v1/merchants/payments with shared getApiHeaders())
  • Response schema update: Update types from flat snake_case to nested camelCase DTOs (AmountWithDisplay, BuyerInfo, TransactionInfo, SettlementInfo)
  • Remove CAIP-19 parsing: Replace client-side token parsing (utils/tokens.ts) with server-provided display values (iconUrl, formatted, assetSymbol)
  • Clean up env vars: Remove EXPO_PUBLIC_MERCHANT_API_URL and EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY
  • Fix layout shift: Prevent layout shift when countdown timer appears in scan view

Test plan

  • All 224 tests pass
  • Lint and Prettier clean
  • Verify activity screen loads transactions from new endpoint
  • Verify status filters (Pending, Completed, Failed, Expired, Cancelled) work correctly
  • Verify date range filters work correctly
  • Verify transaction card displays fiat amount, date, and status
  • Verify transaction detail modal shows all fields including crypto received with server icon
  • Verify infinite scroll pagination works with nextCursor
  • Verify web version works through Vercel proxy

🤖 Generated with Claude Code

…nd add date filtering

- Replace horizontal scrollable filter pills with two compact buttons (Status & Date range)
- Add date range filtering with options: All Time, Today, 7 Days, This Week, This Month
- Status filter button now shows selected filter label instead of generic "Status"
- Each filter opens a bottom sheet modal with radio selection list
- Add color-coded dots in status filter modal matching transaction states
- Add DateRangeFilterType and getDateRange utility for date computation
- Extend API with fromDate/toDate query parameters for server-side filtering
- Store date range filter preference in Zustand (defaults to "Today")
- Bump settings store version to 14 with migration support

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
Copilot AI review requested due to automatic review settings March 10, 2026 17:42
@vercel
Copy link

vercel bot commented Mar 10, 2026

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

Project Deployment Actions Updated (UTC)
pos-demo Ready Ready Preview, Comment Mar 11, 2026 5:22pm

Request Review

@claude
Copy link

claude bot commented Mar 10, 2026

Claude finished @ignaciosantise's task in 2m 29s —— View job


PR Review

  • Read changed files
  • Run breaking changes analysis
  • Run data classification analysis
  • Post review findings

Found 2 issue(s)

Issue 1: toDate drifts between infinite query page fetches

ID: hooks-todate-pagination-drift-a3f1
File: dapps/pos-app/services/hooks.ts:217-229
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: getDateRange(dateRangeFilter) is called inside queryFn, which runs on every page fetch of the infinite query. toDate is new Date().toISOString() at call time, so it advances by seconds/milliseconds between page 1 and page 2.
  • Risk: When paginating, the server receives a different to_date for each page, so the result window shifts. This can cause records to be duplicated or skipped at page boundaries.
  • Impact: Users may see duplicate transactions or miss transactions around pagination boundaries when using date-range filters.
  • Trigger: Happens any time dateRangeFilter !== "all_time" and the list is long enough to paginate.

Recommendation: Compute the date range once and anchor it to the query, e.g. by including the computed fromDate/toDate in the query key or memoizing them alongside the filter:

// Compute once when filter changes, not per-page
const { fromDate, toDate } = useMemo(() => getDateRange(dateRangeFilter), [dateRangeFilter]);

const query = useInfiniteQuery({
  queryKey: ["transactions", filter, dateRangeFilter, sortBy, sortDir, limit],
  queryFn: ({ pageParam }) => {
    return getTransactions({
      ...queryOptions,
      status: filterToStatusArray(filter),
      fromDate,
      toDate,   // stable for this query session
      ...
    });
  },
  ...
});

Fix this →


Issue 2: from variable potentially uninitialized at runtime in getDateRange

ID: date-range-uninitialized-from-b8c2
File: dapps/pos-app/utils/date-range.ts:18-46
Severity: LOW
Category: code_quality

Context:

  • Pattern: let from: Date; is declared without initialization; the switch covers all current union members but has no default case. TypeScript exhaustiveness works at compile time, but if the type widens or a value arrives from JS context at runtime, .toISOString() at line 46 will throw TypeError: Cannot read properties of undefined.
  • Risk: Unhandled runtime exception on any unlisted filter value.
  • Trigger: Future enum expansion or runtime calls from untyped code.

Recommendation: Add a default exhaustive guard:

default: {
  const _exhaustive: never = filter;
  return {};
}

✅ No breaking changes detected. No data classification issues found.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the POS app Activity screen filtering UX by replacing the horizontal status “pill” tabs with two compact buttons that open bottom-sheet modals, and adds server-side date range filtering via new query parameters.

Changes:

  • Introduces a persisted dateRangeFilter setting (defaulting to "today") and a getDateRange utility to compute fromDate/toDate.
  • Extends the transactions API/client hooks to include date range filtering (from_date / to_date) and updates React Query caching keys accordingly.
  • Replaces the FilterTabs UI with new FilterButtons + SettingsBottomSheet + RadioList options, including colored status dots.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
dapps/pos-app/utils/types.ts Adds DateRangeFilterType to represent supported date filters.
dapps/pos-app/utils/date-range.ts New utility to compute fromDate/toDate for server-side date filtering.
dapps/pos-app/store/useSettingsStore.ts Persists dateRangeFilter, adds setter, and bumps persisted store version + migration.
dapps/pos-app/services/transactions.ts Adds fromDate/toDate options and serializes them as from_date/to_date query params.
dapps/pos-app/services/hooks.ts Extends useTransactions to accept dateRangeFilter, compute date ranges, and include them in queryKey + API calls.
dapps/pos-app/components/radio-list.tsx Adds optional dotColor support to render a small colored dot per option.
dapps/pos-app/components/filter-tabs.tsx Removes the old horizontal scroll filter tabs component.
dapps/pos-app/components/filter-buttons.tsx Adds new two-button header component for Status/Date range bottom sheets.
dapps/pos-app/app/activity.tsx Updates Activity screen UI to use the new filter buttons + bottom sheets and wire filters into useTransactions.
Comments suppressed due to low confidence (2)

dapps/pos-app/services/hooks.ts:228

  • queryKey includes sortBy, sortDir, and limit from queryOptions, but queryFn unconditionally overrides these (sortBy: "date", sortDir: "desc", limit: 20). This makes the cache key imply variability that the request ignores, and can lead to unnecessary cache misses/confusing API. Either honor the passed queryOptions values or remove these fields from the key/inputs if they’re meant to be fixed.
  // Extract relevant fields for query key to avoid cache misses from object reference changes
  const { sortBy, sortDir, limit } = queryOptions;

  const query = useInfiniteQuery<TransactionsResponse, Error>({
    queryKey: ["transactions", filter, dateRangeFilter, sortBy, sortDir, limit],
    queryFn: ({ pageParam }) => {
      const statusFilter = filterToStatusArray(filter);
      const { fromDate, toDate } = getDateRange(dateRangeFilter);
      return getTransactions({
        ...queryOptions,
        status: statusFilter,
        fromDate,
        toDate,
        sortBy: "date",
        sortDir: "desc",
        limit: 20,
        cursor: pageParam as string | undefined,

dapps/pos-app/services/hooks.ts:229

  • useTransactions now supports dateRangeFilter and passes fromDate/toDate into getTransactions, but the existing hook tests only cover useStartPayment and usePaymentStatus. Consider adding tests that assert getTransactions is called with the expected fromDate/toDate for a given dateRangeFilter and that the queryKey changes appropriately when the filter changes.
interface UseTransactionsOptions {
  /**
   * Whether to enable the query
   * @default true
   */
  enabled?: boolean;
  /**
   * Filter transactions by UI filter type
   * @default "all"
   */
  filter?: TransactionFilterType;
  /**
   * Filter transactions by date range
   * @default "today"
   */
  dateRangeFilter?: DateRangeFilterType;
  /**
   * Additional query options for the API
   */
  queryOptions?: GetTransactionsOptions;
}

/**
 * Maps UI filter type to API status values
 */
function filterToStatusArray(
  filter: TransactionFilterType,
): string[] | undefined {
  switch (filter) {
    case "completed":
      return ["succeeded"];
    case "failed":
      return ["failed", "expired", "cancelled"];
    case "pending":
      return ["requires_action", "processing"];
    case "all":
    default:
      return undefined;
  }
}

/**
 * Hook to fetch merchant transactions with filtering and infinite scrolling
 * @param options - Query options including filter type
 * @returns Infinite query result with paginated transactions
 */
export function useTransactions(options: UseTransactionsOptions = {}) {
  const {
    enabled = true,
    filter = "all",
    dateRangeFilter = "today",
    queryOptions = {},
  } = options;

  const addLog = useLogsStore.getState().addLog;

  // Extract relevant fields for query key to avoid cache misses from object reference changes
  const { sortBy, sortDir, limit } = queryOptions;

  const query = useInfiniteQuery<TransactionsResponse, Error>({
    queryKey: ["transactions", filter, dateRangeFilter, sortBy, sortDir, limit],
    queryFn: ({ pageParam }) => {
      const statusFilter = filterToStatusArray(filter);
      const { fromDate, toDate } = getDateRange(dateRangeFilter);
      return getTransactions({
        ...queryOptions,
        status: statusFilter,
        fromDate,
        toDate,
        sortBy: "date",
        sortDir: "desc",
        limit: 20,
        cursor: pageParam as string | undefined,
      });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

- Add exhaustive default guard in getDateRange switch to prevent runtime errors
- Stabilize toDate across paginated fetches by memoizing getDateRange outside queryFn
- Update store version assertion in test from 13 to 14

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@ignaciosantise
Copy link
Collaborator Author

Re: toDate drifts between infinite query page fetches

Fixed in 4773484 — moved getDateRange() call outside queryFn into a useMemo keyed on dateRangeFilter, so fromDate/toDate remain stable across all paginated fetches for a given filter.

Re: from variable potentially uninitialized

Also fixed in 4773484 — added exhaustive default case with never type guard.

Rename fromDate/toDate to startTs/endTs across the codebase to match
the actual GET /v1/merchants/payments query parameter names.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Split the "Failed" filter into separate Failed, Expired, and Cancelled
options to match the backend payment statuses. Each status now maps to
its own API value instead of grouping expired/cancelled under failed.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Migrate from old Merchant API (`/merchants/{id}/payments` with separate
`MERCHANT_PORTAL_API_KEY`) to unified Pay API (`/v1/merchants/payments`
with shared `getApiHeaders()`). Update response types from flat
snake_case to nested camelCase DTOs. Replace CAIP-19 token parsing with
server-provided display values. Remove `utils/tokens.ts` and merchant
API env vars.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@ignaciosantise ignaciosantise changed the title feat(pos-app): replace filters with bottom sheet modals and add date filtering feat(pos-app): activity filters, date range, and Pay API migration Mar 11, 2026
@ignaciosantise
Copy link
Collaborator Author

@claude review

@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @ignaciosantise's task in 2m 6s —— View job


PR Review

  • Read changed files
  • Analyze code for issues
  • Post review findings

🔄 Incremental Review: focusing on changes since the previous review.

Previously flagged issues — both resolved ✅

  • hooks-todate-pagination-drift-a3f1: Fixed — getDateRange() moved into useMemo keyed on dateRangeFilter.
  • date-range-uninitialized-from-b8c2: Fixed — exhaustive default with never guard added.

Found 2 issue(s)

Issue 1: queryKey includes sortBy/sortDir/limit from queryOptions but queryFn hardcodes them

ID: hooks-querykey-queryoptions-mismatch-c1d3
File: dapps/pos-app/services/hooks.ts:226-238
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: queryKey extracts { sortBy, sortDir, limit } from queryOptions and includes them in the cache key, but queryFn ignores those values and always passes sortBy: "date", sortDir: "desc", limit: 20.
  • Risk: Two useTransactions calls with different queryOptions sortBy/limit values will produce separate cache entries that never share data, even though the actual requests sent are identical. Conversely, a future caller that genuinely wants a different sort order will silently get the hardcoded one.
  • Impact: Unnecessary cache misses today; silent incorrect behavior if queryOptions is ever used to vary sort/limit.
  • Trigger: Any call to useTransactions that passes queryOptions with non-default sortBy/sortDir/limit.

Recommendation: Either remove the hardcoded overrides and honor queryOptions, or drop sortBy/sortDir/limit from both the input interface and the query key since they aren't actually configurable:

// Option A: Remove from queryKey since they're constants
queryKey: ["transactions", filter, dateRangeFilter],
queryFn: ({ pageParam }) => getTransactions({
  ...queryOptions,
  status: filterToStatusArray(filter),
  startTs,
  endTs,
  sortBy: "date",   // document as non-configurable
  sortDir: "desc",
  limit: 20,
  cursor: pageParam as string | undefined,
}),

Fix this →


Issue 2: Computed startTs/endTs become stale when app stays open past midnight

ID: hooks-datets-stale-midnight-f2a9
File: dapps/pos-app/services/hooks.ts:220-223
Severity: LOW
Category: code_quality

Context:

  • Pattern: useMemo(() => getDateRange(dateRangeFilter), [dateRangeFilter]) computes the date range once when the filter changes. If a user leaves the app open overnight with filter "today", the memoized startTs/endTs will reflect yesterday's range until the filter is toggled.
  • Risk: Pull-to-refresh does not recompute the date window because the query key (dateRangeFilter string) hasn't changed.
  • Impact: On long-lived sessions, the "today" filter continues fetching from yesterday's midnight boundary.
  • Trigger: App open overnight without changing the date filter.

Recommendation: Include the computed timestamps in the query key so stale values are caught when the query is refetched:

const { startTs, endTs } = useMemo(
  () => getDateRange(dateRangeFilter),
  [dateRangeFilter],
);

queryKey: ["transactions", filter, dateRangeFilter, startTs, endTs, sortBy, sortDir, limit],

This forces a cache miss (and fresh fetch) if the computed range ever changes, e.g. after a hard reload.

The upstream API can return an empty body on error responses, causing
JSON.parse to fail. Read as text first and only parse if non-empty.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…queryOptions

Base URL already includes /v1, so endpoint was resolving to /v1/v1/merchants/payments.
Also remove unused queryOptions from useTransactions since sort/limit are hardcoded.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
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