diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index ded1addf..0aa7e473 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -13,15 +13,21 @@ const createJsonResponse = (payload: unknown) => const setupClientMocks = ({ isFirstTime = false, markSuccess = mock(() => {}), + reloadUpdate = mock(() => Promise.resolve()), + setNeedUpdate = mock(() => Promise.resolve()), downloadPatchFromPpk = mock(() => Promise.resolve()), downloadPatchFromPackage = mock(() => Promise.resolve()), downloadFullUpdate = mock(() => Promise.resolve()), + restartApp = mock(() => Promise.resolve()), }: { isFirstTime?: boolean; markSuccess?: ReturnType; + reloadUpdate?: ReturnType; + setNeedUpdate?: ReturnType; downloadPatchFromPpk?: ReturnType; downloadPatchFromPackage?: ReturnType; downloadFullUpdate?: ReturnType; + restartApp?: ReturnType; } = {}) => { (globalThis as any).__DEV__ = false; @@ -42,13 +48,13 @@ const setupClientMocks = ({ mock.module('../core', () => ({ PushyModule: { markSuccess, - reloadUpdate: mock(() => Promise.resolve()), - setNeedUpdate: mock(() => Promise.resolve()), + reloadUpdate, + setNeedUpdate, downloadPatchFromPpk, downloadPatchFromPackage, downloadFullUpdate, downloadAndInstallApk: mock(() => Promise.resolve()), - restartApp: mock(() => Promise.resolve()), + restartApp, }, buildTime: '2023-01-01', cInfo: { @@ -283,4 +289,78 @@ describe('Pushy server config', () => { }), ); }); + + test('waits for beforeReload before switching version', async () => { + const calls: string[] = []; + const reloadUpdate = mock(() => { + calls.push('reloadUpdate'); + return Promise.resolve(); + }); + const beforeReload = mock(async (context: any) => { + calls.push('beforeReload'); + expect(context).toEqual({ + type: 'switchVersion', + hash: 'next-hash', + }); + }); + setupClientMocks({ reloadUpdate }); + + const { Pushy, sharedState } = await importFreshClient('before-reload-switch-version'); + sharedState.downloadedHash = 'next-hash'; + const client = new Pushy({ + appKey: 'demo-app', + beforeReload, + }); + + await client.switchVersion('next-hash'); + + expect(calls).toEqual(['beforeReload', 'reloadUpdate']); + expect(beforeReload).toHaveBeenCalledTimes(1); + expect(reloadUpdate).toHaveBeenCalledWith({ hash: 'next-hash' }); + }); + + test('skips switching version when beforeReload returns false', async () => { + const reloadUpdate = mock(() => Promise.resolve()); + const beforeReload = mock(() => false); + setupClientMocks({ reloadUpdate }); + + const { Pushy, sharedState } = await importFreshClient('before-reload-skip-switch'); + sharedState.downloadedHash = 'next-hash'; + const client = new Pushy({ + appKey: 'demo-app', + beforeReload, + }); + + await client.switchVersion('next-hash'); + + expect(beforeReload).toHaveBeenCalledTimes(1); + expect(reloadUpdate).not.toHaveBeenCalled(); + expect(sharedState.applyingUpdate).toBe(false); + }); + + test('calls beforeReload before restartApp', async () => { + const calls: string[] = []; + const restartApp = mock(() => { + calls.push('restartApp'); + return Promise.resolve(); + }); + const beforeReload = mock(async (context: any) => { + calls.push('beforeReload'); + expect(context).toEqual({ + type: 'restartApp', + }); + }); + setupClientMocks({ restartApp }); + + const { Pushy } = await importFreshClient('before-reload-restart-app'); + const client = new Pushy({ + appKey: 'demo-app', + beforeReload, + }); + + await client.restartApp(); + + expect(calls).toEqual(['beforeReload', 'restartApp']); + expect(restartApp).toHaveBeenCalled(); + }); }); diff --git a/src/client.ts b/src/client.ts index a24c10db..6149c60f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -18,6 +18,7 @@ import { } from './core'; import { PermissionsAndroid } from './permissions'; import { + BeforeReloadContext, CheckResult, ClientOptions, EventType, @@ -218,6 +219,18 @@ export class Pushy { log('afterCheckUpdate failed:', error?.message || error); }); }; + runBeforeReload = async (context: BeforeReloadContext) => { + const { beforeReload } = this.options; + if (!beforeReload) { + return true; + } + const shouldReload = await beforeReload(context); + if (shouldReload === false) { + log('beforeReload returned false, skipping reload'); + return false; + } + return true; + }; getCheckUrl = (endpoint: string) => { return `${endpoint}/checkUpdate/${this.options.appKey}`; }; @@ -325,6 +338,15 @@ export class Pushy { if (assertHash(hash) && !sharedState.applyingUpdate) { log(`switchVersion: ${hash}`); sharedState.applyingUpdate = true; + try { + if (!(await this.runBeforeReload({ type: 'switchVersion', hash }))) { + sharedState.applyingUpdate = false; + return; + } + } catch (e) { + sharedState.applyingUpdate = false; + throw e; + } return PushyModule.reloadUpdate({ hash }); } }; @@ -668,6 +690,9 @@ export class Pushy { } }; restartApp = async () => { + if (!(await this.runBeforeReload({ type: 'restartApp' }))) { + return; + } return PushyModule.restartApp(); }; } diff --git a/src/type.ts b/src/type.ts index 12c4ea8c..cf498b34 100644 --- a/src/type.ts +++ b/src/type.ts @@ -88,6 +88,11 @@ export interface UpdateServerConfig { queryUrls?: string[]; } +export interface BeforeReloadContext { + type: 'switchVersion' | 'restartApp'; + hash?: string; +} + export interface ClientOptions { appKey: string; server?: UpdateServerConfig; @@ -109,6 +114,9 @@ export interface ClientOptions { afterCheckUpdate?: (state: UpdateCheckState) => Promise | void; beforeDownloadUpdate?: (info: CheckResult) => Promise | boolean; afterDownloadUpdate?: (info: CheckResult) => Promise | boolean; + beforeReload?: ( + context: BeforeReloadContext, + ) => Promise | boolean | void; onPackageExpired?: (info: CheckResult) => Promise | boolean; overridePackageVersion?: string; }