diff --git a/src/__tests__/stores.test.ts b/src/__tests__/stores.test.ts index fcf986d..d6dae71 100644 --- a/src/__tests__/stores.test.ts +++ b/src/__tests__/stores.test.ts @@ -1,5 +1,5 @@ import './__mocks__/setup'; -import { useWalletStore } from '../store/walletStore'; +import { useWalletStore, partializeWalletState } from '../store/walletStore'; import { useTaskStore, partializeTaskState, @@ -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', () => { diff --git a/src/store/walletStore.ts b/src/store/walletStore.ts index b42753f..d260bdd 100644 --- a/src/store/walletStore.ts +++ b/src/store/walletStore.ts @@ -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()( persist( set => ({ @@ -123,14 +141,12 @@ export const useWalletStore = create()( // 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, }, ), );