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
5 changes: 5 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ import {
registerForPushNotifications,
sendTokenToServer,
} from './src/services/notifications';
import { initWalletVault } from './src/services/walletVault';

function AppSync() {
const { syncPendingProofs } = useProofSubmit();

useEffect(() => {
void initWalletVault();
}, []);

useEffect(() => {
const sub = AppState.addEventListener('change', (state: AppStateStatus) => {
if (state === 'active') {
Expand Down
27 changes: 27 additions & 0 deletions __mocks__/react-native-keychain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const store = {};

const serviceKey = opts => (opts && opts.service) || 'default';

export const ACCESSIBLE = {
WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'AccessibleWhenUnlockedThisDeviceOnly',
};

export const SECURITY_LEVEL = {
SECURE_HARDWARE: 'secureHardware',
ANY: 'any',
};

export const setGenericPassword = jest.fn(async (username, password, opts) => {
store[serviceKey(opts)] = { username, password };
return true;
});

export const getGenericPassword = jest.fn(async opts => {
const key = serviceKey(opts);
return key in store ? store[key] : false;
});

export const resetGenericPassword = jest.fn(async opts => {
delete store[serviceKey(opts)];
return true;
});
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react": "18.2.0",
"react-native": "0.73.6",
"react-native-config": "^1.6.1",
"react-native-keychain": "^8.2.0",
"react-native-maps": "1.14.0",
"react-native-mmkv": "^2.12.0",
"react-native-safe-area-context": "^4.8.0",
Expand Down
124 changes: 105 additions & 19 deletions src/__tests__/__mocks__/setup.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,88 @@
const __MMKV_STORE__: Record<string, string> = {};
const mockMMKVInstances: Record<string, Record<string, string>> = {
default: {},
};
const mockMMKVDefaultId = 'default';

function mockGetInstance(id: string | undefined) {
const instanceId = id ?? mockMMKVDefaultId;
let store = mockMMKVInstances[instanceId];
if (!store) {
store = {};
mockMMKVInstances[instanceId] = store;
}
const instanceStore: Record<string, string> = store;
return {
getString: (key: string) =>
key in instanceStore ? instanceStore[key] : null,
set: (key: string, value: string) => {
instanceStore[key] = value;
},
delete: (key: string) => {
delete instanceStore[key];
},
getAllKeys: () => Object.keys(instanceStore),
clear: () => {
for (const key of Object.keys(instanceStore)) {
delete instanceStore[key];
}
},
};
}

jest.mock('react-native-mmkv', () => {
const MMKV = jest
.fn()
.mockImplementation((options?: { id?: string }) =>
mockGetInstance(options?.id),
);
(MMKV as unknown as { removeMMKV: jest.Mock }).removeMMKV = jest.fn(
(id: string) => {
delete mockMMKVInstances[id];
},
);
return {
MMKV: jest.fn().mockImplementation(() => ({
getString: (key: string) => __MMKV_STORE__[key] ?? null,
set: (key: string, value: string) => {
__MMKV_STORE__[key] = value;
},
delete: (key: string) => {
delete __MMKV_STORE__[key];
},
})),
MMKV,
removeMMKV: (MMKV as unknown as { removeMMKV: jest.Mock }).removeMMKV,
};
});

jest.mock(
'react-native-keychain',
() => {
const store: Record<string, { username: string; password: string }> = {};
const serviceKey = (opts?: { service?: string }) =>
opts?.service || 'default';
return {
ACCESSIBLE: {
WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'AccessibleWhenUnlockedThisDeviceOnly',
},
SECURITY_LEVEL: {
SECURE_HARDWARE: 'secureHardware',
ANY: 'any',
},
setGenericPassword: jest.fn(
async (
username: string,
password: string,
opts?: { service?: string },
) => {
store[serviceKey(opts)] = { username, password };
return true;
},
),
getGenericPassword: jest.fn(async (opts?: { service?: string }) => {
const key = serviceKey(opts);
return key in store ? store[key] : false;
}),
resetGenericPassword: jest.fn(async (opts?: { service?: string }) => {
delete store[serviceKey(opts)];
return true;
}),
};
},
{ virtual: true },
);

type ZustandSet = (...args: unknown[]) => void;
type ZustandGet = (...args: unknown[]) => unknown;
type ZustandStateCreator = (
Expand Down Expand Up @@ -52,14 +121,31 @@ jest.mock('zustand/middleware', () => {
return config(set, get, api);
};
},
createJSONStorage: () => ({
getItem: (name: string) => __MMKV_STORE__[name] ?? null,
setItem: (name: string, value: string) => {
__MMKV_STORE__[name] = value;
},
removeItem: (name: string) => {
delete __MMKV_STORE__[name];
},
}),
createJSONStorage: (getStorage?: () => unknown) => {
const storage = (getStorage ? getStorage() : undefined) as
| {
getItem: (name: string) => string | null;
setItem: (name: string, value: string) => void;
removeItem: (name: string) => void;
}
| undefined;
if (!storage) {
const fallback = mockMMKVInstances[mockMMKVDefaultId]!;
return {
getItem: (name: string) => fallback[name] ?? null,
setItem: (name: string, value: string) => {
fallback[name] = value;
},
removeItem: (name: string) => {
delete fallback[name];
},
};
}
return {
getItem: (name: string) => storage.getItem(name) ?? null,
setItem: (name: string, value: string) => storage.setItem(name, value),
removeItem: (name: string) => storage.removeItem(name),
};
},
};
});
9 changes: 7 additions & 2 deletions src/__tests__/useStellarWallet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import React from 'react';
import renderer, { act } from 'react-test-renderer';
import { useStellarWallet } from '../hooks/useStellarWallet';
import { useWalletStore } from '../store/walletStore';
import { getInAppSecret, clearInAppSecret } from '../services/walletVault';
import {
getInAppSecret,
clearInAppSecret,
initWalletVault,
} from '../services/walletVault';
import * as stellarMock from '../services/stellar';
import * as lobstrMock from '../services/lobstr';

Expand Down Expand Up @@ -168,9 +172,10 @@ async function renderProbe() {
describe('useStellarWallet connect flow', () => {
let tree: renderer.ReactTestRenderer | null = null;

beforeEach(() => {
beforeEach(async () => {
jest.clearAllMocks();
resetWalletStore();
await initWalletVault();
// The MMKV mock store persists across tests in a file — clear vault
// entries so secret-hygiene assertions start from a clean slate.
clearInAppSecret(IN_APP_PK);
Expand Down
Loading
Loading