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
65 changes: 30 additions & 35 deletions .github/workflows/mobile-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,48 @@ name: Mobile CI

on:
push:
branches: [main, develop]
branches: [ main, develop ]
paths:
- "apps/mobile/**"
- ".github/workflows/mobile-ci.yml"
- 'apps/mobile/**'
- '.github/workflows/mobile-ci.yml'
pull_request:
branches: [main, develop]
branches: [ main, develop ]
paths:
- "apps/mobile/**"
- ".github/workflows/mobile-ci.yml"
- 'apps/mobile/**'
- '.github/workflows/mobile-ci.yml'

jobs:
type-check:
name: Type Check
build:
name: Lint, Type-check, and Test
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/mobile
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Type check
run: pnpm type-check
continue-on-error: true

test:
name: Unit Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/mobile
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Checkout code
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
node-version: '20'
cache: 'pnpm'
cache-dependency-path: apps/mobile/pnpm-lock.yaml

- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Run tests
run: pnpm test -- --passWithNoTests
continue-on-error: true
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm run lint

- name: Type-check
run: pnpm run type-check

- name: Test
run: pnpm run test
22 changes: 22 additions & 0 deletions apps/mobile/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "off"
},
"ignorePatterns": ["node_modules/", "dist/", "build/"]
}
4 changes: 2 additions & 2 deletions apps/mobile/__tests__/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,13 @@ describe('toFriendlyError', () => {
it('maps 500 with testnet context', () => {
const result = toFriendlyError({ response: { status: 500 } });
expect(result.isRetryable).toBe(true);
expect(result.message).toContain('testnet');
expect(result.message.toLowerCase()).toContain('testnet');
});

it('maps 503 with testnet context', () => {
const result = toFriendlyError({ response: { status: 503 } });
expect(result.isRetryable).toBe(true);
expect(result.message).toContain('testnet');
expect(result.message.toLowerCase()).toContain('testnet');
});

it('maps 401 to session expired', () => {
Expand Down
68 changes: 34 additions & 34 deletions apps/mobile/__tests__/session.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
/**
* #549/#550 — SecureStore session layer + access-mode guards.
*/
const store = new Map<string, string>();
const asyncStore = new Map<string, string>();
const mockStore = new Map<string, string>();
const mockAsyncStore = new Map<string, string>();

jest.mock('../utils/secureStore', () => ({
saveSecureItem: jest.fn(async (key: string, value: string) => {
store.set(key, value);
mockStore.set(key, value);
}),
getSecureItem: jest.fn(async (key: string) => store.get(key) ?? null),
getSecureItem: jest.fn(async (key: string) => mockStore.get(key) ?? null),
deleteSecureItem: jest.fn(async (key: string) => {
store.delete(key);
mockStore.delete(key);
}),
}));

jest.mock('@react-native-async-storage/async-storage', () => ({
setItem: jest.fn(async (key: string, value: string) => {
asyncStore.set(key, value);
mockAsyncStore.set(key, value);
}),
getItem: jest.fn(async (key: string) => asyncStore.get(key) ?? null),
getAllKeys: jest.fn(async () => Array.from(asyncStore.keys())),
multiRemove: jest.fn(async (keys: string[]) => {
keys.forEach((key) => asyncStore.delete(key));
getItem: jest.fn(async (key: string) => mockAsyncStore.get(key) ?? null),
getAllKeys: jest.fn(async () => Array.from(mockAsyncStore.keys())),
removeMany: jest.fn(async (keys: string[]) => {
keys.forEach((key) => mockAsyncStore.delete(key));
}),
removeItem: jest.fn(async (key: string) => {
asyncStore.delete(key);
mockAsyncStore.delete(key);
}),
}));

Expand Down Expand Up @@ -60,8 +60,8 @@ const SESSION = {
};

beforeEach(() => {
store.clear();
asyncStore.clear();
mockStore.clear();
mockAsyncStore.clear();
__resetSessionForTests();
__resetAuthForTests();
});
Expand All @@ -77,9 +77,9 @@ describe('session store', () => {
await saveSession(SESSION);

expect(getAccessToken()).toBe(SESSION.accessToken);
expect(store.get('vaultix-access-token')).toBe(SESSION.accessToken);
expect(store.get('vaultix-refresh-token')).toBe(SESSION.refreshToken);
expect(store.get('vaultix-session-address')).toBe(SESSION.walletAddress);
expect(mockStore.get('vaultix-access-token')).toBe(SESSION.accessToken);
expect(mockStore.get('vaultix-refresh-token')).toBe(SESSION.refreshToken);
expect(mockStore.get('vaultix-session-address')).toBe(SESSION.walletAddress);
});

it('hydrates a previously persisted session', async () => {
Expand All @@ -94,7 +94,7 @@ describe('session store', () => {
});

it('hydrates to null when only part of the session survived', async () => {
store.set('vaultix-access-token', 'orphan');
mockStore.set('vaultix-access-token', 'orphan');
expect(await hydrateSession()).toBeNull();
});

Expand All @@ -112,7 +112,7 @@ describe('session store', () => {
await clearSession();

expect(getSession()).toBeNull();
expect(store.size).toBe(0);
expect(mockStore.size).toBe(0);
});

it('notifies subscribers on change', async () => {
Expand Down Expand Up @@ -160,25 +160,25 @@ describe('access modes', () => {
await signOut();

expect(getAccessMode()).toBe('anonymous');
expect(store.size).toBe(0);
expect(mockStore.size).toBe(0);
});

it('logout clears session, guest mode, and cached escrow data (#549)', async () => {
await saveSession(SESSION);
enterGuestMode();

// Simulate cached data
asyncStore.set('dashboard_cache', JSON.stringify({ data: 'test' }));
asyncStore.set('escrow_detail_123', JSON.stringify({ data: 'escrow' }));
asyncStore.set('other_key', 'should_remain');
mockAsyncStore.set('dashboard_cache', JSON.stringify({ data: 'test' }));
mockAsyncStore.set('escrow_detail_123', JSON.stringify({ data: 'escrow' }));
mockAsyncStore.set('other_key', 'should_remain');

await logout();

expect(getAccessMode()).toBe('anonymous');
expect(store.size).toBe(0);
expect(asyncStore.has('dashboard_cache')).toBe(false);
expect(asyncStore.has('escrow_detail_123')).toBe(false);
expect(asyncStore.has('other_key')).toBe(true);
expect(mockStore.size).toBe(0);
expect(mockAsyncStore.has('dashboard_cache')).toBe(false);
expect(mockAsyncStore.has('escrow_detail_123')).toBe(false);
expect(mockAsyncStore.has('other_key')).toBe(true);
});
});

Expand Down Expand Up @@ -227,17 +227,17 @@ describe('route guards', () => {

describe('clearAllCache (#549)', () => {
it('removes only cache keys, leaving other AsyncStorage data intact', async () => {
asyncStore.set('dashboard_cache', JSON.stringify({ data: 'test' }));
asyncStore.set('escrow_detail_abc', JSON.stringify({ data: 'escrow1' }));
asyncStore.set('escrow_detail_def', JSON.stringify({ data: 'escrow2' }));
asyncStore.set('user_preferences', JSON.stringify({ theme: 'dark' }));
mockAsyncStore.set('dashboard_cache', JSON.stringify({ data: 'test' }));
mockAsyncStore.set('escrow_detail_abc', JSON.stringify({ data: 'escrow1' }));
mockAsyncStore.set('escrow_detail_def', JSON.stringify({ data: 'escrow2' }));
mockAsyncStore.set('user_preferences', JSON.stringify({ theme: 'dark' }));

await clearAllCache();

expect(asyncStore.has('dashboard_cache')).toBe(false);
expect(asyncStore.has('escrow_detail_abc')).toBe(false);
expect(asyncStore.has('escrow_detail_def')).toBe(false);
expect(asyncStore.has('user_preferences')).toBe(true);
expect(mockAsyncStore.has('dashboard_cache')).toBe(false);
expect(mockAsyncStore.has('escrow_detail_abc')).toBe(false);
expect(mockAsyncStore.has('escrow_detail_def')).toBe(false);
expect(mockAsyncStore.has('user_preferences')).toBe(true);
});

it('handles empty cache gracefully', async () => {
Expand Down
16 changes: 8 additions & 8 deletions apps/mobile/__tests__/walletAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ jest.mock('../services/wallet', () => ({
signMessage: jest.fn(),
}));

const store = new Map<string, string>();
const mockStore = new Map<string, string>();
jest.mock('../utils/secureStore', () => ({
saveSecureItem: jest.fn(async (key: string, value: string) => {
store.set(key, value);
mockStore.set(key, value);
}),
getSecureItem: jest.fn(async (key: string) => store.get(key) ?? null),
getSecureItem: jest.fn(async (key: string) => mockStore.get(key) ?? null),
deleteSecureItem: jest.fn(async (key: string) => {
store.delete(key);
mockStore.delete(key);
}),
}));

Expand All @@ -39,7 +39,7 @@ const mockChallenge = authApi.requestChallenge as jest.Mock;
const mockVerify = authApi.verifySignature as jest.Mock;

beforeEach(() => {
store.clear();
mockStore.clear();
__resetSessionForTests();
jest.clearAllMocks();

Expand All @@ -65,8 +65,8 @@ describe('signInWithBuiltInWallet', () => {

expect(session.walletAddress).toBe(ADDRESS);
expect(getAccessToken()).toBe('access.jwt');
expect(store.get('vaultix-access-token')).toBe('access.jwt');
expect(store.get('vaultix-refresh-token')).toBe('refresh.jwt');
expect(mockStore.get('vaultix-access-token')).toBe('access.jwt');
expect(mockStore.get('vaultix-refresh-token')).toBe('refresh.jwt');
});

it('never invents a token or address', async () => {
Expand All @@ -80,7 +80,7 @@ describe('signInWithBuiltInWallet', () => {

await expect(signInWithBuiltInWallet()).rejects.toMatchObject({ code: 'ERR_NETWORK' });
expect(getSession()).toBeNull();
expect(store.size).toBe(0);
expect(mockStore.size).toBe(0);
});

it('propagates a rejected signature without signing in', async () => {
Expand Down
Loading
Loading