rn-persistent-timer 1.1.0
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/README.md +607 -0
- package/android/.gradle/7.4.2/checksums/checksums.lock +0 -0
- package/android/.gradle/7.4.2/fileChanges/last-build.bin +0 -0
- package/android/.gradle/7.4.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/7.4.2/gc.properties +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +15 -0
- package/android/src/main/java/com/rnpersistenttimer/RNPersistentTimerModule.java +164 -0
- package/android/src/main/java/com/rnpersistenttimer/RNPersistentTimerPackage.java +27 -0
- package/android/src/main/java/com/rnpersistenttimer/TimerForegroundService.java +280 -0
- package/ios/RNPersistentTimer.h +10 -0
- package/ios/RNPersistentTimer.m +221 -0
- package/lib/commonjs/NativeTimerModule.js +46 -0
- package/lib/commonjs/NativeTimerModule.js.map +1 -0
- package/lib/commonjs/PersistentTimerManager.js +337 -0
- package/lib/commonjs/PersistentTimerManager.js.map +1 -0
- package/lib/commonjs/index.js +76 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/usePersistentTimer.js +159 -0
- package/lib/commonjs/usePersistentTimer.js.map +1 -0
- package/lib/commonjs/utils.js +112 -0
- package/lib/commonjs/utils.js.map +1 -0
- package/lib/module/NativeTimerModule.js +40 -0
- package/lib/module/NativeTimerModule.js.map +1 -0
- package/lib/module/PersistentTimerManager.js +329 -0
- package/lib/module/PersistentTimerManager.js.map +1 -0
- package/lib/module/index.js +17 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/usePersistentTimer.js +153 -0
- package/lib/module/usePersistentTimer.js.map +1 -0
- package/lib/module/utils.js +100 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/typescript/NativeTimerModule.d.ts +31 -0
- package/lib/typescript/NativeTimerModule.d.ts.map +1 -0
- package/lib/typescript/PersistentTimerManager.d.ts +37 -0
- package/lib/typescript/PersistentTimerManager.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +7 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +167 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/usePersistentTimer.d.ts +16 -0
- package/lib/typescript/usePersistentTimer.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +36 -0
- package/lib/typescript/utils.d.ts.map +1 -0
- package/package.json +98 -0
- package/src/NativeTimerModule.ts +73 -0
- package/src/PersistentTimerManager.ts +410 -0
- package/src/index.ts +41 -0
- package/src/types.ts +198 -0
- package/src/usePersistentTimer.tsx +173 -0
- package/src/utils.ts +91 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// rn-persistent-timer — usePersistentTimer Hook
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
5
|
+
import { PersistentTimerManager } from './PersistentTimerManager';
|
|
6
|
+
import { formatTime } from './utils';
|
|
7
|
+
import type {
|
|
8
|
+
TimerConfig,
|
|
9
|
+
TimerCallbacks,
|
|
10
|
+
TimerSnapshot,
|
|
11
|
+
AppState,
|
|
12
|
+
UsePersistentTimerReturn,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function makeInitialSnapshot(config: TimerConfig): TimerSnapshot {
|
|
18
|
+
const mode = config.mode ?? 'stopwatch';
|
|
19
|
+
const duration = config.duration ?? 0;
|
|
20
|
+
return {
|
|
21
|
+
timerId: config.timerId,
|
|
22
|
+
elapsed: 0,
|
|
23
|
+
remaining: mode === 'countdown' ? duration : null,
|
|
24
|
+
state: 'idle',
|
|
25
|
+
appState: 'foreground',
|
|
26
|
+
startedAt: null,
|
|
27
|
+
pausedAt: null,
|
|
28
|
+
formattedElapsed: '00:00:00',
|
|
29
|
+
formattedRemaining: mode === 'countdown' ? formatTime(duration) : null,
|
|
30
|
+
progress: mode === 'countdown' ? 0 : null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* React hook for persistent timers.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const { snapshot, start, pause, reset, isRunning } = usePersistentTimer({
|
|
42
|
+
* timerId: 'workout-timer',
|
|
43
|
+
* mode: 'stopwatch',
|
|
44
|
+
* runInBackground: true,
|
|
45
|
+
* onTick: (snap) => console.log(snap.formattedElapsed),
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function usePersistentTimer(
|
|
50
|
+
config: TimerConfig & TimerCallbacks,
|
|
51
|
+
): UsePersistentTimerReturn {
|
|
52
|
+
const managerRef = useRef<PersistentTimerManager | null>(null);
|
|
53
|
+
|
|
54
|
+
const [snapshot, setSnapshot] = useState<TimerSnapshot>(() =>
|
|
55
|
+
makeInitialSnapshot(config),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Stable callback refs — avoids re-creating the manager when callbacks change
|
|
59
|
+
const callbacksRef = useRef<TimerCallbacks>({});
|
|
60
|
+
callbacksRef.current = {
|
|
61
|
+
onTick: config.onTick,
|
|
62
|
+
onStart: config.onStart,
|
|
63
|
+
onPause: config.onPause,
|
|
64
|
+
onResume: config.onResume,
|
|
65
|
+
onReset: config.onReset,
|
|
66
|
+
onComplete: config.onComplete,
|
|
67
|
+
onBackground: config.onBackground,
|
|
68
|
+
onForeground: config.onForeground,
|
|
69
|
+
onRestore: config.onRestore,
|
|
70
|
+
onError: config.onError,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const manager = new PersistentTimerManager({
|
|
75
|
+
timerId: config.timerId,
|
|
76
|
+
mode: config.mode,
|
|
77
|
+
duration: config.duration,
|
|
78
|
+
runInBackground: config.runInBackground,
|
|
79
|
+
runInKilledState: config.runInKilledState,
|
|
80
|
+
pauseOnBackground: config.pauseOnBackground,
|
|
81
|
+
resetOnForeground: config.resetOnForeground,
|
|
82
|
+
interval: config.interval,
|
|
83
|
+
showNotification: config.showNotification,
|
|
84
|
+
notification: config.notification,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Snapshot callbacks — update React state then call user callback
|
|
88
|
+
type SnapshotCbKey = Exclude<keyof TimerCallbacks, 'onError'>;
|
|
89
|
+
const wire = (
|
|
90
|
+
managerEvent: Parameters<typeof manager.on>[0],
|
|
91
|
+
cbKey: SnapshotCbKey,
|
|
92
|
+
) => {
|
|
93
|
+
manager.on(managerEvent, (snap: TimerSnapshot) => {
|
|
94
|
+
setSnapshot({ ...snap });
|
|
95
|
+
(callbacksRef.current[cbKey] as ((s: TimerSnapshot) => void) | undefined)?.(snap);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
wire('onTick', 'onTick');
|
|
100
|
+
wire('onStart', 'onStart');
|
|
101
|
+
wire('onPause', 'onPause');
|
|
102
|
+
wire('onResume', 'onResume');
|
|
103
|
+
wire('onReset', 'onReset');
|
|
104
|
+
wire('onComplete', 'onComplete');
|
|
105
|
+
wire('onBackground', 'onBackground');
|
|
106
|
+
wire('onForeground', 'onForeground');
|
|
107
|
+
wire('onRestore', 'onRestore');
|
|
108
|
+
|
|
109
|
+
// Error callback — receives Error, not TimerSnapshot
|
|
110
|
+
manager.on('error', (err: Error) => {
|
|
111
|
+
callbacksRef.current.onError?.(err);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Restore from killed state if applicable
|
|
115
|
+
if (config.runInKilledState) {
|
|
116
|
+
PersistentTimerManager.restore(config.timerId).then((restored) => {
|
|
117
|
+
if (!restored) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Replay internal state onto the manager
|
|
122
|
+
(manager as any)._elapsed = restored.elapsed;
|
|
123
|
+
(manager as any)._state = restored.state;
|
|
124
|
+
|
|
125
|
+
const snap = manager.getSnapshot();
|
|
126
|
+
setSnapshot({ ...snap });
|
|
127
|
+
callbacksRef.current.onRestore?.(snap);
|
|
128
|
+
|
|
129
|
+
// Auto-resume if the timer was running when the app was killed
|
|
130
|
+
if (restored.state === 'running') {
|
|
131
|
+
manager.start();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
managerRef.current = manager;
|
|
137
|
+
|
|
138
|
+
return () => {
|
|
139
|
+
manager.destroy();
|
|
140
|
+
managerRef.current = null;
|
|
141
|
+
};
|
|
142
|
+
// Re-create the manager only if timerId changes
|
|
143
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
144
|
+
}, [config.timerId]);
|
|
145
|
+
|
|
146
|
+
// ─── Stable controls ──────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
const start = useCallback(() => managerRef.current?.start(), []);
|
|
149
|
+
const pause = useCallback(() => managerRef.current?.pause(), []);
|
|
150
|
+
const resume = useCallback(() => managerRef.current?.resume(), []);
|
|
151
|
+
const reset = useCallback(() => managerRef.current?.reset(), []);
|
|
152
|
+
const stop = useCallback(() => managerRef.current?.stop(), []);
|
|
153
|
+
const destroy = useCallback(() => managerRef.current?.destroy(), []);
|
|
154
|
+
const getSnapshot = useCallback(
|
|
155
|
+
() => managerRef.current?.getSnapshot() ?? snapshot,
|
|
156
|
+
[snapshot],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
snapshot,
|
|
161
|
+
isRunning: snapshot.state === 'running',
|
|
162
|
+
isPaused: snapshot.state === 'paused',
|
|
163
|
+
isCompleted: snapshot.state === 'completed',
|
|
164
|
+
appState: snapshot.appState as AppState,
|
|
165
|
+
start,
|
|
166
|
+
pause,
|
|
167
|
+
resume,
|
|
168
|
+
reset,
|
|
169
|
+
stop,
|
|
170
|
+
destroy,
|
|
171
|
+
getSnapshot,
|
|
172
|
+
};
|
|
173
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// rn-persistent-timer — Utility Functions
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format a number of seconds into an `HH:MM:SS` string.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* formatTime(3661); // '01:01:01'
|
|
9
|
+
* formatTime(59); // '00:00:59'
|
|
10
|
+
*/
|
|
11
|
+
export function formatTime(seconds: number): string {
|
|
12
|
+
const s = Math.max(0, Math.floor(seconds));
|
|
13
|
+
const h = Math.floor(s / 3600);
|
|
14
|
+
const m = Math.floor((s % 3600) / 60);
|
|
15
|
+
const sec = s % 60;
|
|
16
|
+
return [h, m, sec].map((v) => String(v).padStart(2, '0')).join(':');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse an `HH:MM:SS` or `MM:SS` string into total seconds.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* parseTime('01:30:00'); // 5400
|
|
24
|
+
* parseTime('05:30'); // 330
|
|
25
|
+
*/
|
|
26
|
+
export function parseTime(formatted: string): number {
|
|
27
|
+
const parts = formatted.split(':').map(Number);
|
|
28
|
+
if (parts.length === 3) {
|
|
29
|
+
return parts[0]! * 3600 + parts[1]! * 60 + parts[2]!;
|
|
30
|
+
}
|
|
31
|
+
if (parts.length === 2) {
|
|
32
|
+
return parts[0]! * 60 + parts[1]!;
|
|
33
|
+
}
|
|
34
|
+
return parts[0] ?? 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns `true` if the native background-timer module is available on this
|
|
39
|
+
* device / build. Always returns `false` in Expo Go.
|
|
40
|
+
*/
|
|
41
|
+
export async function isBackgroundTimerSupported(): Promise<boolean> {
|
|
42
|
+
const { NativeTimerModule } = await import('./NativeTimerModule');
|
|
43
|
+
return Boolean(NativeTimerModule);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns `true` if the killed-state persistence feature is supported.
|
|
48
|
+
* Requires the native module **and** platform capabilities
|
|
49
|
+
* (WorkManager on Android, BGTaskScheduler on iOS 13+).
|
|
50
|
+
*/
|
|
51
|
+
export async function isKilledStateTimerSupported(): Promise<boolean> {
|
|
52
|
+
const { NativeTimerModule } = await import('./NativeTimerModule');
|
|
53
|
+
if (!NativeTimerModule) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return await NativeTimerModule.isKilledStateSupported?.() ?? false;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return the IDs of all timers currently active in the native layer.
|
|
65
|
+
*/
|
|
66
|
+
export async function getActiveTimers(): Promise<string[]> {
|
|
67
|
+
const { NativeTimerModule } = await import('./NativeTimerModule');
|
|
68
|
+
if (!NativeTimerModule) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return await NativeTimerModule.getActiveTimers();
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Cancel every active timer in both the native layer and JS.
|
|
80
|
+
*/
|
|
81
|
+
export async function cancelAllTimers(): Promise<void> {
|
|
82
|
+
const { NativeTimerModule } = await import('./NativeTimerModule');
|
|
83
|
+
if (!NativeTimerModule) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await NativeTimerModule.cancelAll();
|
|
88
|
+
} catch {
|
|
89
|
+
// Swallow — best-effort cleanup
|
|
90
|
+
}
|
|
91
|
+
}
|