Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion src/__tests__/stores.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import './__mocks__/setup';
import { useWalletStore } from '../store/walletStore';
import { useWalletStore, partializeWalletState } from '../store/walletStore';
import {
useTaskStore,
partializeTaskState,
Expand Down Expand Up @@ -90,6 +90,82 @@ describe('walletStore', () => {
expect(state.ecoBalance).toBeNull();
expect(state.usdcBalance).toBeNull();
});

it('partialize persists only identity fields, excluding live balances', () => {
const partial = partializeWalletState({
isConnected: true,
status: 'connected',
connectError: null,
publicKey: 'GCXXYZ...',
balance: '100.5',
ecoBalance: '500',
usdcBalance: '25.50',
walletType: 'inapp' as const,
beginConnect: () => {},
connect: () => {},
connectFailed: () => {},
disconnect: () => {},
setBalance: () => {},
setEcoBalance: () => {},
setUsdcBalance: () => {},
});
expect(partial).toEqual({
isConnected: true,
publicKey: 'GCXXYZ...',
walletType: 'inapp',
});
expect(partial).not.toHaveProperty('balance');
expect(partial).not.toHaveProperty('ecoBalance');
expect(partial).not.toHaveProperty('usdcBalance');
expect(partial).not.toHaveProperty('status');
expect(partial).not.toHaveProperty('connectError');
});

it('rehydrated store starts with null balances (stale MMKV snapshot excluded)', () => {
// Simulate what a legacy build persisted: identity PLUS stale balances.
const legacyPersisted = {
isConnected: true,
publicKey: 'GCXXYZ...',
walletType: 'inapp' as const,
balance: '999',
ecoBalance: '888',
usdcBalance: '77.7',
};

// The new partialize drops the stale balance fields, so merging the
// persisted payload back into the live store cannot restore them.
const persisted = partializeWalletState({
...legacyPersisted,
status: 'connected',
connectError: null,
beginConnect: () => {},
connect: () => {},
connectFailed: () => {},
disconnect: () => {},
setBalance: () => {},
setEcoBalance: () => {},
setUsdcBalance: () => {},
});

// Simulate cold-start rehydration: defaults + persisted slice only.
useWalletStore.setState({
isConnected: false,
publicKey: null,
balance: null,
ecoBalance: null,
usdcBalance: null,
});
useWalletStore.setState({ ...persisted });

const state = useWalletStore.getState();
expect(state.isConnected).toBe(true);
expect(state.publicKey).toBe('GCXXYZ...');
expect(state.walletType).toBe('inapp');
// Balances must be null on cold start — useStellarWallet re-fetches them.
expect(state.balance).toBeNull();
expect(state.ecoBalance).toBeNull();
expect(state.usdcBalance).toBeNull();
});
});

describe('taskStore', () => {
Expand Down
32 changes: 24 additions & 8 deletions src/store/walletStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ const clearedWalletFields = {
'publicKey' | 'balance' | 'ecoBalance' | 'usdcBalance' | 'walletType'
>;

/**
* Persisted slice of the wallet store. Only identity fields survive a
* restart; live balances are excluded so a cold start never serves a stale
* snapshot as current data (they are refreshed by useStellarWallet).
*/
export type WalletPersistedState = Pick<
WalletState,
'isConnected' | 'publicKey' | 'walletType'
>;

export const partializeWalletState = (
state: WalletState,
): WalletPersistedState => ({
isConnected: state.isConnected,
publicKey: state.publicKey,
walletType: state.walletType,
});

export const useWalletStore = create<WalletState>()(
persist(
set => ({
Expand Down Expand Up @@ -123,14 +141,12 @@ export const useWalletStore = create<WalletState>()(
// Only durable data survives a restart. `status` and `connectError`
// are transient: a crash mid-connect must not restore a stuck
// 'connecting' status or a stale error banner.
partialize: state => ({
isConnected: state.isConnected,
publicKey: state.publicKey,
balance: state.balance,
ecoBalance: state.ecoBalance,
usdcBalance: state.usdcBalance,
walletType: state.walletType,
}),
//
// Balances (balance/ecoBalance/usdcBalance) are live data and MUST
// NOT be persisted: a stale MMKV snapshot would be served as current
// on cold start. They are re-fetched by useStellarWallet's useEffect
// on rehydration. Keep this in sync when WalletState gains fields.
partialize: partializeWalletState,
},
),
);
Loading