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
package/README.md
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# rn-persistent-timer
|
|
2
|
+
|
|
3
|
+
> **React Native timers that keep running — in the foreground, background, and even after the app is killed.**
|
|
4
|
+
> Full TypeScript support. Zero bundled native dependencies.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/rn-persistent-timer)
|
|
7
|
+
[](https://www.npmjs.com/package/rn-persistent-timer)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](#)
|
|
10
|
+
[](#)
|
|
11
|
+
[](#)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
| Feature | Foreground | Background | Killed App |
|
|
18
|
+
|---|:---:|:---:|:---:|
|
|
19
|
+
| JS-only timer | ✅ | ❌ | ❌ |
|
|
20
|
+
| Background timer | ✅ | ✅ | ❌ |
|
|
21
|
+
| Kill-proof timer | ✅ | ✅ | ✅ |
|
|
22
|
+
|
|
23
|
+
- 🕐 **Stopwatch** (count up) and **Countdown** (count down to zero)
|
|
24
|
+
- 🔔 **Persistent Android notification** with pause/resume actions (Foreground Service)
|
|
25
|
+
- 📱 **iOS background task** via `UIBackgroundTaskIdentifier` + `BGTaskScheduler`
|
|
26
|
+
- 💾 **Killed-state persistence** via `AsyncStorage` — restores elapsed time on next open
|
|
27
|
+
- 🎣 **React Hook** (`usePersistentTimer`) + **Imperative class** (`PersistentTimerManager`)
|
|
28
|
+
- 📦 **Full TypeScript** — types included, no `@types/` package needed
|
|
29
|
+
- 🔄 **Multiple simultaneous timers** with independent configs
|
|
30
|
+
- ⚡ **Zero external JS dependencies** beyond `@react-native-async-storage/async-storage`
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📋 Table of Contents
|
|
35
|
+
|
|
36
|
+
1. [Installation](#-installation)
|
|
37
|
+
2. [Android Setup](#-android-setup)
|
|
38
|
+
3. [iOS Setup](#-ios-setup)
|
|
39
|
+
4. [Quick Start](#-quick-start)
|
|
40
|
+
5. [Usage Examples](#-usage-examples)
|
|
41
|
+
6. [API Reference](#-api-reference)
|
|
42
|
+
7. [TypeScript Types](#-typescript-types)
|
|
43
|
+
8. [Troubleshooting](#-troubleshooting)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 📦 Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# npm
|
|
51
|
+
npm install rn-persistent-timer @react-native-async-storage/async-storage
|
|
52
|
+
|
|
53
|
+
# yarn
|
|
54
|
+
yarn add rn-persistent-timer @react-native-async-storage/async-storage
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> **Note:** `@react-native-async-storage/async-storage` is required for killed-state persistence.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 🤖 Android Setup
|
|
62
|
+
|
|
63
|
+
### 1. Add the Package to your `MainApplication`
|
|
64
|
+
|
|
65
|
+
**`MainApplication.kt`** (Kotlin — React Native 0.71+):
|
|
66
|
+
```kotlin
|
|
67
|
+
import com.rnpersistenttimer.RNPersistentTimerPackage
|
|
68
|
+
|
|
69
|
+
override fun getPackages(): List<ReactPackage> = listOf(
|
|
70
|
+
MainReactPackage(),
|
|
71
|
+
RNPersistentTimerPackage(), // ← add this
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**`MainApplication.java`** (Java):
|
|
76
|
+
```java
|
|
77
|
+
import com.rnpersistenttimer.RNPersistentTimerPackage;
|
|
78
|
+
|
|
79
|
+
@Override
|
|
80
|
+
protected List<ReactPackage> getPackages() {
|
|
81
|
+
return Arrays.asList(
|
|
82
|
+
new MainReactPackage(),
|
|
83
|
+
new RNPersistentTimerPackage() // ← add this
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2. Register the Foreground Service in `AndroidManifest.xml`
|
|
89
|
+
|
|
90
|
+
```xml
|
|
91
|
+
<!-- Inside <application> tag -->
|
|
92
|
+
<service
|
|
93
|
+
android:name="com.rnpersistenttimer.TimerForegroundService"
|
|
94
|
+
android:foregroundServiceType="dataSync"
|
|
95
|
+
android:exported="false" />
|
|
96
|
+
|
|
97
|
+
<!-- Inside <manifest> tag (before <application>) -->
|
|
98
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
99
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
|
100
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> **Android 13+ (API 33+):** You must request `POST_NOTIFICATIONS` permission at runtime before starting a timer with notifications.
|
|
104
|
+
|
|
105
|
+
### 3. Request Notification Permission (Android 13+)
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { PermissionsAndroid, Platform } from 'react-native';
|
|
109
|
+
|
|
110
|
+
async function requestNotificationPermission(): Promise<void> {
|
|
111
|
+
if (Platform.OS === 'android' && Platform.Version >= 33) {
|
|
112
|
+
await PermissionsAndroid.request(
|
|
113
|
+
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🍎 iOS Setup
|
|
122
|
+
|
|
123
|
+
### 1. Add Capability: Background Modes
|
|
124
|
+
|
|
125
|
+
In Xcode:
|
|
126
|
+
1. Select your project target → **Signing & Capabilities**
|
|
127
|
+
2. Click **+ Capability** → add **Background Modes**
|
|
128
|
+
3. Enable ✅ **Background fetch** and ✅ **Background processing**
|
|
129
|
+
|
|
130
|
+
### 2. Register BGTask Identifier in `Info.plist`
|
|
131
|
+
|
|
132
|
+
```xml
|
|
133
|
+
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
|
134
|
+
<array>
|
|
135
|
+
<string>com.yourapp.timer</string>
|
|
136
|
+
</array>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
> Replace `com.yourapp.timer` with your actual bundle ID prefix.
|
|
140
|
+
|
|
141
|
+
### 3. Update `RNPersistentTimer.m`
|
|
142
|
+
|
|
143
|
+
Open `ios/RNPersistentTimer.m` and replace the identifier string:
|
|
144
|
+
|
|
145
|
+
```objc
|
|
146
|
+
// Find this line in scheduleBGTask:
|
|
147
|
+
[[BGAppRefreshTaskRequest alloc] initWithIdentifier:@"com.yourapp.timer"];
|
|
148
|
+
|
|
149
|
+
// Replace with your actual bundle ID:
|
|
150
|
+
[[BGAppRefreshTaskRequest alloc] initWithIdentifier:@"com.YourApp.timer"];
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 4. Install Pods
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
cd ios && pod install && cd ..
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## ⚡ Quick Start
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import React from 'react';
|
|
165
|
+
import { View, Text, TouchableOpacity } from 'react-native';
|
|
166
|
+
import { usePersistentTimer } from 'rn-persistent-timer';
|
|
167
|
+
|
|
168
|
+
export default function StopwatchScreen() {
|
|
169
|
+
const { snapshot, start, pause, resume, reset, isRunning, isPaused } =
|
|
170
|
+
usePersistentTimer({
|
|
171
|
+
timerId: 'my-stopwatch', // unique ID — required
|
|
172
|
+
mode: 'stopwatch',
|
|
173
|
+
runInBackground: true, // keeps ticking when app is backgrounded
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<View>
|
|
178
|
+
<Text>{snapshot.formattedElapsed}</Text> {/* "HH:MM:SS" */}
|
|
179
|
+
<Text>State: {snapshot.state}</Text>
|
|
180
|
+
|
|
181
|
+
{!isRunning && !isPaused && (
|
|
182
|
+
<TouchableOpacity onPress={start}>
|
|
183
|
+
<Text>▶ Start</Text>
|
|
184
|
+
</TouchableOpacity>
|
|
185
|
+
)}
|
|
186
|
+
{isRunning && (
|
|
187
|
+
<TouchableOpacity onPress={pause}>
|
|
188
|
+
<Text>⏸ Pause</Text>
|
|
189
|
+
</TouchableOpacity>
|
|
190
|
+
)}
|
|
191
|
+
{isPaused && (
|
|
192
|
+
<TouchableOpacity onPress={resume}>
|
|
193
|
+
<Text>▶ Resume</Text>
|
|
194
|
+
</TouchableOpacity>
|
|
195
|
+
)}
|
|
196
|
+
<TouchableOpacity onPress={reset}>
|
|
197
|
+
<Text>⟳ Reset</Text>
|
|
198
|
+
</TouchableOpacity>
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 📖 Usage Examples
|
|
207
|
+
|
|
208
|
+
### Stopwatch — Foreground Only
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
const timer = usePersistentTimer({
|
|
212
|
+
timerId: 'foreground-sw',
|
|
213
|
+
mode: 'stopwatch',
|
|
214
|
+
runInBackground: false,
|
|
215
|
+
runInKilledState: false,
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Stopwatch — Keeps Running in Background
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
const timer = usePersistentTimer({
|
|
223
|
+
timerId: 'bg-stopwatch',
|
|
224
|
+
mode: 'stopwatch',
|
|
225
|
+
runInBackground: true,
|
|
226
|
+
showNotification: true,
|
|
227
|
+
notification: {
|
|
228
|
+
title: '⏱ Stopwatch Running',
|
|
229
|
+
body: 'Elapsed: {time}', // {time} is replaced automatically
|
|
230
|
+
color: '#4CAF50',
|
|
231
|
+
showActions: true, // adds Pause/Resume buttons to notification
|
|
232
|
+
},
|
|
233
|
+
onBackground: (snap) => console.log('Backgrounded at', snap.formattedElapsed),
|
|
234
|
+
onForeground: (snap) => console.log('Foregrounded at', snap.formattedElapsed),
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Kill-Proof Timer (Survives App Kill)
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
import { Alert } from 'react-native';
|
|
242
|
+
|
|
243
|
+
const timer = usePersistentTimer({
|
|
244
|
+
timerId: 'kill-proof',
|
|
245
|
+
mode: 'stopwatch',
|
|
246
|
+
runInBackground: true,
|
|
247
|
+
runInKilledState: true, // ← enables AsyncStorage persistence
|
|
248
|
+
onRestore: (snap) => {
|
|
249
|
+
Alert.alert('Welcome back!', `Timer restored to ${snap.formattedElapsed}`);
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Countdown Timer
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
const timer = usePersistentTimer({
|
|
258
|
+
timerId: 'workout-countdown',
|
|
259
|
+
mode: 'countdown',
|
|
260
|
+
duration: 5 * 60, // 5 minutes in seconds
|
|
261
|
+
runInBackground: true,
|
|
262
|
+
runInKilledState: true,
|
|
263
|
+
notification: {
|
|
264
|
+
title: '⏳ Workout Timer',
|
|
265
|
+
body: '{time} remaining',
|
|
266
|
+
color: '#F44336',
|
|
267
|
+
},
|
|
268
|
+
onComplete: () => Alert.alert('Time\'s up! 🎉'),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Use snapshot.formattedRemaining for countdown display
|
|
272
|
+
// Use snapshot.progress (0→1) for a progress bar
|
|
273
|
+
console.log(timer.snapshot.formattedRemaining); // "04:59"
|
|
274
|
+
console.log(timer.snapshot.progress); // 0.003...
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Auto-Pause on Background (Game Timer)
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
const timer = usePersistentTimer({
|
|
281
|
+
timerId: 'game-timer',
|
|
282
|
+
mode: 'stopwatch',
|
|
283
|
+
pauseOnBackground: true, // auto-pauses when app is backgrounded
|
|
284
|
+
runInKilledState: false,
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Multiple Concurrent Timers
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
const workTimer = usePersistentTimer({ timerId: 'work', mode: 'countdown', duration: 25 * 60, runInBackground: true });
|
|
292
|
+
const breakTimer = usePersistentTimer({ timerId: 'break', mode: 'countdown', duration: 5 * 60, runInBackground: true });
|
|
293
|
+
const sessionTimer = usePersistentTimer({ timerId: 'session', mode: 'stopwatch', runInBackground: true });
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Imperative API — Outside React (Redux, MobX, Services)
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
import { PersistentTimerManager } from 'rn-persistent-timer';
|
|
300
|
+
|
|
301
|
+
const manager = new PersistentTimerManager({
|
|
302
|
+
timerId: 'service-timer',
|
|
303
|
+
mode: 'stopwatch',
|
|
304
|
+
runInBackground: true,
|
|
305
|
+
runInKilledState: false,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
manager.on('onTick', (snap) => {
|
|
309
|
+
console.log('Tick:', snap.formattedElapsed);
|
|
310
|
+
reduxStore.dispatch(updateTimer(snap));
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
manager.on('onComplete', (snap) => {
|
|
314
|
+
console.log('Done!');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
manager.start();
|
|
318
|
+
// Later...
|
|
319
|
+
manager.pause();
|
|
320
|
+
manager.resume();
|
|
321
|
+
manager.reset();
|
|
322
|
+
manager.destroy(); // Always call when done to prevent memory leaks
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Platform Support Check
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
import {
|
|
329
|
+
isBackgroundTimerSupported,
|
|
330
|
+
isKilledStateTimerSupported,
|
|
331
|
+
} from 'rn-persistent-timer';
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
isBackgroundTimerSupported().then((supported) => {
|
|
335
|
+
console.log('Background timer supported:', supported);
|
|
336
|
+
});
|
|
337
|
+
isKilledStateTimerSupported().then((supported) => {
|
|
338
|
+
console.log('Kill-state timer supported:', supported);
|
|
339
|
+
});
|
|
340
|
+
}, []);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Utility Functions
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
import { formatTime, parseTime } from 'rn-persistent-timer';
|
|
347
|
+
|
|
348
|
+
formatTime(3661); // → "01:01:01"
|
|
349
|
+
formatTime(59); // → "00:00:59"
|
|
350
|
+
parseTime('01:30:00'); // → 5400
|
|
351
|
+
parseTime('05:30'); // → 330
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 📚 API Reference
|
|
357
|
+
|
|
358
|
+
### `usePersistentTimer(config)`
|
|
359
|
+
|
|
360
|
+
React hook. Returns `UsePersistentTimerReturn`.
|
|
361
|
+
|
|
362
|
+
| Prop | Type | Default | Description |
|
|
363
|
+
|---|---|---|---|
|
|
364
|
+
| `timerId` | `string` | **required** | Unique timer ID |
|
|
365
|
+
| `mode` | `'stopwatch' \| 'countdown'` | `'stopwatch'` | Timer direction |
|
|
366
|
+
| `duration` | `number` | `0` | Seconds for countdown |
|
|
367
|
+
| `runInBackground` | `boolean` | `true` | Continue when app is backgrounded |
|
|
368
|
+
| `runInKilledState` | `boolean` | `false` | Restore after app kill |
|
|
369
|
+
| `interval` | `number` | `1000` | Tick interval (ms) |
|
|
370
|
+
| `showNotification` | `boolean` | `true` | Show Android notification |
|
|
371
|
+
| `notification` | `AndroidNotificationConfig` | `{}` | Notification options |
|
|
372
|
+
| `pauseOnBackground` | `boolean` | `false` | Auto-pause on background |
|
|
373
|
+
| `resetOnForeground` | `boolean` | `false` | Auto-reset when returned from kill |
|
|
374
|
+
|
|
375
|
+
**Callbacks** (all optional):
|
|
376
|
+
|
|
377
|
+
| Callback | Signature | Description |
|
|
378
|
+
|---|---|---|
|
|
379
|
+
| `onTick` | `(snap: TimerSnapshot) => void` | Every tick |
|
|
380
|
+
| `onStart` | `(snap: TimerSnapshot) => void` | Timer started |
|
|
381
|
+
| `onPause` | `(snap: TimerSnapshot) => void` | Timer paused |
|
|
382
|
+
| `onResume` | `(snap: TimerSnapshot) => void` | Timer resumed |
|
|
383
|
+
| `onReset` | `(snap: TimerSnapshot) => void` | Timer reset |
|
|
384
|
+
| `onComplete` | `(snap: TimerSnapshot) => void` | Countdown finished |
|
|
385
|
+
| `onBackground` | `(snap: TimerSnapshot) => void` | App went to background |
|
|
386
|
+
| `onForeground` | `(snap: TimerSnapshot) => void` | App returned to foreground |
|
|
387
|
+
| `onRestore` | `(snap: TimerSnapshot) => void` | State restored after kill |
|
|
388
|
+
| `onError` | `(error: Error) => void` | Internal error |
|
|
389
|
+
|
|
390
|
+
**Return value:**
|
|
391
|
+
|
|
392
|
+
| Field | Type | Description |
|
|
393
|
+
|---|---|---|
|
|
394
|
+
| `snapshot` | `TimerSnapshot` | Reactive timer state |
|
|
395
|
+
| `isRunning` | `boolean` | `state === 'running'` |
|
|
396
|
+
| `isPaused` | `boolean` | `state === 'paused'` |
|
|
397
|
+
| `isCompleted` | `boolean` | `state === 'completed'` |
|
|
398
|
+
| `appState` | `AppState` | Current app state |
|
|
399
|
+
| `start()` | `() => void` | Start the timer |
|
|
400
|
+
| `pause()` | `() => void` | Pause the timer |
|
|
401
|
+
| `resume()` | `() => void` | Resume from pause |
|
|
402
|
+
| `reset()` | `() => void` | Stop and reset to 0 |
|
|
403
|
+
| `stop()` | `() => void` | Stop without reset |
|
|
404
|
+
| `getSnapshot()` | `() => TimerSnapshot` | Synchronous snapshot |
|
|
405
|
+
| `destroy()` | `() => void` | Clean up all listeners |
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### `PersistentTimerManager`
|
|
410
|
+
|
|
411
|
+
Class-based API for use outside of React components.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const manager = new PersistentTimerManager(config: TimerConfig);
|
|
415
|
+
|
|
416
|
+
manager.start();
|
|
417
|
+
manager.pause();
|
|
418
|
+
manager.resume();
|
|
419
|
+
manager.reset();
|
|
420
|
+
manager.stop();
|
|
421
|
+
manager.getSnapshot(): TimerSnapshot;
|
|
422
|
+
manager.destroy();
|
|
423
|
+
|
|
424
|
+
manager.on(event, handler);
|
|
425
|
+
manager.off(event, handler);
|
|
426
|
+
|
|
427
|
+
// Static: restore from killed state
|
|
428
|
+
const restored = await PersistentTimerManager.restore(timerId);
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
### `TimerSnapshot`
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
interface TimerSnapshot {
|
|
437
|
+
timerId: string;
|
|
438
|
+
elapsed: number; // seconds since start
|
|
439
|
+
remaining: number | null; // seconds left (countdown only)
|
|
440
|
+
state: TimerState; // 'idle' | 'running' | 'paused' | 'completed'
|
|
441
|
+
appState: AppState; // 'foreground' | 'background' | 'killed'
|
|
442
|
+
startedAt: number | null; // unix ms when started
|
|
443
|
+
pausedAt: number | null; // unix ms when paused
|
|
444
|
+
formattedElapsed: string; // "HH:MM:SS"
|
|
445
|
+
formattedRemaining: string | null; // "HH:MM:SS" (countdown only)
|
|
446
|
+
progress: number | null; // 0→1 (countdown only)
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
### `AndroidNotificationConfig`
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
interface AndroidNotificationConfig {
|
|
456
|
+
title?: string; // Notification title
|
|
457
|
+
body?: string; // Body text; use {time} for the current timer value
|
|
458
|
+
channelId?: string; // Notification channel ID
|
|
459
|
+
channelName?: string; // Channel display name
|
|
460
|
+
icon?: string; // Drawable resource name
|
|
461
|
+
color?: string; // Hex accent color, e.g. '#FF5733'
|
|
462
|
+
showTime?: boolean; // Show time in notification
|
|
463
|
+
showActions?: boolean;// Show Pause/Resume action buttons
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 🔧 Troubleshooting
|
|
470
|
+
|
|
471
|
+
### Native module not found warning
|
|
472
|
+
|
|
473
|
+
```
|
|
474
|
+
[rn-persistent-timer] Native module not found.
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**iOS:** Run `npx pod-install` (or `cd ios && pod install`) then rebuild.
|
|
478
|
+
**Android:** Clean and rebuild the Android project: `cd android && ./gradlew clean && cd ..`.
|
|
479
|
+
|
|
480
|
+
Foreground (JS-only) timers still work without the native module.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
### Timer stops in background on Android
|
|
485
|
+
|
|
486
|
+
1. Ensure `TimerForegroundService` is registered in `AndroidManifest.xml`.
|
|
487
|
+
2. Check that `POST_NOTIFICATIONS` permission is granted on Android 13+.
|
|
488
|
+
3. Set `showNotification: true` — Android 8+ **requires** a visible notification for foreground services.
|
|
489
|
+
4. Some OEM ROMs (Xiaomi, OPPO, etc.) aggressively kill background processes. Guide users to whitelist your app in battery settings.
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
### Timer stops in background on iOS
|
|
494
|
+
|
|
495
|
+
1. Confirm **Background Modes** capability is added in Xcode.
|
|
496
|
+
2. Verify `BGTaskSchedulerPermittedIdentifiers` is set in `Info.plist`.
|
|
497
|
+
3. iOS limits background execution to ~30 seconds unless using `BGTaskScheduler`. The module handles this automatically via `UIBackgroundTaskIdentifier`.
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
### Timer not restoring after app kill
|
|
502
|
+
|
|
503
|
+
1. Ensure `runInKilledState: true` in your config.
|
|
504
|
+
2. Confirm `@react-native-async-storage/async-storage` is installed and linked.
|
|
505
|
+
3. Rebuild the project after installing `async-storage`.
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
### TypeScript errors
|
|
510
|
+
|
|
511
|
+
Ensure your `tsconfig.json` has:
|
|
512
|
+
```json
|
|
513
|
+
{
|
|
514
|
+
"compilerOptions": {
|
|
515
|
+
"moduleResolution": "bundler",
|
|
516
|
+
"strict": true
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## 📁 Project Structure
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
rn-persistent-timer/
|
|
527
|
+
├── src/
|
|
528
|
+
│ ├── index.ts # Public API barrel
|
|
529
|
+
│ ├── types.ts # All TypeScript types
|
|
530
|
+
│ ├── NativeTimerModule.ts # JS ↔ Native bridge
|
|
531
|
+
│ ├── PersistentTimerManager.ts # Core timer class
|
|
532
|
+
│ ├── usePersistentTimer.tsx # React hook
|
|
533
|
+
│ └── utils.ts # Utility functions
|
|
534
|
+
├── android/
|
|
535
|
+
│ ├── build.gradle
|
|
536
|
+
│ └── src/main/java/com/rnpersistenttimer/
|
|
537
|
+
│ ├── RNPersistentTimerModule.java
|
|
538
|
+
│ ├── RNPersistentTimerPackage.java
|
|
539
|
+
│ └── TimerForegroundService.java
|
|
540
|
+
├── ios/
|
|
541
|
+
│ ├── RNPersistentTimer.h
|
|
542
|
+
│ └── RNPersistentTimer.m
|
|
543
|
+
├── example/
|
|
544
|
+
│ └── App.tsx # 8-scenario demo app
|
|
545
|
+
└── lib/ # Built output (generated)
|
|
546
|
+
├── commonjs/
|
|
547
|
+
├── module/
|
|
548
|
+
└── typescript/
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## 🚀 Running the Example App
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
# 1. Clone or navigate to the package
|
|
557
|
+
cd /path/to/rn-persistent-timer
|
|
558
|
+
|
|
559
|
+
# 2. Install dependencies
|
|
560
|
+
yarn install
|
|
561
|
+
|
|
562
|
+
# 3. Install example dependencies
|
|
563
|
+
cd example && yarn install && cd ..
|
|
564
|
+
|
|
565
|
+
# 4. iOS — install pods
|
|
566
|
+
cd example/ios && pod install && cd ../..
|
|
567
|
+
npx react-native run-ios
|
|
568
|
+
|
|
569
|
+
# 5. Android
|
|
570
|
+
npx react-native run-android
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## 📤 Building & Publishing to npm
|
|
576
|
+
|
|
577
|
+
```bash
|
|
578
|
+
# 1. Install dependencies
|
|
579
|
+
yarn install
|
|
580
|
+
|
|
581
|
+
# 2. Build the TypeScript output
|
|
582
|
+
yarn build # runs react-native-builder-bob
|
|
583
|
+
|
|
584
|
+
# 3. Verify types compile
|
|
585
|
+
yarn typecheck # runs tsc --noEmit
|
|
586
|
+
|
|
587
|
+
# 4. Publish
|
|
588
|
+
npm publish --access public
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## 📄 License
|
|
594
|
+
|
|
595
|
+
MIT © Vipin Jaiswal
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## 🙏 Contributing
|
|
600
|
+
|
|
601
|
+
PRs and issues welcome! Please open an issue before starting a large change.
|
|
602
|
+
|
|
603
|
+
1. Fork the repo
|
|
604
|
+
2. Create your feature branch: `git checkout -b feat/my-feature`
|
|
605
|
+
3. Commit changes: `git commit -m 'feat: add my feature'`
|
|
606
|
+
4. Push: `git push origin feat/my-feature`
|
|
607
|
+
5. Open a Pull Request
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
android {
|
|
4
|
+
compileSdkVersion 34
|
|
5
|
+
defaultConfig {
|
|
6
|
+
minSdkVersion 21
|
|
7
|
+
targetSdkVersion 34
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
implementation 'com.facebook.react:react-native:+'
|
|
13
|
+
implementation 'androidx.work:work-runtime:2.9.0'
|
|
14
|
+
implementation 'androidx.core:core:1.12.0'
|
|
15
|
+
}
|