@symbiote-native/keep-awake 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/build/angular/index.d.ts +2 -0
- package/build/angular/index.js +4 -0
- package/build/angular/services/keep-awake.service/index.d.ts +5 -0
- package/build/angular/services/keep-awake.service/index.js +83 -0
- package/build/core/index.d.ts +3 -0
- package/build/core/index.js +1 -0
- package/build/core/keep-awake.d.ts +17 -0
- package/build/core/keep-awake.js +41 -0
- package/build/core/native-module.d.ts +9 -0
- package/build/core/native-module.js +3 -0
- package/build/core/types.d.ts +8 -0
- package/build/core/types.js +1 -0
- package/build/react/hooks/use-keep-awake/index.d.ts +2 -0
- package/build/react/hooks/use-keep-awake/index.js +35 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.js +5 -0
- package/build/vue/composables/use-keep-awake/index.d.ts +2 -0
- package/build/vue/composables/use-keep-awake/index.js +28 -0
- package/build/vue/index.d.ts +2 -0
- package/build/vue/index.js +5 -0
- package/build-ngc/angular/index.d.ts +2 -0
- package/build-ngc/angular/index.js +5 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/services/keep-awake.service/index.d.ts +8 -0
- package/build-ngc/angular/services/keep-awake.service/index.js +41 -0
- package/build-ngc/angular/services/keep-awake.service/index.js.map +1 -0
- package/build-ngc/core/index.d.ts +3 -0
- package/build-ngc/core/index.js +2 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/keep-awake.d.ts +17 -0
- package/build-ngc/core/keep-awake.js +42 -0
- package/build-ngc/core/keep-awake.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +9 -0
- package/build-ngc/core/native-module.js +4 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/types.d.ts +8 -0
- package/build-ngc/core/types.js +2 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +107 -0
- package/src/angular/index.ts +14 -0
- package/src/angular/services/keep-awake.service/index.ts +46 -0
- package/src/angular/services/keep-awake.service/keep-awake.service.test.ts +98 -0
- package/src/core/index.ts +9 -0
- package/src/core/keep-awake.test.ts +119 -0
- package/src/core/keep-awake.ts +50 -0
- package/src/core/native-module.ts +20 -0
- package/src/core/types.ts +14 -0
- package/src/react/hooks/use-keep-awake/index.ts +43 -0
- package/src/react/hooks/use-keep-awake/use-keep-awake.test.tsx +70 -0
- package/src/react/index.ts +16 -0
- package/src/vue/composables/use-keep-awake/index.ts +36 -0
- package/src/vue/composables/use-keep-awake/use-keep-awake.test.ts +76 -0
- package/src/vue/index.ts +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAIxD,MAAM,2BAA2B,GAAG,eAAe,CAAC;AAapD,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,CAC9C,2BAA2B,CAC5B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
|
package/native-link.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@symbiote-native/keep-awake",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "expo-keep-awake wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Keeps the screen awake for the lifetime of a mounted component/composable/service, with an optional listener for keep-awake state changes.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/OneEyed1366/symbiote-native.git",
|
|
9
|
+
"directory": "packages/keep-awake"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/keep-awake#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/OneEyed1366/symbiote-native/issues"
|
|
14
|
+
},
|
|
15
|
+
"author": "Andrey Prokopenko <psevdoproger@gmail.com>",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./build/core/index.js",
|
|
18
|
+
"module": "./build/core/index.js",
|
|
19
|
+
"types": "./build/core/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./build/core/index.d.ts",
|
|
23
|
+
"default": "./build/core/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./vue": {
|
|
26
|
+
"types": "./build/vue/index.d.ts",
|
|
27
|
+
"default": "./build/vue/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./react": {
|
|
30
|
+
"types": "./build/react/index.d.ts",
|
|
31
|
+
"default": "./build/react/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./angular": {
|
|
34
|
+
"types": "./build-ngc/angular/index.d.ts",
|
|
35
|
+
"react-native": "./build-ngc/angular/index.js",
|
|
36
|
+
"default": "./src/angular/index.ts"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"build",
|
|
42
|
+
"build-ngc",
|
|
43
|
+
"native-link.json"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"expo-keep-awake": "57.0.1",
|
|
50
|
+
"expo-modules-core": "57.0.5"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@symbiote-native/engine": ">=0.1.7",
|
|
54
|
+
"@angular/core": ">=20",
|
|
55
|
+
"@vue/runtime-core": "^3.5.13",
|
|
56
|
+
"react": ">=19.0.0",
|
|
57
|
+
"react-native": ">=0.86",
|
|
58
|
+
"vue": ">=3.5.0",
|
|
59
|
+
"@symbiote-native/angular": "0.6.1",
|
|
60
|
+
"@symbiote-native/react": "0.2.8",
|
|
61
|
+
"@symbiote-native/vue": "0.3.8"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@symbiote-native/angular": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"@symbiote-native/react": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@symbiote-native/vue": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@angular/core": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@vue/runtime-core": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"react": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"vue": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@angular/compiler": "^22",
|
|
88
|
+
"@angular/compiler-cli": "^22",
|
|
89
|
+
"@angular/core": "^22",
|
|
90
|
+
"@types/node": "^26.0.0",
|
|
91
|
+
"@types/react": "^19.2.0",
|
|
92
|
+
"@vue/runtime-core": "^3.5.13",
|
|
93
|
+
"react": "19.2.3",
|
|
94
|
+
"typescript": "~6.0.0",
|
|
95
|
+
"@symbiote-native/test-utils": "0.1.6",
|
|
96
|
+
"@symbiote-native/engine": "0.1.7",
|
|
97
|
+
"@symbiote-native/react": "0.2.8",
|
|
98
|
+
"@symbiote-native/angular": "0.6.1",
|
|
99
|
+
"@symbiote-native/vue": "0.3.8"
|
|
100
|
+
},
|
|
101
|
+
"scripts": {
|
|
102
|
+
"typecheck": "tsc --build",
|
|
103
|
+
"clean": "rm -rf build-ngc",
|
|
104
|
+
"ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
|
|
105
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// KeepAwakeService is the Angular-only lifecycle half; the free functions and event
|
|
2
|
+
// subscription plumbing all live in core, shared with React/Vue.
|
|
3
|
+
export { KeepAwakeService } from './services/keep-awake.service';
|
|
4
|
+
export {
|
|
5
|
+
ExpoKeepAwakeTag,
|
|
6
|
+
isAvailableAsync,
|
|
7
|
+
activateKeepAwakeAsync,
|
|
8
|
+
deactivateKeepAwake,
|
|
9
|
+
addListener,
|
|
10
|
+
type KeepAwakeEvent,
|
|
11
|
+
type KeepAwakeListener,
|
|
12
|
+
type KeepAwakeOptions,
|
|
13
|
+
type EventSubscription,
|
|
14
|
+
} from '../core';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { effect, inject, Injectable, Injector } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
activateKeepAwakeAsync,
|
|
4
|
+
addListener,
|
|
5
|
+
deactivateKeepAwake,
|
|
6
|
+
type KeepAwakeOptions,
|
|
7
|
+
} from '../../../core';
|
|
8
|
+
|
|
9
|
+
// Angular twin of React's `useKeepAwake` hook and Vue's `useKeepAwake` composable. See
|
|
10
|
+
// LowPowerModeService (packages/battery) for the `connect()`/`effect()` pattern rationale — same
|
|
11
|
+
// wiring, but keep-awake has no value to surface as a Signal since it's a pure side effect for
|
|
12
|
+
// the component's lifetime. Angular has no useId equivalent either, so the default tag comes
|
|
13
|
+
// from the same monotonically-incrementing module-local counter the Vue composable uses.
|
|
14
|
+
//
|
|
15
|
+
// constructor() { inject(KeepAwakeService).connect(); }
|
|
16
|
+
let tagCounter = 0;
|
|
17
|
+
|
|
18
|
+
@Injectable({ providedIn: 'root' })
|
|
19
|
+
export class KeepAwakeService {
|
|
20
|
+
private readonly injector = inject(Injector);
|
|
21
|
+
|
|
22
|
+
connect(tag?: string, options?: KeepAwakeOptions): void {
|
|
23
|
+
const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
|
|
24
|
+
|
|
25
|
+
effect(
|
|
26
|
+
onCleanup => {
|
|
27
|
+
activateKeepAwakeAsync(tagOrDefault)
|
|
28
|
+
.then(() => {
|
|
29
|
+
if (options?.listener) {
|
|
30
|
+
addListener(tagOrDefault, options.listener);
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.catch(() => {});
|
|
34
|
+
|
|
35
|
+
onCleanup(() => {
|
|
36
|
+
if (options?.suppressDeactivateWarnings) {
|
|
37
|
+
deactivateKeepAwake(tagOrDefault).catch(() => {});
|
|
38
|
+
} else {
|
|
39
|
+
deactivateKeepAwake(tagOrDefault);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
{ injector: this.injector },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Co-located Angular-driven test (ADR 0025) for KeepAwakeService. See battery's
|
|
2
|
+
// low-power-mode.service.test.ts for the shared rationale.
|
|
3
|
+
//
|
|
4
|
+
// Two host classes rather than one @Input-driven host: Angular sets @Input-bound properties
|
|
5
|
+
// AFTER construction, so a constructor-time `connect(this.tag)` read off an @Input would always
|
|
6
|
+
// see `undefined` — the same field-initializer/constructor timing every other service host in
|
|
7
|
+
// this repo relies on (see battery's own PermissionsHost/BatteryLevelHost).
|
|
8
|
+
|
|
9
|
+
import '@angular/compiler';
|
|
10
|
+
import { Component, inject } from '@angular/core';
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { mount, unmount } from '@symbiote-native/angular';
|
|
13
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
14
|
+
import { KeepAwakeService } from './index';
|
|
15
|
+
|
|
16
|
+
const activateKeepAwakeAsyncMock = vi.fn(async (_tag: string) => undefined);
|
|
17
|
+
const addListenerMock = vi.fn();
|
|
18
|
+
const deactivateKeepAwakeMock = vi.fn(async (_tag: string) => undefined);
|
|
19
|
+
|
|
20
|
+
vi.mock('../../../core', () => ({
|
|
21
|
+
activateKeepAwakeAsync: (tag: string) => activateKeepAwakeAsyncMock(tag),
|
|
22
|
+
addListener: (...args: unknown[]) => addListenerMock(...args),
|
|
23
|
+
deactivateKeepAwake: (tag: string) => deactivateKeepAwakeMock(tag),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const ROOT_TAG = 974;
|
|
27
|
+
const fabric = installFabric();
|
|
28
|
+
const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
|
|
29
|
+
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'symbiote-keep-awake-default-host',
|
|
32
|
+
standalone: true,
|
|
33
|
+
template: '',
|
|
34
|
+
})
|
|
35
|
+
class KeepAwakeDefaultHost {
|
|
36
|
+
constructor() {
|
|
37
|
+
inject(KeepAwakeService).connect();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Component({
|
|
42
|
+
selector: 'symbiote-keep-awake-custom-host',
|
|
43
|
+
standalone: true,
|
|
44
|
+
template: '',
|
|
45
|
+
})
|
|
46
|
+
class KeepAwakeCustomHost {
|
|
47
|
+
constructor() {
|
|
48
|
+
inject(KeepAwakeService).connect('custom-tag');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
activateKeepAwakeAsyncMock.mockClear();
|
|
54
|
+
addListenerMock.mockClear();
|
|
55
|
+
deactivateKeepAwakeMock.mockClear();
|
|
56
|
+
activateKeepAwakeAsyncMock.mockResolvedValue(undefined);
|
|
57
|
+
deactivateKeepAwakeMock.mockResolvedValue(undefined);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
unmount(ROOT_TAG);
|
|
62
|
+
fabric.reset();
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('KeepAwakeService.connect', () => {
|
|
67
|
+
it('activates a default tag on mount', async () => {
|
|
68
|
+
mount(ROOT_TAG, KeepAwakeDefaultHost);
|
|
69
|
+
await tick();
|
|
70
|
+
|
|
71
|
+
expect(activateKeepAwakeAsyncMock).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(typeof activateKeepAwakeAsyncMock.mock.calls[0][0]).toBe('string');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('activates the explicit tag when one is given', async () => {
|
|
76
|
+
mount(ROOT_TAG, KeepAwakeCustomHost);
|
|
77
|
+
await tick();
|
|
78
|
+
|
|
79
|
+
expect(activateKeepAwakeAsyncMock).toHaveBeenCalledWith('custom-tag');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('deactivates the same tag when the host component is unmounted', async () => {
|
|
83
|
+
mount(ROOT_TAG, KeepAwakeCustomHost);
|
|
84
|
+
await tick();
|
|
85
|
+
|
|
86
|
+
unmount(ROOT_TAG);
|
|
87
|
+
await tick();
|
|
88
|
+
|
|
89
|
+
expect(deactivateKeepAwakeMock).toHaveBeenCalledWith('custom-tag');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('never touches addListener when no options are given', async () => {
|
|
93
|
+
mount(ROOT_TAG, KeepAwakeCustomHost);
|
|
94
|
+
await tick();
|
|
95
|
+
|
|
96
|
+
expect(addListenerMock).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ExpoKeepAwakeTag,
|
|
3
|
+
isAvailableAsync,
|
|
4
|
+
activateKeepAwakeAsync,
|
|
5
|
+
deactivateKeepAwake,
|
|
6
|
+
addListener,
|
|
7
|
+
} from './keep-awake';
|
|
8
|
+
export type { KeepAwakeEvent, KeepAwakeListener, KeepAwakeOptions } from './types';
|
|
9
|
+
export type { EventSubscription } from 'expo-modules-core';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const FAKE_NATIVE_KEEP_AWAKE = {
|
|
4
|
+
isAvailableAsync: vi.fn(async () => true),
|
|
5
|
+
activate: vi.fn(async () => undefined),
|
|
6
|
+
deactivate: vi.fn(async () => undefined),
|
|
7
|
+
addListenerForTag: vi.fn(() => ({ remove: vi.fn() })),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// The real ExpoKeepAwake native module only exists on device — resolving it via
|
|
11
|
+
// requireNativeModule() at import time would throw in this headless test run, so the
|
|
12
|
+
// module-lookup file is faked in place of expo-modules-core's runtime resolution, same pattern
|
|
13
|
+
// packages/battery/src/core/battery.test.ts uses.
|
|
14
|
+
vi.mock('./native-module', () => ({
|
|
15
|
+
expoKeepAwake: FAKE_NATIVE_KEEP_AWAKE,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// expo-modules-core's real entry transitively imports 'react-native' for Platform/
|
|
19
|
+
// TurboModuleRegistry, whose Flow-typed source Vitest's Oxc transform can't parse — same fake
|
|
20
|
+
// packages/haptics/src/core/haptics.test.ts uses. keep-awake.ts imports UnavailabilityError as a
|
|
21
|
+
// real value (not just a type), so it needs mocking here too.
|
|
22
|
+
vi.mock('expo-modules-core', () => ({
|
|
23
|
+
UnavailabilityError: class UnavailabilityError extends Error {
|
|
24
|
+
constructor(moduleName: string, propertyName: string) {
|
|
25
|
+
super(`${propertyName} is not available on ${moduleName}`);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
ExpoKeepAwakeTag,
|
|
32
|
+
isAvailableAsync,
|
|
33
|
+
activateKeepAwakeAsync,
|
|
34
|
+
deactivateKeepAwake,
|
|
35
|
+
addListener,
|
|
36
|
+
} = await import('./keep-awake');
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('isAvailableAsync', () => {
|
|
43
|
+
it('delegates to the native module when isAvailableAsync exists', async () => {
|
|
44
|
+
await expect(isAvailableAsync()).resolves.toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('defaults to true when the native method is absent', async () => {
|
|
48
|
+
const { isAvailableAsync: native } = FAKE_NATIVE_KEEP_AWAKE;
|
|
49
|
+
// @ts-expect-error -- simulating a platform where the native module has no such method
|
|
50
|
+
FAKE_NATIVE_KEEP_AWAKE.isAvailableAsync = undefined;
|
|
51
|
+
|
|
52
|
+
await expect(isAvailableAsync()).resolves.toBe(true);
|
|
53
|
+
|
|
54
|
+
FAKE_NATIVE_KEEP_AWAKE.isAvailableAsync = native;
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('activateKeepAwakeAsync', () => {
|
|
59
|
+
it('activates the default tag when none is given', async () => {
|
|
60
|
+
await activateKeepAwakeAsync();
|
|
61
|
+
|
|
62
|
+
expect(FAKE_NATIVE_KEEP_AWAKE.activate).toHaveBeenCalledWith(ExpoKeepAwakeTag);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('activates an explicit tag', async () => {
|
|
66
|
+
await activateKeepAwakeAsync('custom-tag');
|
|
67
|
+
|
|
68
|
+
expect(FAKE_NATIVE_KEEP_AWAKE.activate).toHaveBeenCalledWith('custom-tag');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('deactivateKeepAwake', () => {
|
|
73
|
+
it('deactivates the default tag when none is given', async () => {
|
|
74
|
+
await deactivateKeepAwake();
|
|
75
|
+
|
|
76
|
+
expect(FAKE_NATIVE_KEEP_AWAKE.deactivate).toHaveBeenCalledWith(ExpoKeepAwakeTag);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('deactivates an explicit tag', async () => {
|
|
80
|
+
await deactivateKeepAwake('custom-tag');
|
|
81
|
+
|
|
82
|
+
expect(FAKE_NATIVE_KEEP_AWAKE.deactivate).toHaveBeenCalledWith('custom-tag');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('addListener', () => {
|
|
87
|
+
it('subscribes the default tag when called with a bare listener', () => {
|
|
88
|
+
const listener = vi.fn();
|
|
89
|
+
addListener(listener);
|
|
90
|
+
|
|
91
|
+
expect(FAKE_NATIVE_KEEP_AWAKE.addListenerForTag).toHaveBeenCalledWith(
|
|
92
|
+
ExpoKeepAwakeTag,
|
|
93
|
+
listener,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('subscribes an explicit tag when given tag + listener', () => {
|
|
98
|
+
const listener = vi.fn();
|
|
99
|
+
addListener('custom-tag', listener);
|
|
100
|
+
|
|
101
|
+
expect(FAKE_NATIVE_KEEP_AWAKE.addListenerForTag).toHaveBeenCalledWith('custom-tag', listener);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('throws an UnavailabilityError-shaped error when addListenerForTag is absent', () => {
|
|
105
|
+
const { addListenerForTag: native } = FAKE_NATIVE_KEEP_AWAKE;
|
|
106
|
+
// @ts-expect-error -- simulating a platform where the native module has no such method
|
|
107
|
+
FAKE_NATIVE_KEEP_AWAKE.addListenerForTag = undefined;
|
|
108
|
+
|
|
109
|
+
expect(() => addListener(vi.fn())).toThrow(
|
|
110
|
+
'addListenerForTag is not available on ExpoKeepAwake',
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
FAKE_NATIVE_KEEP_AWAKE.addListenerForTag = native;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('throws when no listener can be resolved', () => {
|
|
117
|
+
expect(() => addListener('custom-tag')).toThrow('addListener requires a listener function');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Hand-ported from .vendors/expo/packages/expo-keep-awake/src/index.ts (sdk-57, confirmed
|
|
2
|
+
// against origin/sdk-57 — the .vendors/expo working tree sits on `main` and differs
|
|
3
|
+
// cosmetically). Unlike battery (which falls back to a sentinel when a method is missing),
|
|
4
|
+
// upstream keep-awake throws from `addListener` when the native module lacks
|
|
5
|
+
// `addListenerForTag`, matching local-auth/haptics's UnavailabilityError convention.
|
|
6
|
+
import { UnavailabilityError, type EventSubscription } from 'expo-modules-core';
|
|
7
|
+
import { expoKeepAwake } from './native-module';
|
|
8
|
+
import type { KeepAwakeListener } from './types';
|
|
9
|
+
|
|
10
|
+
export const ExpoKeepAwakeTag = 'ExpoKeepAwakeDefaultTag';
|
|
11
|
+
|
|
12
|
+
/** Resolves with whether the keep-awake API is available on this device. */
|
|
13
|
+
export async function isAvailableAsync(): Promise<boolean> {
|
|
14
|
+
if (expoKeepAwake.isAvailableAsync) {
|
|
15
|
+
return await expoKeepAwake.isAvailableAsync();
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Activates a keep-awake lock under `tag` (the shared default tag when none is given) — the
|
|
22
|
+
* screen stays on for as long as any tag holds an active lock.
|
|
23
|
+
*/
|
|
24
|
+
export async function activateKeepAwakeAsync(tag: string = ExpoKeepAwakeTag): Promise<void> {
|
|
25
|
+
await expoKeepAwake.activate?.(tag);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Releases the keep-awake lock held under `tag` (the shared default tag when none is given). */
|
|
29
|
+
export async function deactivateKeepAwake(tag: string = ExpoKeepAwakeTag): Promise<void> {
|
|
30
|
+
await expoKeepAwake.deactivate?.(tag);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Subscribes to keep-awake state changes for a tag. Overloaded to accept a bare listener for the
|
|
35
|
+
* default tag, matching upstream's own `addListener` signature.
|
|
36
|
+
*/
|
|
37
|
+
export function addListener(
|
|
38
|
+
tagOrListener: string | KeepAwakeListener,
|
|
39
|
+
listener?: KeepAwakeListener,
|
|
40
|
+
): EventSubscription {
|
|
41
|
+
if (!expoKeepAwake.addListenerForTag) {
|
|
42
|
+
throw new UnavailabilityError('ExpoKeepAwake', 'addListenerForTag');
|
|
43
|
+
}
|
|
44
|
+
const tag = typeof tagOrListener === 'string' ? tagOrListener : ExpoKeepAwakeTag;
|
|
45
|
+
const resolvedListener = typeof tagOrListener === 'function' ? tagOrListener : listener;
|
|
46
|
+
if (!resolvedListener) {
|
|
47
|
+
throw new TypeError('addListener requires a listener function');
|
|
48
|
+
}
|
|
49
|
+
return expoKeepAwake.addListenerForTag(tag, resolvedListener);
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
import type { EventSubscription } from 'expo-modules-core';
|
|
3
|
+
import type { KeepAwakeListener } from './types';
|
|
4
|
+
|
|
5
|
+
const EXPO_KEEP_AWAKE_MODULE_NAME = 'ExpoKeepAwake';
|
|
6
|
+
|
|
7
|
+
// Every method is optional — each call site checks for its presence before calling through and
|
|
8
|
+
// throws (or falls back, for isAvailableAsync) itself, matching upstream's own per-platform
|
|
9
|
+
// capability checks rather than assuming the native module implements the whole surface (same
|
|
10
|
+
// convention as packages/battery, packages/haptics, packages/local-auth native-module.ts).
|
|
11
|
+
export type INativeKeepAwakeModule = {
|
|
12
|
+
isAvailableAsync?(): Promise<boolean>;
|
|
13
|
+
activate?(tag: string): Promise<void>;
|
|
14
|
+
deactivate?(tag: string): Promise<void>;
|
|
15
|
+
addListenerForTag?(tag: string, listener: KeepAwakeListener): EventSubscription;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const expoKeepAwake = requireNativeModule<INativeKeepAwakeModule>(
|
|
19
|
+
EXPO_KEEP_AWAKE_MODULE_NAME,
|
|
20
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Hand-ported from .vendors/expo/packages/expo-keep-awake/src/KeepAwake.types.ts (sdk-57).
|
|
2
|
+
// Upstream's KeepAwakeEvent#state is a web-only WakeLockSentinel-shaped value with no native
|
|
3
|
+
// analogue — native listeners rarely fire and never depend on `state`'s shape, so it's kept as
|
|
4
|
+
// a minimal placeholder rather than guessed at.
|
|
5
|
+
export type KeepAwakeEvent = {
|
|
6
|
+
state: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type KeepAwakeListener = (event: KeepAwakeEvent) => void;
|
|
10
|
+
|
|
11
|
+
export type KeepAwakeOptions = {
|
|
12
|
+
listener?: KeepAwakeListener;
|
|
13
|
+
suppressDeactivateWarnings?: boolean;
|
|
14
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// React lifecycle wiring over the framework-agnostic core (core/keep-awake.ts) — activates on
|
|
2
|
+
// mount, deactivates on unmount, mirroring upstream's own useKeepAwake. Uses React's useId for
|
|
3
|
+
// the default per-instance tag exactly like upstream, so two components calling useKeepAwake()
|
|
4
|
+
// concurrently without an explicit tag don't clobber each other's activation/deactivation.
|
|
5
|
+
//
|
|
6
|
+
// Upstream also checks the native module's own `addListenerForTag` presence directly before
|
|
7
|
+
// calling `addListener`; that private module isn't reachable from here (only core's public API
|
|
8
|
+
// is), so registering the listener just goes through the same promise chain — an
|
|
9
|
+
// UnavailabilityError from `addListener` is swallowed by the trailing `.catch(() => {})` exactly
|
|
10
|
+
// like a rejected `activateKeepAwakeAsync`, same net effect as upstream's own guard.
|
|
11
|
+
import { useEffect, useId } from 'react';
|
|
12
|
+
import {
|
|
13
|
+
activateKeepAwakeAsync,
|
|
14
|
+
addListener,
|
|
15
|
+
deactivateKeepAwake,
|
|
16
|
+
type KeepAwakeOptions,
|
|
17
|
+
} from '../../../core';
|
|
18
|
+
|
|
19
|
+
export function useKeepAwake(tag?: string, options?: KeepAwakeOptions): void {
|
|
20
|
+
const defaultTag = useId();
|
|
21
|
+
const tagOrDefault = tag ?? defaultTag;
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
let isMounted = true;
|
|
25
|
+
|
|
26
|
+
activateKeepAwakeAsync(tagOrDefault)
|
|
27
|
+
.then(() => {
|
|
28
|
+
if (isMounted && options?.listener) {
|
|
29
|
+
addListener(tagOrDefault, options.listener);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.catch(() => {});
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
isMounted = false;
|
|
36
|
+
if (options?.suppressDeactivateWarnings) {
|
|
37
|
+
deactivateKeepAwake(tagOrDefault).catch(() => {});
|
|
38
|
+
} else {
|
|
39
|
+
deactivateKeepAwake(tagOrDefault);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}, [tagOrDefault]);
|
|
43
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Co-located React-driven test (ADR 0025) for useKeepAwake. See battery's
|
|
2
|
+
// use-low-power-mode.test.tsx for the shared rationale (mocks `core`, not expo-modules-core
|
|
3
|
+
// internals).
|
|
4
|
+
|
|
5
|
+
import { createElement, type ReactElement } from 'react';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { mount, unmount, View } from '@symbiote-native/react';
|
|
8
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
9
|
+
import { useKeepAwake } from './index';
|
|
10
|
+
|
|
11
|
+
const { activateKeepAwakeAsync, addListener, deactivateKeepAwake } = vi.hoisted(() => ({
|
|
12
|
+
activateKeepAwakeAsync: vi.fn(async (_tag: string) => undefined),
|
|
13
|
+
addListener: vi.fn(),
|
|
14
|
+
deactivateKeepAwake: vi.fn(async (_tag: string) => undefined),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('../../../core', () => ({
|
|
18
|
+
activateKeepAwakeAsync,
|
|
19
|
+
addListener,
|
|
20
|
+
deactivateKeepAwake,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const ROOT_TAG = 954;
|
|
24
|
+
|
|
25
|
+
function Probe({ tag }: { tag?: string }): ReactElement {
|
|
26
|
+
useKeepAwake(tag);
|
|
27
|
+
return createElement(View);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const fabric = installFabric();
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
fabric.reset();
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
activateKeepAwakeAsync.mockResolvedValue(undefined);
|
|
36
|
+
deactivateKeepAwake.mockResolvedValue(undefined);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => unmount(ROOT_TAG));
|
|
40
|
+
|
|
41
|
+
describe('useKeepAwake', () => {
|
|
42
|
+
it('activates a default tag on mount', async () => {
|
|
43
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
44
|
+
|
|
45
|
+
await vi.waitFor(() => expect(activateKeepAwakeAsync).toHaveBeenCalledTimes(1));
|
|
46
|
+
expect(typeof activateKeepAwakeAsync.mock.calls[0][0]).toBe('string');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('activates the explicit tag when one is given', async () => {
|
|
50
|
+
mount(ROOT_TAG, createElement(Probe, { tag: 'custom-tag' }));
|
|
51
|
+
|
|
52
|
+
await vi.waitFor(() => expect(activateKeepAwakeAsync).toHaveBeenCalledWith('custom-tag'));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('deactivates the same tag on unmount', async () => {
|
|
56
|
+
mount(ROOT_TAG, createElement(Probe, { tag: 'custom-tag' }));
|
|
57
|
+
await vi.waitFor(() => expect(activateKeepAwakeAsync).toHaveBeenCalledTimes(1));
|
|
58
|
+
|
|
59
|
+
unmount(ROOT_TAG);
|
|
60
|
+
|
|
61
|
+
expect(deactivateKeepAwake).toHaveBeenCalledWith('custom-tag');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('never touches addListener when no options are given', async () => {
|
|
65
|
+
mount(ROOT_TAG, createElement(Probe, { tag: 'custom-tag' }));
|
|
66
|
+
|
|
67
|
+
await vi.waitFor(() => expect(activateKeepAwakeAsync).toHaveBeenCalledTimes(1));
|
|
68
|
+
expect(addListener).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @symbiote-native/keep-awake/react: the React entry over the framework-agnostic core. The
|
|
2
|
+
// lifecycle bucket is `hooks/` (never `composables/`, that's Vue's term) — see
|
|
3
|
+
// adapters/react/src/hooks for the same convention.
|
|
4
|
+
|
|
5
|
+
export { useKeepAwake } from './hooks/use-keep-awake';
|
|
6
|
+
export {
|
|
7
|
+
ExpoKeepAwakeTag,
|
|
8
|
+
isAvailableAsync,
|
|
9
|
+
activateKeepAwakeAsync,
|
|
10
|
+
deactivateKeepAwake,
|
|
11
|
+
addListener,
|
|
12
|
+
type KeepAwakeEvent,
|
|
13
|
+
type KeepAwakeListener,
|
|
14
|
+
type KeepAwakeOptions,
|
|
15
|
+
type EventSubscription,
|
|
16
|
+
} from '../core';
|