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.
Files changed (55) hide show
  1. package/README.md +607 -0
  2. package/android/.gradle/7.4.2/checksums/checksums.lock +0 -0
  3. package/android/.gradle/7.4.2/fileChanges/last-build.bin +0 -0
  4. package/android/.gradle/7.4.2/fileHashes/fileHashes.lock +0 -0
  5. package/android/.gradle/7.4.2/gc.properties +0 -0
  6. package/android/.gradle/vcs-1/gc.properties +0 -0
  7. package/android/build.gradle +15 -0
  8. package/android/src/main/java/com/rnpersistenttimer/RNPersistentTimerModule.java +164 -0
  9. package/android/src/main/java/com/rnpersistenttimer/RNPersistentTimerPackage.java +27 -0
  10. package/android/src/main/java/com/rnpersistenttimer/TimerForegroundService.java +280 -0
  11. package/ios/RNPersistentTimer.h +10 -0
  12. package/ios/RNPersistentTimer.m +221 -0
  13. package/lib/commonjs/NativeTimerModule.js +46 -0
  14. package/lib/commonjs/NativeTimerModule.js.map +1 -0
  15. package/lib/commonjs/PersistentTimerManager.js +337 -0
  16. package/lib/commonjs/PersistentTimerManager.js.map +1 -0
  17. package/lib/commonjs/index.js +76 -0
  18. package/lib/commonjs/index.js.map +1 -0
  19. package/lib/commonjs/types.js +2 -0
  20. package/lib/commonjs/types.js.map +1 -0
  21. package/lib/commonjs/usePersistentTimer.js +159 -0
  22. package/lib/commonjs/usePersistentTimer.js.map +1 -0
  23. package/lib/commonjs/utils.js +112 -0
  24. package/lib/commonjs/utils.js.map +1 -0
  25. package/lib/module/NativeTimerModule.js +40 -0
  26. package/lib/module/NativeTimerModule.js.map +1 -0
  27. package/lib/module/PersistentTimerManager.js +329 -0
  28. package/lib/module/PersistentTimerManager.js.map +1 -0
  29. package/lib/module/index.js +17 -0
  30. package/lib/module/index.js.map +1 -0
  31. package/lib/module/types.js +2 -0
  32. package/lib/module/types.js.map +1 -0
  33. package/lib/module/usePersistentTimer.js +153 -0
  34. package/lib/module/usePersistentTimer.js.map +1 -0
  35. package/lib/module/utils.js +100 -0
  36. package/lib/module/utils.js.map +1 -0
  37. package/lib/typescript/NativeTimerModule.d.ts +31 -0
  38. package/lib/typescript/NativeTimerModule.d.ts.map +1 -0
  39. package/lib/typescript/PersistentTimerManager.d.ts +37 -0
  40. package/lib/typescript/PersistentTimerManager.d.ts.map +1 -0
  41. package/lib/typescript/index.d.ts +7 -0
  42. package/lib/typescript/index.d.ts.map +1 -0
  43. package/lib/typescript/types.d.ts +167 -0
  44. package/lib/typescript/types.d.ts.map +1 -0
  45. package/lib/typescript/usePersistentTimer.d.ts +16 -0
  46. package/lib/typescript/usePersistentTimer.d.ts.map +1 -0
  47. package/lib/typescript/utils.d.ts +36 -0
  48. package/lib/typescript/utils.d.ts.map +1 -0
  49. package/package.json +98 -0
  50. package/src/NativeTimerModule.ts +73 -0
  51. package/src/PersistentTimerManager.ts +410 -0
  52. package/src/index.ts +41 -0
  53. package/src/types.ts +198 -0
  54. package/src/usePersistentTimer.tsx +173 -0
  55. 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
+ }