Skip to content

Commit f285cd7

Browse files
abretonc7sclaude
andauthored
fix(perps): missing latest funding payments in Activity (#28671)
## **Description** ### Root cause The Activity page was not showing the most recent funding payments for accounts with many open positions. `HyperLiquidProvider.getFunding()` made a single API call for the full 365-day history window; HyperLiquid's API silently caps responses at 500 records (oldest-first), so the most recent payments were dropped. ### Fix - **Initial load — 1 API call**: `getFunding()` now defaults to fetching only the most recent 30-day window instead of 365 days. For windows exceeding the 500-record cap, `fetchWindowWithAutoSplit` recursively halves the window until every sub-window is under the cap. - **Boundary dedup by time+coin**: Adjacent chunk windows share boundary timestamps; dedup uses `time+coin` composite key (not hash — HyperLiquid funding records all share a zero hash). - **On-demand older history**: A cursor-based `loadMoreFunding` in `usePerpsTransactionHistory` loads the next 30-day page when the user scrolls to the bottom of the Funding tab. Max lookback remains 365 days. - **Auto-advance on empty**: When the Funding tab is empty after initial load (no funding in the most recent 30 days), `PerpsTransactionsView` auto-triggers `loadMoreFunding` to fetch the next older window. - **Error resilience**: Transient API failures during pull-to-refresh preserve existing loaded data instead of wiping to empty state. - **Stable sort**: Secondary sort by asset name ensures deterministic ordering when multiple coins share the same funding timestamp. - **Performance**: Shared `deduplicateById` helper (O(n) Set) replaces O(n²) reduce+findIndex. Redundant intermediate sorts removed — single sort pass in `mergedTransactions` memo. - **Rate limit improvement**: 1 API call per navigation/refresh vs 13 previously — ~50 loads/min safe vs 3. ### Changes | File | Change | |---|---| | `HyperLiquidProvider.ts` | `getFunding()` defaults to 30-day window; `fetchWindowWithAutoSplit` for cap-safe fetching; boundary dedup by `time+coin` (not hash) | | `usePerpsTransactionHistory.ts` | Cursor pagination (`loadMoreFunding`, `hasFundingMore`, `isFetchingMoreFunding`); ref-based lock to prevent double-fire; generation counter to discard stale results; shared `deduplicateById` helper; preserve data on error; stable sort by asset; removed redundant intermediate sorts | | `PerpsTransactionsView.tsx` | Wired `onEndReached → loadMoreFunding` on Funding tab; auto-advance effect when Funding tab is empty; `ActivityIndicator` spinner in `ListFooterComponent` | | `transactionTransforms.ts` | Removed sort from `transformFundingToTransactions` — sorting centralized in consumer (`mergedTransactions` memo) | | `transactionsHistoryConfig.ts` | Added `FUNDING_HISTORY_PAGE_WINDOW_DAYS`, `FUNDING_HISTORY_API_LIMIT`, `MIN_SPLIT_WINDOW_MS` constants | | `PerpsTransactionsView.styles.ts` | Added `loadMoreContainer` style | | `Perps.testIds.ts` | Added `FUNDING_LOAD_MORE_SPINNER` testID | | `usePerpsTransactionHistory.test.ts` | 5 new `loadMoreFunding` tests (happy path, empty result, error handling, exhausted cursor, dedup); 93% new code coverage | | `HyperLiquidProvider.test.ts` | Tests for parallel pagination, auto-split, null page handling, explicit time range forwarding | | `transactionTransforms.test.ts` | Updated sort test to reflect consumer-side sorting | | `PerpsTransactionsView.test.tsx`, `usePerpsHomeData.test.ts` | Updated mocks for new hook API surface | ## **Changelog** CHANGELOG entry: Fixed missing recent perpetuals funding payments — `getFunding` now fetches the most recent 30-day window by default (1 API call) and loads older history on demand as the user scrolls, replacing the previous 365-day call that silently dropped recent records past the 500-record cap. ## **Related issues** Fixes: [TAT-2057](https://consensyssoftware.atlassian.net/browse/TAT-2057) ## **Manual testing steps** \`\`\`gherkin Feature: On-demand funding history pagination Scenario: AC1 — Activity shows most recent funding payments (1 API call) Given the user has an account with perpetuals positions When the user navigates to Activity > Perps > Funding tab Then the most recent funding payments appear (457 records in 30-day window) And only 1 getFunding API call is made (visible in Metro logs) Scenario: AC2 — Consecutive fetches return consistent results Given the user is on the Funding tab When getFunding is called twice with the same time window Then both calls return identical record counts and latest timestamps Scenario: AC3 — Scrolling to bottom loads older history Given the user is on the Funding tab with recent data loaded When the user scrolls to the bottom of the Funding list Then a loading spinner appears at the bottom And a second API call fetches the next 30-day window And older funding entries appear in the list Scenario: AC4 — Pull-to-refresh failure preserves existing data Given the user has funding data loaded When the API returns an error during pull-to-refresh Then existing funding entries remain visible And no empty state is shown Scenario: AC5 — Refresh resets to fresh recent data Given the user pulls to refresh on the Funding tab Then the cursor resets and the most recent 30-day window is re-fetched (1 call) \`\`\` ## **Screenshots/Recordings** ### Latest funding entries | Before | After | |---|---| | <img src="https://raw.githubusercontent.com/abretonc7s/mm-mobile-farm-artifacts/main/fixes/28671/before-ac1-funding-latest.png" alt="Latest funding entries before" width="360" /> | <img src="https://raw.githubusercontent.com/abretonc7s/mm-mobile-farm-artifacts/main/fixes/28671/after-ac1-funding-latest.png" alt="Latest funding entries after" width="360" /> | ### Consecutive fetch consistency | Before | After | |---|---| | <img src="https://raw.githubusercontent.com/abretonc7s/mm-mobile-farm-artifacts/main/fixes/28671/before-ac2-funding-consistent.png" alt="Funding consistency before" width="360" /> | <img src="https://raw.githubusercontent.com/abretonc7s/mm-mobile-farm-artifacts/main/fixes/28671/after-ac2-funding-consistent.png" alt="Funding consistency after" width="360" /> | ### Video - Before video: [before.mp4](https://raw.githubusercontent.com/abretonc7s/mm-mobile-farm-artifacts/main/fixes/28671/before.mp4) - After video: [after.mp4](https://raw.githubusercontent.com/abretonc7s/mm-mobile-farm-artifacts/main/fixes/28671/after.mp4) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test changes) - [ ] I confirm that this PR addresses all items listed in the description [TAT-2057]: https://consensyssoftware.atlassian.net/browse/TAT-2057?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes funding-history fetching and pagination logic in both the HyperLiquid provider and UI hook; regressions could hide funding rows or cause extra API calls/spinners if cursor/generation handling is wrong. > > **Overview** > Fixes missing recent perps funding payments by changing `HyperLiquidProvider.getFunding()` to fetch funding in **30‑day pages** (defaulting to the most recent window) and **auto-splitting** any window that hits the API record cap, with boundary deduping. > > Adds cursor-based **on-demand funding pagination** to `usePerpsTransactionHistory` (`loadMoreFunding`, `hasFundingMore`, `isFetchingMoreFunding`) with deduping, stable sorting, and refresh/error behavior that preserves already-loaded data. `PerpsTransactionsView` wires infinite scroll + an empty-list auto-advance for the Funding tab and shows a footer spinner, with updated constants/test IDs and expanded tests to cover the new behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c7506b5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 845825f commit f285cd7

12 files changed

Lines changed: 743 additions & 70 deletions

app/components/UI/Perps/Perps.testIds.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ export const PerpsTransactionsViewSelectorsIDs = {
759759
TAB_ORDERS: 'perps-transactions-tab-orders',
760760
TAB_FUNDING: 'perps-transactions-tab-funding',
761761
TAB_DEPOSITS: 'perps-transactions-tab-deposits',
762+
FUNDING_LOAD_MORE_SPINNER: 'perps-transactions-funding-load-more-spinner',
762763
} as const;
763764

764765
// ========================================

app/components/UI/Perps/Views/PerpsTransactionsView/PerpsTransactionsView.styles.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,9 @@ export const styleSheet = (params: { theme: Theme }) => {
8989
paddingVertical: 12,
9090
paddingHorizontal: 16,
9191
},
92+
loadMoreContainer: {
93+
paddingVertical: 16,
94+
alignItems: 'center' as const,
95+
},
9296
};
9397
};

