react-native-step-tracker-pro 1.0.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/LICENSE +21 -0
- package/README.md +148 -0
- package/android/build.gradle +119 -0
- package/android/consumer-rules.pro +13 -0
- package/android/gradle.properties +3 -0
- package/android/src/main/AndroidManifest.xml +78 -0
- package/android/src/main/java/com/steptrackerpro/StepTrackerProModule.kt +598 -0
- package/android/src/main/java/com/steptrackerpro/StepTrackerProPackage.kt +26 -0
- package/android/src/main/java/com/steptrackerpro/core/DateKeys.kt +90 -0
- package/android/src/main/java/com/steptrackerpro/core/GoalTracker.kt +66 -0
- package/android/src/main/java/com/steptrackerpro/core/MetricsCalculator.kt +48 -0
- package/android/src/main/java/com/steptrackerpro/core/Models.kt +73 -0
- package/android/src/main/java/com/steptrackerpro/core/StepCounterEngine.kt +338 -0
- package/android/src/main/java/com/steptrackerpro/core/StepStateStore.kt +137 -0
- package/android/src/main/java/com/steptrackerpro/core/StepTrackerConfig.kt +184 -0
- package/android/src/main/java/com/steptrackerpro/core/StepTrackerCore.kt +280 -0
- package/android/src/main/java/com/steptrackerpro/db/Entities.kt +67 -0
- package/android/src/main/java/com/steptrackerpro/db/StepDao.kt +118 -0
- package/android/src/main/java/com/steptrackerpro/db/StepDatabase.kt +60 -0
- package/android/src/main/java/com/steptrackerpro/db/StepRepository.kt +146 -0
- package/android/src/main/java/com/steptrackerpro/health/HealthConnectManager.kt +198 -0
- package/android/src/main/java/com/steptrackerpro/health/HealthPermissionActivity.kt +99 -0
- package/android/src/main/java/com/steptrackerpro/service/BootReceiver.kt +54 -0
- package/android/src/main/java/com/steptrackerpro/service/NotificationActionReceiver.kt +17 -0
- package/android/src/main/java/com/steptrackerpro/service/NotificationFactory.kt +170 -0
- package/android/src/main/java/com/steptrackerpro/service/ServiceCommands.kt +42 -0
- package/android/src/main/java/com/steptrackerpro/service/StepTrackerService.kt +409 -0
- package/android/src/main/java/com/steptrackerpro/sync/SyncScheduler.kt +102 -0
- package/android/src/main/java/com/steptrackerpro/sync/SyncWorkers.kt +175 -0
- package/android/src/main/java/com/steptrackerpro/util/BatteryOptimizationHelper.kt +94 -0
- package/android/src/main/java/com/steptrackerpro/util/Bridge.kt +149 -0
- package/android/src/main/java/com/steptrackerpro/util/PermissionHelper.kt +92 -0
- package/android/src/main/java/com/steptrackerpro/util/StepEventBus.kt +53 -0
- package/android/src/main/res/drawable/stp_ic_steps.xml +9 -0
- package/android/src/main/res/values/strings.xml +9 -0
- package/android/src/main/res/values/styles.xml +8 -0
- package/android/src/newarch/java/com/steptrackerpro/StepTrackerProSpec.kt +10 -0
- package/android/src/oldarch/java/com/steptrackerpro/StepTrackerProSpec.kt +60 -0
- package/docs/API.md +329 -0
- package/docs/ARCHITECTURE.md +203 -0
- package/docs/INSTALLATION.md +175 -0
- package/docs/PLAY_STORE_COMPLIANCE.md +122 -0
- package/docs/TESTING.md +312 -0
- package/docs/TROUBLESHOOTING.md +96 -0
- package/lib/commonjs/NativeStepTrackerPro.js +14 -0
- package/lib/commonjs/NativeStepTrackerPro.js.map +1 -0
- package/lib/commonjs/StepTracker.js +290 -0
- package/lib/commonjs/StepTracker.js.map +1 -0
- package/lib/commonjs/constants.js +30 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/errors.js +24 -0
- package/lib/commonjs/errors.js.map +1 -0
- package/lib/commonjs/hooks/index.js +20 -0
- package/lib/commonjs/hooks/index.js.map +1 -0
- package/lib/commonjs/hooks/useStepStats.js +51 -0
- package/lib/commonjs/hooks/useStepStats.js.map +1 -0
- package/lib/commonjs/hooks/useStepTracker.js +134 -0
- package/lib/commonjs/hooks/useStepTracker.js.map +1 -0
- package/lib/commonjs/index.js +64 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/NativeStepTrackerPro.js +12 -0
- package/lib/module/NativeStepTrackerPro.js.map +1 -0
- package/lib/module/StepTracker.js +284 -0
- package/lib/module/StepTracker.js.map +1 -0
- package/lib/module/constants.js +26 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/errors.js +18 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/hooks/index.js +5 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/useStepStats.js +46 -0
- package/lib/module/hooks/useStepStats.js.map +1 -0
- package/lib/module/hooks/useStepTracker.js +129 -0
- package/lib/module/hooks/useStepTracker.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/NativeStepTrackerPro.d.ts +50 -0
- package/lib/typescript/NativeStepTrackerPro.d.ts.map +1 -0
- package/lib/typescript/StepTracker.d.ts +76 -0
- package/lib/typescript/StepTracker.d.ts.map +1 -0
- package/lib/typescript/constants.d.ts +10 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/errors.d.ts +8 -0
- package/lib/typescript/errors.d.ts.map +1 -0
- package/lib/typescript/hooks/index.d.ts +5 -0
- package/lib/typescript/hooks/index.d.ts.map +1 -0
- package/lib/typescript/hooks/useStepStats.d.ts +11 -0
- package/lib/typescript/hooks/useStepStats.d.ts.map +1 -0
- package/lib/typescript/hooks/useStepTracker.d.ts +26 -0
- package/lib/typescript/hooks/useStepTracker.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 +183 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/package.json +79 -0
- package/src/NativeStepTrackerPro.ts +66 -0
- package/src/StepTracker.ts +410 -0
- package/src/constants.ts +43 -0
- package/src/errors.ts +31 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useStepStats.ts +55 -0
- package/src/hooks/useStepTracker.ts +167 -0
- package/src/index.ts +27 -0
- package/src/types.ts +203 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
|
2
|
+
import NativeStepTrackerPro from './NativeStepTrackerPro';
|
|
3
|
+
import type { Spec } from './NativeStepTrackerPro';
|
|
4
|
+
import { DEFAULT_CONFIG, MODULE_NAME, STRIDE_COEFFICIENT } from './constants';
|
|
5
|
+
import { StepTrackerError, toStepTrackerError } from './errors';
|
|
6
|
+
import type {
|
|
7
|
+
DayRecord,
|
|
8
|
+
DeviceCapabilities,
|
|
9
|
+
EventSubscription,
|
|
10
|
+
HealthConnectStatus,
|
|
11
|
+
PermissionStatus,
|
|
12
|
+
RangeOptions,
|
|
13
|
+
RangeStats,
|
|
14
|
+
StepSnapshot,
|
|
15
|
+
StepTrackerConfig,
|
|
16
|
+
StepTrackerEvent,
|
|
17
|
+
StepTrackerEventMap,
|
|
18
|
+
SyncEvent,
|
|
19
|
+
TrackingState,
|
|
20
|
+
} from './types';
|
|
21
|
+
|
|
22
|
+
const LINKING_ERROR =
|
|
23
|
+
`The native module '${MODULE_NAME}' could not be found.\n` +
|
|
24
|
+
'- Rebuild the app after installing (npx react-native run-android)\n' +
|
|
25
|
+
'- Make sure autolinking picked up the package\n' +
|
|
26
|
+
'- This package is Android only.';
|
|
27
|
+
|
|
28
|
+
function getNativeModule(): Spec {
|
|
29
|
+
if (Platform.OS !== 'android') {
|
|
30
|
+
throw new StepTrackerError(
|
|
31
|
+
'E_UNSUPPORTED_PLATFORM',
|
|
32
|
+
'react-native-step-tracker-pro only supports Android.'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const mod = (NativeStepTrackerPro ??
|
|
36
|
+
(NativeModules as Record<string, unknown>)[MODULE_NAME]) as Spec | undefined;
|
|
37
|
+
if (!mod) throw new StepTrackerError('E_UNKNOWN', LINKING_ERROR);
|
|
38
|
+
return mod;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Android is the only supported platform; call this before anything else. */
|
|
42
|
+
export function isSupported(): boolean {
|
|
43
|
+
if (Platform.OS !== 'android') return false;
|
|
44
|
+
return Boolean(
|
|
45
|
+
NativeStepTrackerPro ?? (NativeModules as Record<string, unknown>)[MODULE_NAME]
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let emitter: NativeEventEmitter | null = null;
|
|
50
|
+
function getEmitter(): NativeEventEmitter {
|
|
51
|
+
if (!emitter) {
|
|
52
|
+
emitter = new NativeEventEmitter(
|
|
53
|
+
(NativeModules as Record<string, never>)[MODULE_NAME] ?? undefined
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return emitter;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const EVENT_PREFIX = 'StepTrackerPro:';
|
|
60
|
+
|
|
61
|
+
async function call<T>(fn: () => Promise<T>): Promise<T> {
|
|
62
|
+
try {
|
|
63
|
+
return await fn();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw toStepTrackerError(error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveStride(config: StepTrackerConfig): number {
|
|
70
|
+
if (config.strideLength && config.strideLength > 0) return config.strideLength;
|
|
71
|
+
const height = config.height ?? DEFAULT_CONFIG.height;
|
|
72
|
+
const sex = config.sex ?? DEFAULT_CONFIG.sex;
|
|
73
|
+
return (height * STRIDE_COEFFICIENT[sex]) / 100;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DATE_KEY = /^\d{4}-\d{2}-\d{2}$/;
|
|
77
|
+
|
|
78
|
+
function assertRange(startDate: string, endDate: string): void {
|
|
79
|
+
if (!DATE_KEY.test(startDate) || !DATE_KEY.test(endDate)) {
|
|
80
|
+
throw new StepTrackerError(
|
|
81
|
+
'E_INVALID_CONFIG',
|
|
82
|
+
'Dates must be yyyy-MM-dd'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (startDate > endDate) {
|
|
86
|
+
throw new StepTrackerError(
|
|
87
|
+
'E_INVALID_CONFIG',
|
|
88
|
+
`startDate (${startDate}) must be on or before endDate (${endDate})`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normaliseConfig(config: StepTrackerConfig): StepTrackerConfig {
|
|
94
|
+
const daily = config.dailyGoal ?? DEFAULT_CONFIG.dailyGoal;
|
|
95
|
+
if (daily <= 0) {
|
|
96
|
+
throw new StepTrackerError('E_INVALID_CONFIG', 'dailyGoal must be > 0');
|
|
97
|
+
}
|
|
98
|
+
if (config.height != null && (config.height < 50 || config.height > 260)) {
|
|
99
|
+
throw new StepTrackerError('E_INVALID_CONFIG', 'height must be 50–260 cm');
|
|
100
|
+
}
|
|
101
|
+
if (config.weight != null && (config.weight < 10 || config.weight > 400)) {
|
|
102
|
+
throw new StepTrackerError('E_INVALID_CONFIG', 'weight must be 10–400 kg');
|
|
103
|
+
}
|
|
104
|
+
if (config.persistEveryNSteps != null && config.persistEveryNSteps < 1) {
|
|
105
|
+
// 0 makes the native `pendingCommit >= threshold` check true with nothing
|
|
106
|
+
// pending, which turns every step into a database write.
|
|
107
|
+
throw new StepTrackerError(
|
|
108
|
+
'E_INVALID_CONFIG',
|
|
109
|
+
'persistEveryNSteps must be >= 1'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (config.calorieCoefficient != null && config.calorieCoefficient < 0) {
|
|
113
|
+
throw new StepTrackerError(
|
|
114
|
+
'E_INVALID_CONFIG',
|
|
115
|
+
'calorieCoefficient must be >= 0'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (config.historyRetentionDays != null && config.historyRetentionDays < 1) {
|
|
119
|
+
throw new StepTrackerError(
|
|
120
|
+
'E_INVALID_CONFIG',
|
|
121
|
+
'historyRetentionDays must be >= 1'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const normalised: StepTrackerConfig = {
|
|
125
|
+
...DEFAULT_CONFIG,
|
|
126
|
+
...config,
|
|
127
|
+
weeklyGoal: config.weeklyGoal ?? daily * 7,
|
|
128
|
+
monthlyGoal: config.monthlyGoal ?? daily * 30,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Only derive a stride when the caller actually supplied something it is
|
|
132
|
+
// derived from. Deriving unconditionally meant that initialize({dailyGoal})
|
|
133
|
+
// on a returning user recomputed their stride from the default 170 cm and
|
|
134
|
+
// silently discarded the height they had configured; omitting the key leaves
|
|
135
|
+
// whatever the native side already has.
|
|
136
|
+
if (
|
|
137
|
+
config.strideLength != null ||
|
|
138
|
+
config.height != null ||
|
|
139
|
+
config.sex != null
|
|
140
|
+
) {
|
|
141
|
+
normalised.strideLength = resolveStride(config);
|
|
142
|
+
} else {
|
|
143
|
+
delete normalised.strideLength;
|
|
144
|
+
}
|
|
145
|
+
return normalised;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The public API. All methods reject with a `StepTrackerError` carrying a
|
|
150
|
+
* stable `code`, so callers can branch without string matching.
|
|
151
|
+
*/
|
|
152
|
+
export const StepTracker = {
|
|
153
|
+
/**
|
|
154
|
+
* Loads config into the native layer and restores any persisted state.
|
|
155
|
+
* Safe to call on every app launch — it is idempotent.
|
|
156
|
+
*/
|
|
157
|
+
async initialize(config: StepTrackerConfig = {}): Promise<StepSnapshot> {
|
|
158
|
+
const merged = normaliseConfig(config);
|
|
159
|
+
return call(() =>
|
|
160
|
+
getNativeModule().initialize(merged)
|
|
161
|
+
) as Promise<StepSnapshot>;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/** Patches config at runtime. Notification and goals update immediately. */
|
|
165
|
+
async updateConfig(config: StepTrackerConfig): Promise<StepTrackerConfig> {
|
|
166
|
+
const patch: StepTrackerConfig = { ...config };
|
|
167
|
+
if (config.height != null || config.sex != null || config.strideLength != null) {
|
|
168
|
+
// Stride depends on height *and* sex, so a patch carrying only one of
|
|
169
|
+
// them has to be combined with the stored value of the other. Passing the
|
|
170
|
+
// bare patch fell back to the package defaults instead, which silently
|
|
171
|
+
// reset a 190 cm user to 170 cm on updateConfig({ sex }).
|
|
172
|
+
const current = await StepTracker.getConfig();
|
|
173
|
+
patch.strideLength = resolveStride({
|
|
174
|
+
height: config.height ?? current.height,
|
|
175
|
+
sex: config.sex ?? current.sex,
|
|
176
|
+
strideLength: config.strideLength,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return call(() =>
|
|
180
|
+
getNativeModule().updateConfig(patch)
|
|
181
|
+
) as Promise<StepTrackerConfig>;
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async getConfig(): Promise<StepTrackerConfig> {
|
|
185
|
+
return call(() => getNativeModule().getConfig()) as Promise<StepTrackerConfig>;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/** Starts the foreground service. Requires permissions to be granted first. */
|
|
189
|
+
async startTracking(): Promise<StepSnapshot> {
|
|
190
|
+
return call(() => getNativeModule().startTracking()) as Promise<StepSnapshot>;
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/** Keeps the service and notification alive but ignores sensor deltas. */
|
|
194
|
+
async pauseTracking(): Promise<StepSnapshot> {
|
|
195
|
+
return call(() => getNativeModule().pauseTracking()) as Promise<StepSnapshot>;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async resumeTracking(): Promise<StepSnapshot> {
|
|
199
|
+
return call(() => getNativeModule().resumeTracking()) as Promise<StepSnapshot>;
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/** Flushes to the database, unregisters sensors and kills the service. */
|
|
203
|
+
async stopTracking(): Promise<StepSnapshot> {
|
|
204
|
+
return call(() => getNativeModule().stopTracking()) as Promise<StepSnapshot>;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async getTrackingState(): Promise<TrackingState> {
|
|
208
|
+
const result = (await call(() => getNativeModule().getTrackingState())) as {
|
|
209
|
+
state: TrackingState;
|
|
210
|
+
};
|
|
211
|
+
return result.state;
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
async isTracking(): Promise<boolean> {
|
|
215
|
+
return call(() => getNativeModule().isTracking());
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
// ---- reads -----------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
async getTodaySteps(): Promise<StepSnapshot> {
|
|
221
|
+
return call(() => getNativeModule().getTodaySteps()) as Promise<StepSnapshot>;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
async getYesterdaySteps(): Promise<DayRecord> {
|
|
225
|
+
return call(() => getNativeModule().getYesterdaySteps()) as Promise<DayRecord>;
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
/** @param date yyyy-MM-dd */
|
|
229
|
+
async getStepsForDate(date: string): Promise<DayRecord> {
|
|
230
|
+
return call(() => getNativeModule().getStepsForDate(date)) as Promise<DayRecord>;
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
async getWeeklyStats(options: RangeOptions = {}): Promise<RangeStats> {
|
|
234
|
+
return call(() => getNativeModule().getWeeklyStats(options)) as Promise<RangeStats>;
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async getMonthlyStats(options: RangeOptions = {}): Promise<RangeStats> {
|
|
238
|
+
return call(() => getNativeModule().getMonthlyStats(options)) as Promise<RangeStats>;
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
async getYearlyStats(options: RangeOptions = {}): Promise<RangeStats> {
|
|
242
|
+
return call(() => getNativeModule().getYearlyStats(options)) as Promise<RangeStats>;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/** Inclusive on both ends. Dates are yyyy-MM-dd, and start must precede end. */
|
|
246
|
+
async getStatsForRange(startDate: string, endDate: string): Promise<RangeStats> {
|
|
247
|
+
assertRange(startDate, endDate);
|
|
248
|
+
return call(() =>
|
|
249
|
+
getNativeModule().getStatsForRange(startDate, endDate)
|
|
250
|
+
) as Promise<RangeStats>;
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
async getHistory(startDate: string, endDate: string): Promise<DayRecord[]> {
|
|
254
|
+
assertRange(startDate, endDate);
|
|
255
|
+
const result = (await call(() =>
|
|
256
|
+
getNativeModule().getHistory(startDate, endDate)
|
|
257
|
+
)) as { records: DayRecord[] };
|
|
258
|
+
return result.records;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// ---- writes ----------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
/** Zeroes today's counter without touching history. Useful in QA builds. */
|
|
264
|
+
async resetToday(): Promise<boolean> {
|
|
265
|
+
return call(() => getNativeModule().resetToday());
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
async clearHistory(): Promise<boolean> {
|
|
269
|
+
return call(() => getNativeModule().clearHistory());
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
/** Deletes rows older than `retentionDays`; returns rows removed. */
|
|
273
|
+
async pruneHistory(retentionDays?: number): Promise<number> {
|
|
274
|
+
return call(() =>
|
|
275
|
+
getNativeModule().pruneHistory(
|
|
276
|
+
retentionDays ?? DEFAULT_CONFIG.historyRetentionDays
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
// ---- permissions -----------------------------------------------------
|
|
282
|
+
|
|
283
|
+
async checkPermissions(): Promise<PermissionStatus> {
|
|
284
|
+
return call(() => getNativeModule().checkPermissions()) as Promise<PermissionStatus>;
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
/** Shows the Android runtime dialogs. Must be called with an Activity attached. */
|
|
288
|
+
async requestPermissions(): Promise<PermissionStatus> {
|
|
289
|
+
return call(() => getNativeModule().requestPermissions()) as Promise<PermissionStatus>;
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async getDeviceCapabilities(): Promise<DeviceCapabilities> {
|
|
293
|
+
return call(() =>
|
|
294
|
+
getNativeModule().getDeviceCapabilities()
|
|
295
|
+
) as Promise<DeviceCapabilities>;
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
async openAppSettings(): Promise<boolean> {
|
|
299
|
+
return call(() => getNativeModule().openAppSettings());
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
// ---- battery ---------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
/** True when the OS is still applying doze restrictions to this app. */
|
|
305
|
+
async isBatteryOptimizationEnabled(): Promise<boolean> {
|
|
306
|
+
return call(() => getNativeModule().isBatteryOptimizationEnabled());
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Shows the system exemption dialog. Play policy requires an eligible use
|
|
311
|
+
* case — see docs/PLAY_STORE_COMPLIANCE.md before shipping this.
|
|
312
|
+
*/
|
|
313
|
+
async requestDisableBatteryOptimization(): Promise<boolean> {
|
|
314
|
+
return call(() => getNativeModule().requestDisableBatteryOptimization());
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
/** Policy-safe alternative: opens the settings list, no direct prompt. */
|
|
318
|
+
async openBatteryOptimizationSettings(): Promise<boolean> {
|
|
319
|
+
return call(() => getNativeModule().openBatteryOptimizationSettings());
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
/** Best-effort deep link into Xiaomi/Oppo/Vivo/Huawei autostart screens. */
|
|
323
|
+
async openManufacturerAutoStartSettings(): Promise<boolean> {
|
|
324
|
+
return call(() => getNativeModule().openManufacturerAutoStartSettings());
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// ---- health connect --------------------------------------------------
|
|
328
|
+
|
|
329
|
+
async getHealthConnectStatus(): Promise<HealthConnectStatus> {
|
|
330
|
+
return call(() =>
|
|
331
|
+
getNativeModule().getHealthConnectStatus()
|
|
332
|
+
) as Promise<HealthConnectStatus>;
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
async requestHealthConnectPermissions(): Promise<HealthConnectStatus> {
|
|
336
|
+
return call(() =>
|
|
337
|
+
getNativeModule().requestHealthConnectPermissions()
|
|
338
|
+
) as Promise<HealthConnectStatus>;
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
async openHealthConnectSettings(): Promise<boolean> {
|
|
342
|
+
return call(() => getNativeModule().openHealthConnectSettings());
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/** ISO-8601 instants, e.g. '2026-09-01T00:00:00Z'. */
|
|
346
|
+
async readHealthConnectSteps(
|
|
347
|
+
startIso: string,
|
|
348
|
+
endIso: string
|
|
349
|
+
): Promise<{ totalSteps: number; records: DayRecord[] }> {
|
|
350
|
+
return call(() =>
|
|
351
|
+
getNativeModule().readHealthConnectSteps(startIso, endIso)
|
|
352
|
+
) as Promise<{ totalSteps: number; records: DayRecord[] }>;
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
/** Upserts one day's steps/distance/calories into Health Connect. */
|
|
356
|
+
async writeHealthConnectSteps(date: string): Promise<boolean> {
|
|
357
|
+
return call(() => getNativeModule().writeHealthConnectSteps(date));
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
async syncWithHealthConnect(): Promise<SyncEvent> {
|
|
361
|
+
return call(() => getNativeModule().syncWithHealthConnect()) as Promise<SyncEvent>;
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
// ---- sync ------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
async getPendingSyncCount(): Promise<number> {
|
|
367
|
+
return call(() => getNativeModule().getPendingSyncCount());
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
/** Runs Health Connect + remote sync immediately instead of waiting for WorkManager. */
|
|
371
|
+
async syncNow(): Promise<SyncEvent[]> {
|
|
372
|
+
const result = (await call(() => getNativeModule().syncNow())) as {
|
|
373
|
+
results: SyncEvent[];
|
|
374
|
+
};
|
|
375
|
+
return result.results;
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
// ---- events ----------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
addListener<E extends StepTrackerEvent>(
|
|
381
|
+
event: E,
|
|
382
|
+
listener: (payload: StepTrackerEventMap[E]) => void
|
|
383
|
+
): EventSubscription {
|
|
384
|
+
if (Platform.OS !== 'android') return { remove: () => {} };
|
|
385
|
+
const sub = getEmitter().addListener(`${EVENT_PREFIX}${event}`, listener);
|
|
386
|
+
return { remove: () => sub.remove() };
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
/** Removes every listener for one event, or all events when omitted. */
|
|
390
|
+
removeListener(event?: StepTrackerEvent): void {
|
|
391
|
+
if (Platform.OS !== 'android') return;
|
|
392
|
+
const em = getEmitter();
|
|
393
|
+
if (event) em.removeAllListeners(`${EVENT_PREFIX}${event}`);
|
|
394
|
+
else {
|
|
395
|
+
(
|
|
396
|
+
[
|
|
397
|
+
'stepsChanged',
|
|
398
|
+
'goalReached',
|
|
399
|
+
'goalProgressChanged',
|
|
400
|
+
'trackingStateChanged',
|
|
401
|
+
'dayChanged',
|
|
402
|
+
'syncCompleted',
|
|
403
|
+
'error',
|
|
404
|
+
] as StepTrackerEvent[]
|
|
405
|
+
).forEach((name) => em.removeAllListeners(`${EVENT_PREFIX}${name}`));
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export default StepTracker;
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { StepTrackerConfig } from './types';
|
|
2
|
+
|
|
3
|
+
export const MODULE_NAME = 'StepTrackerPro';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_CONFIG: Required<
|
|
6
|
+
Pick<
|
|
7
|
+
StepTrackerConfig,
|
|
8
|
+
| 'height'
|
|
9
|
+
| 'weight'
|
|
10
|
+
| 'sex'
|
|
11
|
+
| 'dailyGoal'
|
|
12
|
+
| 'calorieCoefficient'
|
|
13
|
+
| 'historyRetentionDays'
|
|
14
|
+
| 'notificationActions'
|
|
15
|
+
| 'notificationThrottleMs'
|
|
16
|
+
| 'eventThrottleMs'
|
|
17
|
+
| 'persistEveryNSteps'
|
|
18
|
+
| 'healthConnectEnabled'
|
|
19
|
+
| 'healthConnectSyncIntervalMinutes'
|
|
20
|
+
| 'autoStartOnBoot'
|
|
21
|
+
>
|
|
22
|
+
> = {
|
|
23
|
+
height: 170,
|
|
24
|
+
weight: 70,
|
|
25
|
+
sex: 'unspecified',
|
|
26
|
+
dailyGoal: 10000,
|
|
27
|
+
calorieCoefficient: 0.57,
|
|
28
|
+
historyRetentionDays: 35,
|
|
29
|
+
notificationActions: true,
|
|
30
|
+
notificationThrottleMs: 1000,
|
|
31
|
+
eventThrottleMs: 500,
|
|
32
|
+
persistEveryNSteps: 10,
|
|
33
|
+
healthConnectEnabled: true,
|
|
34
|
+
healthConnectSyncIntervalMinutes: 30,
|
|
35
|
+
autoStartOnBoot: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Stride length = height(cm) * coefficient / 100. */
|
|
39
|
+
export const STRIDE_COEFFICIENT = {
|
|
40
|
+
male: 0.415,
|
|
41
|
+
female: 0.413,
|
|
42
|
+
unspecified: 0.414,
|
|
43
|
+
} as const;
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type StepTrackerErrorCode =
|
|
2
|
+
| 'E_UNSUPPORTED_PLATFORM'
|
|
3
|
+
| 'E_NO_SENSOR'
|
|
4
|
+
| 'E_NOT_INITIALIZED'
|
|
5
|
+
| 'E_PERMISSION_DENIED'
|
|
6
|
+
| 'E_SERVICE_START_FAILED'
|
|
7
|
+
| 'E_HEALTH_CONNECT_UNAVAILABLE'
|
|
8
|
+
| 'E_HEALTH_CONNECT_DENIED'
|
|
9
|
+
| 'E_DATABASE'
|
|
10
|
+
| 'E_INVALID_CONFIG'
|
|
11
|
+
| 'E_NO_ACTIVITY'
|
|
12
|
+
| 'E_NOT_TRACKING'
|
|
13
|
+
| 'E_UNKNOWN';
|
|
14
|
+
|
|
15
|
+
export class StepTrackerError extends Error {
|
|
16
|
+
readonly code: StepTrackerErrorCode;
|
|
17
|
+
|
|
18
|
+
constructor(code: StepTrackerErrorCode, message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'StepTrackerError';
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Normalises rejections coming off the native bridge. */
|
|
26
|
+
export function toStepTrackerError(error: unknown): StepTrackerError {
|
|
27
|
+
if (error instanceof StepTrackerError) return error;
|
|
28
|
+
const anyError = error as { code?: string; message?: string } | null;
|
|
29
|
+
const code = (anyError?.code ?? 'E_UNKNOWN') as StepTrackerErrorCode;
|
|
30
|
+
return new StepTrackerError(code, anyError?.message ?? 'Unknown native error');
|
|
31
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import StepTracker, { isSupported } from '../StepTracker';
|
|
3
|
+
import type { RangeOptions, RangeStats } from '../types';
|
|
4
|
+
|
|
5
|
+
export type StatsPeriod = 'week' | 'month' | 'year';
|
|
6
|
+
|
|
7
|
+
export interface UseStepStatsResult {
|
|
8
|
+
stats: RangeStats | null;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
reload: () => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Fetches a windowed summary and refreshes it when the day rolls over. */
|
|
15
|
+
export function useStepStats(
|
|
16
|
+
period: StatsPeriod,
|
|
17
|
+
options: RangeOptions = {}
|
|
18
|
+
): UseStepStatsResult {
|
|
19
|
+
const [stats, setStats] = useState<RangeStats | null>(null);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [error, setError] = useState<Error | null>(null);
|
|
22
|
+
|
|
23
|
+
const { mode, offset } = options;
|
|
24
|
+
|
|
25
|
+
const reload = useCallback(async () => {
|
|
26
|
+
if (!isSupported()) {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
setLoading(true);
|
|
31
|
+
try {
|
|
32
|
+
const opts: RangeOptions = { mode, offset };
|
|
33
|
+
const next =
|
|
34
|
+
period === 'week'
|
|
35
|
+
? await StepTracker.getWeeklyStats(opts)
|
|
36
|
+
: period === 'month'
|
|
37
|
+
? await StepTracker.getMonthlyStats(opts)
|
|
38
|
+
: await StepTracker.getYearlyStats(opts);
|
|
39
|
+
setStats(next);
|
|
40
|
+
setError(null);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
setError(e as Error);
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, [period, mode, offset]);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
void reload();
|
|
50
|
+
const sub = StepTracker.addListener('dayChanged', () => void reload());
|
|
51
|
+
return () => sub.remove();
|
|
52
|
+
}, [reload]);
|
|
53
|
+
|
|
54
|
+
return { stats, loading, error, reload };
|
|
55
|
+
}
|