88 * Covers:
99 * - subscriptionStore: add/fetch, update (field preservation), delete (cleanup),
1010 * persistence, multi-action workflows, error recovery
11- * - walletStore: connect/persist, load-from-storage, disconnect cleanup,
12- * multi-action workflow, crypto stream create → cancel
11+ * - walletStore (#62 + #69): consolidated with walletServiceManager as single
12+ * source of truth; network mismatch detection; crypto stream create → cancel
1313 */
1414
1515import { act } from 'react' ;
@@ -18,6 +18,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
1818import { useSubscriptionStore } from '../subscriptionStore' ;
1919import { useInvoiceStore } from '../invoiceStore' ;
2020import { useWalletStore } from '../walletStore' ;
21+ import { walletServiceManager } from '../../services/walletService' ;
2122import { SubscriptionCategory , BillingCycle } from '../../types/subscription' ;
2223import { BILLING_CONVERSIONS } from '../../utils/constants/values' ;
2324import { TaxType } from '../../types/invoice' ;
@@ -51,6 +52,79 @@ jest.mock('../../services/notificationService', () => ({
5152 presentLocalNotification : jest . fn ( ( ) => Promise . resolve ( ) ) ,
5253} ) ) ;
5354
55+ // Mock networkService to avoid AsyncStorage calls in walletStore.setPreferredNetwork.
56+ jest . mock ( '../../services/networkService' , ( ) => ( {
57+ networkService : {
58+ getSelectedNetwork : jest . fn ( ( ) => Promise . resolve ( null ) ) ,
59+ setSelectedNetwork : jest . fn ( ( ) => Promise . resolve ( true ) ) ,
60+ checkNetworkHealth : jest . fn ( ( ) => Promise . resolve ( { healthy : true } ) ) ,
61+ getAvailableNetworks : jest . fn ( ( ) => Promise . resolve ( [ ] ) ) ,
62+ } ,
63+ } ) ) ;
64+
65+ // Mock walletService so tests don't require ethers / Superfluid / native modules.
66+ // We expose a real WalletServiceManager-like singleton so the store's listener
67+ // subscription and setConnection/getConnection calls work correctly.
68+ jest . mock ( '../../services/walletService' , ( ) => {
69+ type Listener = ( conn : MockConnection | null ) => void ;
70+ type MockConnection = { address : string ; chainId : number ; isConnected : boolean } ;
71+
72+ class MockWalletServiceManager {
73+ private static _instance : MockWalletServiceManager ;
74+ private _connection : MockConnection | null = null ;
75+ private _listeners : Listener [ ] = [ ] ;
76+
77+ static getInstance ( ) {
78+ if ( ! MockWalletServiceManager . _instance ) {
79+ MockWalletServiceManager . _instance = new MockWalletServiceManager ( ) ;
80+ }
81+ return MockWalletServiceManager . _instance ;
82+ }
83+
84+ setConnection ( conn : MockConnection | null ) {
85+ this . _connection = conn ;
86+ this . _listeners . forEach ( ( l ) => l ( conn ) ) ;
87+ }
88+
89+ getConnection ( ) {
90+ return this . _connection ;
91+ }
92+
93+ addListener ( l : Listener ) {
94+ this . _listeners . push ( l ) ;
95+ }
96+
97+ removeListener ( l : Listener ) {
98+ const i = this . _listeners . indexOf ( l ) ;
99+ if ( i > - 1 ) this . _listeners . splice ( i , 1 ) ;
100+ }
101+
102+ async disconnectWallet ( ) {
103+ this . setConnection ( null ) ;
104+ }
105+
106+ async initialize ( ) { }
107+
108+ isConnected ( ) {
109+ return this . _connection ?. isConnected ?? false ;
110+ }
111+ }
112+
113+ const instance = MockWalletServiceManager . getInstance ( ) ;
114+
115+ return {
116+ WalletServiceManager : MockWalletServiceManager ,
117+ walletServiceManager : instance ,
118+ PaymentMethodService : { getInstance : ( ) => ( { canAddMethod : jest . fn ( ) , validatePaymentMethodForm : jest . fn ( ) , isDuplicateMethod : jest . fn ( ) , generateId : jest . fn ( ) , verifyPaymentMethod : jest . fn ( ) , processPaymentWithFallback : jest . fn ( ) , getExpiredMethods : jest . fn ( ( ) => [ ] ) , getExpiringSoonMethods : jest . fn ( ( ) => [ ] ) , checkExpiry : jest . fn ( ) , getPrimaryMethods : jest . fn ( ( ) => [ ] ) , getBackupMethods : jest . fn ( ( ) => [ ] ) , getFallbackMethods : jest . fn ( ( ) => [ ] ) , detectTokenContractUpgrade : jest . fn ( ) } ) } ,
119+ PaymentMethodError : class PaymentMethodError extends Error { constructor ( public code : string , msg : string ) { super ( msg ) ; } } ,
120+ PaymentMethodErrorCode : { DUPLICATE : 'DUPLICATE' , INVALID_TOKEN : 'INVALID_TOKEN' , MAX_METHODS : 'MAX_METHODS' , VERIFICATION_FAILED : 'VERIFICATION_FAILED' } ,
121+ WalletError : class WalletError extends Error { } ,
122+ WalletErrorCode : { } ,
123+ errorTracker : { record : jest . fn ( ) } ,
124+ default : instance ,
125+ } ;
126+ } ) ;
127+
54128// ── Helpers ───────────────────────────────────────────────────────────────────
55129const emptyStats = {
56130 totalActive : 0 ,
@@ -472,8 +546,13 @@ describe('subscriptionStore integration', () => {
472546} ) ;
473547
474548// ═════════════════════════════════════════════════════════════════════════════
475- // walletStore
549+ // walletStore — consolidated with walletServiceManager (#62)
476550// ═════════════════════════════════════════════════════════════════════════════
551+
552+ // walletServiceManager is the single source of truth for connection state.
553+ // The store derives address/chainId/network/isConnected from it via a listener.
554+ // There is no longer a `wallet` property or a `@subtrackr_wallet` storage key.
555+
477556describe ( 'walletStore integration' , ( ) => {
478557 // ── Connect loads persisted data and syncs with service ────────────────────
479558 it ( 'connectWallet loads persisted payment methods and attempts' , async ( ) => {
@@ -498,8 +577,20 @@ describe('walletStore integration', () => {
498577 ] ) ;
499578 mockMemoryStore . set ( '@subtrackr_payment_methods' , mockPaymentMethods ) ;
500579
580+ const { address, isConnected, isLoading } = useWalletStore . getState ( ) ;
581+ expect ( address ) . toBeNull ( ) ;
582+ expect ( isConnected ) . toBe ( false ) ;
583+ expect ( isLoading ) . toBe ( false ) ;
584+ } ) ;
585+
586+ // ── syncWalletConnection updates store via walletServiceManager ─────────────
587+ it ( 'syncWalletConnection sets connection state through walletServiceManager' , async ( ) => {
501588 await act ( async ( ) => {
502- await useWalletStore . getState ( ) . connectWallet ( ) ;
589+ await useWalletStore . getState ( ) . syncWalletConnection ( {
590+ address : '0xDEF456' ,
591+ chainId : 137 ,
592+ network : 'Polygon' ,
593+ } ) ;
503594 } ) ;
504595
505596 const { paymentMethods, isLoading } = useWalletStore . getState ( ) ;
@@ -598,7 +689,35 @@ describe('walletStore integration', () => {
598689
599690 // walletService.disconnectWallet is being called, no need to mock AsyncStorage
600691 await act ( async ( ) => {
601- await useWalletStore . getState ( ) . disconnect ( ) ;
692+ await useWalletStore . getState ( ) . connectWallet ( ) ;
693+ } ) ;
694+
695+ useWalletStore . setState ( {
696+ preferredNetwork : { id : 'ethereum' , name : 'Ethereum' , type : 'evm' , chainId : 1 } ,
697+ networkMismatch : { connectedChainId : 137 , preferredNetwork : { id : 'ethereum' , name : 'Ethereum' , type : 'evm' , chainId : 1 } } ,
698+ } ) ;
699+
700+ act ( ( ) => {
701+ useWalletStore . getState ( ) . detectNetworkMismatch ( ) ;
702+ } ) ;
703+
704+ expect ( useWalletStore . getState ( ) . networkMismatch ) . toBeNull ( ) ;
705+ } ) ;
706+
707+ // ── Network detection (#69): Stellar networks never mismatch ────────────────
708+ it ( 'detectNetworkMismatch ignores Stellar networks (no numeric chainId)' , async ( ) => {
709+ walletServiceManager . setConnection ( { address : '0xABC' , chainId : 1 , isConnected : true } ) ;
710+
711+ await act ( async ( ) => {
712+ await useWalletStore . getState ( ) . connectWallet ( ) ;
713+ } ) ;
714+
715+ useWalletStore . setState ( {
716+ preferredNetwork : { id : 'stellar-testnet' , name : 'Stellar Testnet' , type : 'stellar' } ,
717+ } ) ;
718+
719+ act ( ( ) => {
720+ useWalletStore . getState ( ) . detectNetworkMismatch ( ) ;
602721 } ) ;
603722
604723 // Should complete without error in normal flow
0 commit comments