app/components/UI/Perps/Views/PerpsTransactionsView/PerpsTransactionsView.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ describe('PerpsTransactionsView', () => {
175175
isLoading: false,
176176
error: null,
177177
refetch: mockRefetchTransactions,
178+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
179+
hasFundingMore: true,
180+
isFetchingMoreFunding: false,
178181
});
179182

180183
mockUsePerpsEventTracking.mockReturnValue({
@@ -299,6 +302,9 @@ describe('PerpsTransactionsView', () => {
299302
isLoading: false,
300303
error: null,
301304
refetch: mockRefetch,
305+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
306+
hasFundingMore: true,
307+
isFetchingMoreFunding: false,
302308
});
303309

304310
renderWithProvider(<PerpsTransactionsView />, {
@@ -318,6 +324,9 @@ describe('PerpsTransactionsView', () => {
318324
isLoading: false,
319325
error: null,
320326
refetch: jest.fn(),
327+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
328+
hasFundingMore: true,
329+
isFetchingMoreFunding: false,
321330
});
322331

323332
const component = renderWithProvider(<PerpsTransactionsView />, {
@@ -350,6 +359,9 @@ describe('PerpsTransactionsView', () => {
350359
isLoading: false,
351360
error: null,
352361
refetch: mockRefetchTransactions,
362+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
363+
hasFundingMore: true,
364+
isFetchingMoreFunding: false,
353365
});
354366

355367
const component = renderWithProvider(<PerpsTransactionsView />, {
@@ -368,6 +380,9 @@ describe('PerpsTransactionsView', () => {
368380
isLoading: false,
369381
error: 'API Error',
370382
refetch: jest.fn(),
383+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
384+
hasFundingMore: true,
385+
isFetchingMoreFunding: false,
371386
});
372387

373388
const component = renderWithProvider(<PerpsTransactionsView />, {
@@ -426,6 +441,9 @@ describe('PerpsTransactionsView', () => {
426441
isLoading: false,
427442
error: null,
428443
refetch: jest.fn(),
444+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
445+
hasFundingMore: true,
446+
isFetchingMoreFunding: false,
429447
});
430448

431449
renderWithProvider(<PerpsTransactionsView />, {
@@ -459,6 +477,9 @@ describe('PerpsTransactionsView', () => {
459477
isLoading: false,
460478
error: null,
461479
refetch: jest.fn(),
480+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
481+
hasFundingMore: true,
482+
isFetchingMoreFunding: false,
462483
});
463484

464485
renderWithProvider(<PerpsTransactionsView />, {
@@ -477,6 +498,9 @@ describe('PerpsTransactionsView', () => {
477498
isLoading: false,
478499
error: 'Network error',
479500
refetch: jest.fn(),
501+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
502+
hasFundingMore: true,
503+
isFetchingMoreFunding: false,
480504
});
481505

482506
const component = renderWithProvider(<PerpsTransactionsView />, {
@@ -499,6 +523,9 @@ describe('PerpsTransactionsView', () => {
499523
isLoading: false,
500524
error: null,
501525
refetch: jest.fn(),
526+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
527+
hasFundingMore: true,
528+
isFetchingMoreFunding: false,
502529
});
503530

504531
const component = renderWithProvider(<PerpsTransactionsView />, {
@@ -614,6 +641,9 @@ describe('PerpsTransactionsView', () => {
614641
isLoading: false,
615642
error: null,
616643
refetch: jest.fn(),
644+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
645+
hasFundingMore: true,
646+
isFetchingMoreFunding: false,
617647
});
618648

619649
const component = renderWithProvider(<PerpsTransactionsView />, {

app/components/UI/Perps/Views/PerpsTransactionsView/PerpsTransactionsView.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import { useFocusEffect, useNavigation } from '@react-navigation/native';
22
import { FlashList } from '@shopify/flash-list';
3-
import React, { useCallback, useMemo, useRef, useState } from 'react';
4-
import { RefreshControl, ScrollView, View } from 'react-native';
3+
import React, {
4+
useCallback,
5+
useEffect,
6+
useMemo,
7+
useRef,
8+
useState,
9+
} from 'react';
10+
import {
11+
ActivityIndicator,
12+
RefreshControl,
13+
ScrollView,
14+
View,
15+
} from 'react-native';
516
import { useSelector } from 'react-redux';
617
import { strings } from '../../../../../../locales/i18n';
718
import {
@@ -77,6 +88,9 @@ const PerpsTransactionsView: React.FC = () => {
7788
transactions: allTransactions,
7889
isLoading: transactionsLoading,
7990
refetch: refreshTransactions,
91+
loadMoreFunding,
92+
hasFundingMore,
93+
isFetchingMoreFunding,
8094
} = usePerpsTransactionHistory({
8195
skipInitialFetch: !isConnected,
8296
accountId,
@@ -194,6 +208,28 @@ const PerpsTransactionsView: React.FC = () => {
194208
}
195209
}, [isConnected, refreshTransactions]);
196210

211+
// Auto-advance funding cursor when the Funding tab is empty but more data
212+
// exists. FlashList does not reliably call onEndReached on empty lists, so
213+
// we trigger loadMoreFunding directly when the tab shows no results.
214+
useEffect(() => {
215+
if (
216+
activeFilter === 'Funding' &&
217+
!transactionsLoading &&
218+
!isFetchingMoreFunding &&
219+
hasFundingMore &&
220+
fundingTransactions.length === 0
221+
) {
222+
loadMoreFunding();
223+
}
224+
}, [
225+
activeFilter,
226+
transactionsLoading,
227+
isFetchingMoreFunding,
228+
hasFundingMore,
229+
fundingTransactions,
230+
loadMoreFunding,
231+
]);
232+
197233
useFocusEffect(
198234
useCallback(() => {
199235
if (!isConnected) {
@@ -490,9 +526,26 @@ const PerpsTransactionsView: React.FC = () => {
490526
item.type === 'header' ? 'header' : 'transaction'
491527
}
492528
ListEmptyComponent={shouldShowEmptyState ? renderEmptyState : null}
529+
ListFooterComponent={
530+
activeFilter === 'Funding' && isFetchingMoreFunding ? (
531+
<View style={styles.loadMoreContainer}>
532+
<ActivityIndicator
533+
testID={
534+
PerpsTransactionsViewSelectorsIDs.FUNDING_LOAD_MORE_SPINNER
535+
}
536+
/>
537+
</View>
538+
) : null
539+
}
493540
refreshControl={
494541
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
495542
}
543+
onEndReached={
544+
activeFilter === 'Funding' && hasFundingMore
545+
? loadMoreFunding
546+
: undefined
547+
}
548+
onEndReachedThreshold={0.5}
496549
showsVerticalScrollIndicator={false}
497550
drawDistance={
498551
PERPS_TRANSACTIONS_HISTORY_CONSTANTS.FLASH_LIST_DRAW_DISTANCE

app/components/UI/Perps/hooks/usePerpsHomeData.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ describe('usePerpsHomeData', () => {
302302
isLoading: false,
303303
error: null,
304304
refetch: jest.fn().mockResolvedValue(undefined),
305+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
306+
hasFundingMore: true,
307+
isFetchingMoreFunding: false,
305308
});
306309

307310
// Mock sortMarkets to return markets as-is by default
@@ -897,6 +900,9 @@ describe('usePerpsHomeData', () => {
897900
isLoading: false,
898901
error: null,
899902
refetch: jest.fn().mockResolvedValue(undefined),
903+
loadMoreFunding: jest.fn().mockResolvedValue(undefined),
904+
hasFundingMore: true,
905+
isFetchingMoreFunding: false,
900906
});
901907

902908
const { result } = renderHook(() => usePerpsHomeData());

app/components/UI/Perps/hooks/usePerpsTransactionHistory.test.ts

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -588,8 +588,6 @@ describe('usePerpsTransactionHistory', () => {
588588

589589
it('uses provided parameters', async () => {
590590
const params = {
591-
startTime: 1640995200000,
592-
endTime: 1640995300000,
593591
accountId:
594592
'eip155:1:0x1234567890123456789012345678901234567890' as CaipAccountId,
595593
};
@@ -608,10 +606,9 @@ describe('usePerpsTransactionHistory', () => {
608606
expect(mockProvider.getOrders).toHaveBeenCalledWith({
609607
accountId: 'eip155:1:0x1234567890123456789012345678901234567890',
610608
});
609+
// startTime/endTime defaults are handled in HyperLiquidProvider via 30-day window
611610
expect(mockProvider.getFunding).toHaveBeenCalledWith({
612611
accountId: 'eip155:1:0x1234567890123456789012345678901234567890',
613-
startTime: 1640995200000,
614-
endTime: 1640995300000,
615612
});
616613
});
617614

@@ -1369,6 +1366,137 @@ describe('usePerpsTransactionHistory', () => {
13691366
});
13701367
});
13711368

1369+
describe('loadMoreFunding', () => {
1370+
const olderFundingRaw = [
1371+
{
1372+
symbol: 'ETH',
1373+
amountUsd: '-1.00',
1374+
rate: '0.0001',
1375+
timestamp: 1638403200000,
1376+
},
1377+
];
1378+
const olderFundingTx = {
1379+
id: 'funding-1638403200000-ETH',
1380+
type: 'funding' as const,
1381+
category: 'funding_fee' as const,
1382+
title: 'Paid funding fee',
1383+
subtitle: 'ETH',
1384+
timestamp: 1638403200000,
1385+
asset: 'ETH',
1386+
fundingAmount: {
1387+
isPositive: false,
1388+
fee: '-$1.00',
1389+
feeNumber: -1,
1390+
rate: '0.01%',
1391+
},
1392+
};
1393+
1394+
async function renderAndWaitForInitialFetch() {
1395+
const hook = renderHook(() =>
1396+
usePerpsTransactionHistory({ skipInitialFetch: false }),
1397+
);
1398+
await act(async () => {
1399+
await new Promise((resolve) => setTimeout(resolve, 0));
1400+
});
1401+
return hook;
1402+
}
1403+
1404+
it('fetches older funding and appends to transactions', async () => {
1405+
// Arrange
1406+
const { result } = await renderAndWaitForInitialFetch();
1407+
mockProvider.getFunding.mockResolvedValueOnce(olderFundingRaw);
1408+
mockTransformFundingToTransactions.mockReturnValueOnce([olderFundingTx]);
1409+
1410+
// Act
1411+
await act(async () => {
1412+
await result.current.loadMoreFunding();
1413+
});
1414+
1415+
// Assert
1416+
expect(mockProvider.getFunding).toHaveBeenLastCalledWith(
1417+
expect.objectContaining({
1418+
startTime: expect.any(Number),
1419+
endTime: expect.any(Number),
1420+
}),
1421+
);
1422+
expect(result.current.hasFundingMore).toBe(true);
1423+
expect(result.current.isFetchingMoreFunding).toBe(false);
1424+
});
1425+
1426+
it('skips empty windows and keeps hasFundingMore true', async () => {
1427+
// Arrange
1428+
const { result } = await renderAndWaitForInitialFetch();
1429+
mockProvider.getFunding.mockResolvedValueOnce([]);
1430+
1431+
// Act
1432+
await act(async () => {
1433+
await result.current.loadMoreFunding();
1434+
});
1435+
1436+
// Assert — cursor advances but pagination continues
1437+
expect(result.current.hasFundingMore).toBe(true);
1438+
});
1439+
1440+
it('sets hasFundingMore to false on fetch error', async () => {
1441+
// Arrange
1442+
const { result } = await renderAndWaitForInitialFetch();
1443+
mockProvider.getFunding.mockRejectedValueOnce(new Error('API error'));
1444+
1445+
// Act
1446+
await act(async () => {
1447+
await result.current.loadMoreFunding();
1448+
});
1449+
1450+
// Assert
1451+
expect(result.current.hasFundingMore).toBe(false);
1452+
expect(result.current.isFetchingMoreFunding).toBe(false);
1453+
});
1454+
1455+
it('does not fetch when hasFundingMore is false', async () => {
1456+
// Arrange
1457+
const { result } = await renderAndWaitForInitialFetch();
1458+
// Force hasFundingMore to false via error (errors still stop pagination)
1459+
mockProvider.getFunding.mockRejectedValueOnce(new Error('API error'));
1460+
await act(async () => {
1461+
await result.current.loadMoreFunding();
1462+
});
1463+
expect(result.current.hasFundingMore).toBe(false);
1464+
mockProvider.getFunding.mockClear();
1465+
1466+
// Act
1467+
await act(async () => {
1468+
await result.current.loadMoreFunding();
1469+
});
1470+
1471+
// Assert — no new call
1472+
expect(mockProvider.getFunding).not.toHaveBeenCalled();
1473+
});
1474+
1475+
it('deduplicates transactions when loadMore returns overlapping ids', async () => {
1476+
// Arrange
1477+
const { result } = await renderAndWaitForInitialFetch();
1478+
const dupFundingTx = {
1479+
...olderFundingTx,
1480+
id: 'funding-1638403200000-ETH',
1481+
};
1482+
mockProvider.getFunding.mockResolvedValueOnce(olderFundingRaw);
1483+
mockTransformFundingToTransactions.mockReturnValueOnce([
1484+
dupFundingTx,
1485+
dupFundingTx,
1486+
]);
1487+
1488+
// Act
1489+
await act(async () => {
1490+
await result.current.loadMoreFunding();
1491+
});
1492+
1493+
// Assert — no duplicates in result
1494+
const ids = result.current.transactions.map((tx) => tx.id);
1495+
const uniqueIds = new Set(ids);
1496+
expect(ids.length).toBe(uniqueIds.size);
1497+
});
1498+
});
1499+
13721500
describe('connection state transitions', () => {
13731501
it('triggers fetch when skipInitialFetch transitions from true to false', async () => {
13741502
// Reset mocks to track calls clearly

0 commit comments

Comments
 (0)