Skip to content
Open
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
73 changes: 73 additions & 0 deletions src/analytics/utils/lifecycleStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
getLifecycleVersion,
getLifecycleBuild,
setLifecycleVersionBuild,
LIFECYCLE_VERSION_KEY,
LIFECYCLE_BUILD_KEY,
} from './lifecycleStorage';
import AsyncStorage from '@react-native-async-storage/async-storage';

jest.mock('@react-native-async-storage/async-storage', () => ({
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
}));

describe('lifecycleStorage', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('reads the lifecycle version key from AsyncStorage', async () => {
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('1.4.0');

const version = await getLifecycleVersion();

expect(version).toBe('1.4.0');
expect(AsyncStorage.getItem).toHaveBeenCalledWith(LIFECYCLE_VERSION_KEY);
});

it('reads the lifecycle build key from AsyncStorage', async () => {
(AsyncStorage.getItem as jest.Mock).mockResolvedValue('42');

const build = await getLifecycleBuild();

expect(build).toBe('42');
expect(AsyncStorage.getItem).toHaveBeenCalledWith(LIFECYCLE_BUILD_KEY);
});

it('returns null when version key is missing', async () => {
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(null);

expect(await getLifecycleVersion()).toBeNull();
expect(await getLifecycleBuild()).toBeNull();
});

it('returns null when AsyncStorage throws on read', async () => {
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('boom'));

expect(await getLifecycleVersion()).toBeNull();
expect(await getLifecycleBuild()).toBeNull();
});

it('writes both version and build to AsyncStorage', async () => {
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);

await setLifecycleVersionBuild('1.4.0', '42');

expect(AsyncStorage.setItem).toHaveBeenCalledWith(
LIFECYCLE_VERSION_KEY,
'1.4.0'
);
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
LIFECYCLE_BUILD_KEY,
'42'
);
});

it('does not throw if write fails', async () => {
(AsyncStorage.setItem as jest.Mock).mockRejectedValue(new Error('fail'));

await expect(setLifecycleVersionBuild('1.0', '1')).resolves.toBeUndefined();
});
});
40 changes: 40 additions & 0 deletions src/analytics/utils/lifecycleStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import AsyncStorage from '@react-native-async-storage/async-storage';

export const LIFECYCLE_VERSION_KEY = 'metarouter:lifecycle:version';
export const LIFECYCLE_BUILD_KEY = 'metarouter:lifecycle:build';

/**
* Storage for app lifecycle state (last-seen version + build). Lives in a
* dedicated module so neither IdentityManager.reset() nor the client's reset()
* can wipe install/update history. Errors are swallowed so missing or
* unavailable storage is treated as "no prior lifecycle state" — the caller
* decides whether that means install or update.
*/

export async function getLifecycleVersion(): Promise<string | null> {
try {
return await AsyncStorage.getItem(LIFECYCLE_VERSION_KEY);
} catch {
return null;
}
}

export async function getLifecycleBuild(): Promise<string | null> {
try {
return await AsyncStorage.getItem(LIFECYCLE_BUILD_KEY);
} catch {
return null;
}
}

export async function setLifecycleVersionBuild(
version: string,
build: string
): Promise<void> {
try {
await AsyncStorage.setItem(LIFECYCLE_VERSION_KEY, version);
await AsyncStorage.setItem(LIFECYCLE_BUILD_KEY, build);
} catch {
// best-effort; cold-launch state will be re-derived on next run
}
}
Loading