@adminide-stack/yantra-mobile 12.0.125 → 12.0.127-alpha.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/lib/features/health/HealthSyncRunner.js +9 -1
- package/lib/features/health/HealthSyncRunner.js.map +1 -1
- package/lib/features/health/backgroundSync.js +41 -0
- package/lib/features/health/backgroundSync.js.map +1 -0
- package/lib/features/health/healthConnectBackground.js +32 -0
- package/lib/features/health/healthConnectBackground.js.map +1 -0
- package/lib/features/health/healthConnectReader.js +16 -2
- package/lib/features/health/healthConnectReader.js.map +1 -1
- package/lib/features/health/healthSync.js +31 -3
- package/lib/features/health/healthSync.js.map +1 -1
- package/lib/features/health/healthSyncStore.js +89 -5
- package/lib/features/health/healthSyncStore.js.map +1 -1
- package/lib/features/health/healthkitBackground.js +17 -0
- package/lib/features/health/healthkitBackground.js.map +1 -0
- package/lib/features/health/reader.js.map +1 -1
- package/lib/features/health/sessionVault.js +38 -0
- package/lib/features/health/sessionVault.js.map +1 -0
- package/lib/features/health/syncState.js.map +1 -1
- package/lib/features/messaging/useChatViewer.js +14 -18
- package/lib/features/messaging/useChatViewer.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/screens/Health/index.js +12 -2
- package/lib/screens/Health/index.js.map +1 -1
- package/package.json +6 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {jsx}from'react/jsx-runtime';import {useEffect}from'react';import {AppState}from'react-native';import {useSelector}from'react-redux';import {isAuthenticatedSelector}from'@adminide-stack/user-auth0-client';import {config}from'../../config/env-config.js';import {useCanvasSession}from'../canvas/useCanvasSession.js';import {sessionIdentityFromToken}from'../canvas/sessionIdentity.js';import {useHealthSync,healthSyncStore}from'./healthSync.js';import {useDevices,deviceStore}from'../devices/devices.js';function ConnectedHealthSync({
|
|
1
|
+
import {jsx}from'react/jsx-runtime';import {useEffect,useRef}from'react';import {AppState}from'react-native';import {useSelector}from'react-redux';import {isAuthenticatedSelector}from'@adminide-stack/user-auth0-client';import {config}from'../../config/env-config.js';import {useCanvasSession}from'../canvas/useCanvasSession.js';import {sessionIdentityFromToken}from'../canvas/sessionIdentity.js';import {useHealthSync,healthSyncStore}from'./healthSync.js';import {useDevices,deviceStore}from'../devices/devices.js';function ConnectedHealthSync({
|
|
2
2
|
syncOn
|
|
3
3
|
}) {
|
|
4
4
|
const {
|
|
@@ -43,6 +43,14 @@ function HealthSyncRunner() {
|
|
|
43
43
|
useEffect(() => {
|
|
44
44
|
void healthSyncStore.load();
|
|
45
45
|
}, []);
|
|
46
|
+
const wasAuthenticated = useRef(false);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (isAuthenticated) wasAuthenticated.current = true;
|
|
49
|
+
else if (wasAuthenticated.current) {
|
|
50
|
+
wasAuthenticated.current = false;
|
|
51
|
+
void healthSyncStore.forgetSavedSession();
|
|
52
|
+
}
|
|
53
|
+
}, [isAuthenticated]);
|
|
46
54
|
const syncOn = snapshot.loaded && snapshot.sync.connected;
|
|
47
55
|
const hasDevices = devices.loaded && (devices.devices.length > 0 || devices.pendingDates.length > 0);
|
|
48
56
|
if (!isAuthenticated || !syncOn && !hasDevices) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HealthSyncRunner.js","sources":["../../../src/features/health/HealthSyncRunner.tsx"],"sourcesContent":["/**\n * Mounted once at the app root (portable-devices/mobile App.tsx), next to\n * the invite deep-link handlers. When health sync is on, or a Bluetooth device\n * is paired, and someone is signed in, it keeps a platform session for the\n * Fitness backend: health syncs on every return to the foreground (throttled\n * in the store), and device readings waiting to be sent go out. Renders nothing.\n *\n * Signed out, or neither sync nor a device: no session is minted and nothing runs.\n */\nimport { useEffect } from 'react';\nimport { AppState } from 'react-native';\nimport { useSelector } from 'react-redux';\nimport { isAuthenticatedSelector } from '@adminide-stack/user-auth0-client';\nimport { config } from '../../config/env-config';\nimport { useCanvasSession } from '../canvas/useCanvasSession';\nimport { sessionIdentityFromToken } from '../canvas/sessionIdentity';\nimport { healthSyncStore, useHealthSync } from './healthSync';\nimport { deviceStore, useDevices } from '../devices/devices';\n\nfunction ConnectedHealthSync({ syncOn }: { syncOn: boolean }) {\n // The surface session: the same platform system JWT the canvas and the\n // other surface backends accept, renewed before it expires.\n const { token, error } = useCanvasSession();\n const url = config.FITNESS_BACKEND_GRAPHQL_URL;\n\n useEffect(() => {\n if (token && url) {\n const session = { token, url, owner: sessionIdentityFromToken(token).user };\n healthSyncStore.setSession(session);\n deviceStore.setSession(session);\n } else {\n healthSyncStore.setSession(\n null,\n !url ? 'The Fitness service address is not configured in this build.' : error,\n );\n deviceStore.setSession(null);\n }\n }, [token, url, error]);\n\n useEffect(\n () => () => {\n healthSyncStore.setSession(null);\n deviceStore.setSession(null);\n },\n [],\n );\n\n useEffect(() => {\n if (syncOn) void healthSyncStore.syncIfDue();\n void deviceStore.flush();\n const sub = AppState.addEventListener('change', (next) => {\n if (next !== 'active') return;\n if (syncOn) void healthSyncStore.syncIfDue();\n void deviceStore.flush();\n });\n return () => sub.remove();\n }, [syncOn]);\n\n return null;\n}\n\nexport function HealthSyncRunner() {\n const snapshot = useHealthSync();\n const devices = useDevices();\n const isAuthenticated = useSelector(isAuthenticatedSelector);\n\n useEffect(() => {\n void healthSyncStore.load();\n }, []);\n\n const syncOn = snapshot.loaded && snapshot.sync.connected;\n const hasDevices = devices.loaded && (devices.devices.length > 0 || devices.pendingDates.length > 0);\n if (!isAuthenticated || (!syncOn && !hasDevices)) return null;\n return <ConnectedHealthSync syncOn={syncOn} />;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"HealthSyncRunner.js","sources":["../../../src/features/health/HealthSyncRunner.tsx"],"sourcesContent":["/**\n * Mounted once at the app root (portable-devices/mobile App.tsx), next to\n * the invite deep-link handlers. When health sync is on, or a Bluetooth device\n * is paired, and someone is signed in, it keeps a platform session for the\n * Fitness backend: health syncs on every return to the foreground (throttled\n * in the store), and device readings waiting to be sent go out. Renders nothing.\n *\n * Signed out, or neither sync nor a device: no session is minted and nothing runs.\n * Signing out also forgets the session kept for background syncs (backgroundSync.ts).\n */\nimport { useEffect, useRef } from 'react';\nimport { AppState } from 'react-native';\nimport { useSelector } from 'react-redux';\nimport { isAuthenticatedSelector } from '@adminide-stack/user-auth0-client';\nimport { config } from '../../config/env-config';\nimport { useCanvasSession } from '../canvas/useCanvasSession';\nimport { sessionIdentityFromToken } from '../canvas/sessionIdentity';\nimport { healthSyncStore, useHealthSync } from './healthSync';\nimport { deviceStore, useDevices } from '../devices/devices';\n\nfunction ConnectedHealthSync({ syncOn }: { syncOn: boolean }) {\n // The surface session: the same platform system JWT the canvas and the\n // other surface backends accept, renewed before it expires.\n const { token, error } = useCanvasSession();\n const url = config.FITNESS_BACKEND_GRAPHQL_URL;\n\n useEffect(() => {\n if (token && url) {\n const session = { token, url, owner: sessionIdentityFromToken(token).user };\n healthSyncStore.setSession(session);\n deviceStore.setSession(session);\n } else {\n healthSyncStore.setSession(\n null,\n !url ? 'The Fitness service address is not configured in this build.' : error,\n );\n deviceStore.setSession(null);\n }\n }, [token, url, error]);\n\n useEffect(\n () => () => {\n healthSyncStore.setSession(null);\n deviceStore.setSession(null);\n },\n [],\n );\n\n useEffect(() => {\n if (syncOn) void healthSyncStore.syncIfDue();\n void deviceStore.flush();\n const sub = AppState.addEventListener('change', (next) => {\n if (next !== 'active') return;\n if (syncOn) void healthSyncStore.syncIfDue();\n void deviceStore.flush();\n });\n return () => sub.remove();\n }, [syncOn]);\n\n return null;\n}\n\nexport function HealthSyncRunner() {\n const snapshot = useHealthSync();\n const devices = useDevices();\n const isAuthenticated = useSelector(isAuthenticatedSelector);\n\n useEffect(() => {\n void healthSyncStore.load();\n }, []);\n\n // Only a real sign-out (signed in, then not): a cold start reads false before auth hydrates.\n const wasAuthenticated = useRef(false);\n useEffect(() => {\n if (isAuthenticated) wasAuthenticated.current = true;\n else if (wasAuthenticated.current) {\n wasAuthenticated.current = false;\n void healthSyncStore.forgetSavedSession();\n }\n }, [isAuthenticated]);\n\n const syncOn = snapshot.loaded && snapshot.sync.connected;\n const hasDevices = devices.loaded && (devices.devices.length > 0 || devices.pendingDates.length > 0);\n if (!isAuthenticated || (!syncOn && !hasDevices)) return null;\n return <ConnectedHealthSync syncOn={syncOn} />;\n}\n"],"names":[],"mappings":"mgBAmBA,SAAS,mBAAoB,CAAA;AAAA,EAC3B;AACF,CAEG,EAAA;AAGD,EAAM,MAAA;AAAA,IACJ,KAAA;AAAA,IACA;AAAA,MACE,gBAAiB,EAAA;AACrB,EAAA,MAAM,MAAM,MAAO,CAAA,2BAAA;AACnB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,SAAS,GAAK,EAAA;AAChB,MAAA,MAAM,OAAU,GAAA;AAAA,QACd,KAAA;AAAA,QACA,GAAA;AAAA,QACA,KAAA,EAAO,wBAAyB,CAAA,KAAK,CAAE,CAAA;AAAA,OACzC;AACA,MAAA,eAAA,CAAgB,WAAW,OAAO,CAAA;AAClC,MAAA,WAAA,CAAY,WAAW,OAAO,CAAA;AAAA,KACzB,MAAA;AACL,MAAA,eAAA,CAAgB,UAAW,CAAA,IAAA,EAAM,CAAC,GAAA,GAAM,iEAAiE,KAAK,CAAA;AAC9G,MAAA,WAAA,CAAY,WAAW,IAAI,CAAA;AAAA;AAC7B,GACC,EAAA,CAAC,KAAO,EAAA,GAAA,EAAK,KAAK,CAAC,CAAA;AACtB,EAAA,SAAA,CAAU,MAAM,MAAM;AACpB,IAAA,eAAA,CAAgB,WAAW,IAAI,CAAA;AAC/B,IAAA,WAAA,CAAY,WAAW,IAAI,CAAA;AAAA,GAC7B,EAAG,EAAE,CAAA;AACL,EAAA,SAAA,CAAU,MAAM;AACd,IAAI,IAAA,MAAA,EAAa,KAAA,eAAA,CAAgB,SAAU,EAAA;AAC3C,IAAA,KAAK,YAAY,KAAM,EAAA;AACvB,IAAA,MAAM,GAAM,GAAA,QAAA,CAAS,gBAAiB,CAAA,QAAA,EAAU,CAAQ,IAAA,KAAA;AACtD,MAAA,IAAI,SAAS,QAAU,EAAA;AACvB,MAAI,IAAA,MAAA,EAAa,KAAA,eAAA,CAAgB,SAAU,EAAA;AAC3C,MAAA,KAAK,YAAY,KAAM,EAAA;AAAA,KACxB,CAAA;AACD,IAAO,OAAA,MAAM,IAAI,MAAO,EAAA;AAAA,GAC1B,EAAG,CAAC,MAAM,CAAC,CAAA;AACX,EAAO,OAAA,IAAA;AACT;AACO,SAAS,gBAAmB,GAAA;AACjC,EAAA,MAAM,WAAW,aAAc,EAAA;AAC/B,EAAA,MAAM,UAAU,UAAW,EAAA;AAC3B,EAAM,MAAA,eAAA,GAAkB,YAAY,uBAAuB,CAAA;AAC3D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,KAAK,gBAAgB,IAAK,EAAA;AAAA,GAC5B,EAAG,EAAE,CAAA;AAGL,EAAM,MAAA,gBAAA,GAAmB,OAAO,KAAK,CAAA;AACrC,EAAA,SAAA,CAAU,MAAM;AACd,IAAI,IAAA,eAAA,mBAAkC,OAAU,GAAA,IAAA;AAAA,SAAA,IAAc,iBAAiB,OAAS,EAAA;AACtF,MAAA,gBAAA,CAAiB,OAAU,GAAA,KAAA;AAC3B,MAAA,KAAK,gBAAgB,kBAAmB,EAAA;AAAA;AAC1C,GACF,EAAG,CAAC,eAAe,CAAC,CAAA;AACpB,EAAA,MAAM,MAAS,GAAA,QAAA,CAAS,MAAU,IAAA,QAAA,CAAS,IAAK,CAAA,SAAA;AAChD,EAAM,MAAA,UAAA,GAAa,QAAQ,MAAW,KAAA,OAAA,CAAQ,QAAQ,MAAS,GAAA,CAAA,IAAK,OAAQ,CAAA,YAAA,CAAa,MAAS,GAAA,CAAA,CAAA;AAClG,EAAA,IAAI,CAAC,eAAmB,IAAA,CAAC,MAAU,IAAA,CAAC,YAAmB,OAAA,IAAA;AACvD,EAAO,uBAAA,GAAA,CAAC,uBAAoB,MAAgB,EAAA,CAAA;AAC9C"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {AppState}from'react-native';import {loadPlatformBackground,healthSyncStore}from'./healthSync.js';const COALESCE_MS = 3e3;
|
|
2
|
+
function createBackgroundSync(deps) {
|
|
3
|
+
let timer = null;
|
|
4
|
+
let waiting = [];
|
|
5
|
+
let started = null;
|
|
6
|
+
const onChange = () => new Promise((resolve) => {
|
|
7
|
+
waiting.push(resolve);
|
|
8
|
+
if (timer) clearTimeout(timer);
|
|
9
|
+
timer = setTimeout(() => {
|
|
10
|
+
timer = null;
|
|
11
|
+
const done = waiting;
|
|
12
|
+
waiting = [];
|
|
13
|
+
const run = deps.isForeground() ? deps.store.syncIfDue() : deps.store.syncInBackground();
|
|
14
|
+
void run.catch(() => void 0).then(() => done.forEach((d) => d()));
|
|
15
|
+
}, COALESCE_MS);
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
onChange,
|
|
19
|
+
/** Subscribe (iOS delivers anything queued since launch), then make sure delivery is on. */
|
|
20
|
+
start() {
|
|
21
|
+
if (!started) {
|
|
22
|
+
started = (async () => {
|
|
23
|
+
const source = await deps.loadSource();
|
|
24
|
+
if (!source) return;
|
|
25
|
+
source.watch(onChange);
|
|
26
|
+
await deps.store.ensureBackgroundDelivery();
|
|
27
|
+
})().catch(() => void 0);
|
|
28
|
+
}
|
|
29
|
+
return started;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const healthBackgroundSync = createBackgroundSync({
|
|
34
|
+
store: healthSyncStore,
|
|
35
|
+
loadSource: loadPlatformBackground,
|
|
36
|
+
// A background launch reads 'background' (or 'unknown' this early): both are the background path.
|
|
37
|
+
isForeground: () => AppState.currentState === "active"
|
|
38
|
+
});
|
|
39
|
+
function startHealthBackgroundSync() {
|
|
40
|
+
void healthBackgroundSync.start();
|
|
41
|
+
}export{COALESCE_MS,createBackgroundSync,healthBackgroundSync,startHealthBackgroundSync};//# sourceMappingURL=backgroundSync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backgroundSync.js","sources":["../../../src/features/health/backgroundSync.ts"],"sourcesContent":["/**\n * Health sync while Yantra is closed, the way Bevel stays current.\n *\n * Started once at every launch (portable-devices/mobile App.tsx), including\n * the launches made in the background: iOS when the watch saves new data\n * (healthkitBackground.ts), Android for its hourly job, since Health Connect\n * cannot wake an app (healthConnectBackground.ts). A burst of changes - sleep,\n * HRV and resting heart rate land together - becomes one sync: in the\n * background through `syncInBackground` (the saved session, never a sign-in),\n * and while the app is open through the foreground's own throttled\n * `syncIfDue`, so an open app does not sync every time the watch saves a step.\n */\nimport { AppState } from 'react-native';\nimport type { BackgroundSource, HealthSyncStore } from './healthSyncStore';\nimport { healthSyncStore, loadPlatformBackground } from './healthSync';\n\n/** Changes this close together are one burst. */\nexport const COALESCE_MS = 3_000;\n\nexport function createBackgroundSync(deps: {\n store: Pick<HealthSyncStore, 'syncInBackground' | 'syncIfDue' | 'ensureBackgroundDelivery'>;\n loadSource: () => Promise<BackgroundSource | null>;\n isForeground: () => boolean;\n}) {\n let timer: ReturnType<typeof setTimeout> | null = null;\n let waiting: Array<() => void> = [];\n let started: Promise<void> | null = null;\n /** Resolves once the sync this change joined has finished (Android's job waits for it). */\n const onChange = (): Promise<void> =>\n new Promise((resolve) => {\n waiting.push(resolve);\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n const done = waiting;\n waiting = [];\n const run = deps.isForeground() ? deps.store.syncIfDue() : deps.store.syncInBackground();\n void run.catch(() => undefined).then(() => done.forEach((d) => d()));\n }, COALESCE_MS);\n });\n return {\n onChange,\n /** Subscribe (iOS delivers anything queued since launch), then make sure delivery is on. */\n start(): Promise<void> {\n if (!started) {\n started = (async () => {\n const source = await deps.loadSource();\n if (!source) return;\n source.watch(onChange);\n await deps.store.ensureBackgroundDelivery();\n })().catch(() => undefined);\n }\n return started;\n },\n };\n}\n\nexport const healthBackgroundSync = createBackgroundSync({\n store: healthSyncStore,\n loadSource: loadPlatformBackground,\n // A background launch reads 'background' (or 'unknown' this early): both are the background path.\n isForeground: () => AppState.currentState === 'active',\n});\n\nexport function startHealthBackgroundSync(): void {\n void healthBackgroundSync.start();\n}\n"],"names":[],"mappings":"yGAiBO,MAAM,WAAc,GAAA;AACpB,SAAS,qBAAqB,IAIlC,EAAA;AACD,EAAA,IAAI,KAA8C,GAAA,IAAA;AAClD,EAAA,IAAI,UAA6B,EAAC;AAClC,EAAA,IAAI,OAAgC,GAAA,IAAA;AAEpC,EAAA,MAAM,QAAW,GAAA,MAAqB,IAAI,OAAA,CAAQ,CAAW,OAAA,KAAA;AAC3D,IAAA,OAAA,CAAQ,KAAK,OAAO,CAAA;AACpB,IAAI,IAAA,KAAA,eAAoB,KAAK,CAAA;AAC7B,IAAA,KAAA,GAAQ,WAAW,MAAM;AACvB,MAAQ,KAAA,GAAA,IAAA;AACR,MAAA,MAAM,IAAO,GAAA,OAAA;AACb,MAAA,OAAA,GAAU,EAAC;AACX,MAAM,MAAA,GAAA,GAAM,IAAK,CAAA,YAAA,EAAiB,GAAA,IAAA,CAAK,MAAM,SAAU,EAAA,GAAI,IAAK,CAAA,KAAA,CAAM,gBAAiB,EAAA;AACvF,MAAA,KAAK,GAAI,CAAA,KAAA,CAAM,MAAM,MAAS,CAAE,CAAA,IAAA,CAAK,MAAM,IAAA,CAAK,OAAQ,CAAA,CAAA,CAAA,KAAK,CAAE,EAAC,CAAC,CAAA;AAAA,OAChE,WAAW,CAAA;AAAA,GACf,CAAA;AACD,EAAO,OAAA;AAAA,IACL,QAAA;AAAA;AAAA,IAEA,KAAuB,GAAA;AACrB,MAAA,IAAI,CAAC,OAAS,EAAA;AACZ,QAAA,OAAA,GAAA,CAAW,YAAY;AACrB,UAAM,MAAA,MAAA,GAAS,MAAM,IAAA,CAAK,UAAW,EAAA;AACrC,UAAA,IAAI,CAAC,MAAQ,EAAA;AACb,UAAA,MAAA,CAAO,MAAM,QAAQ,CAAA;AACrB,UAAM,MAAA,IAAA,CAAK,MAAM,wBAAyB,EAAA;AAAA,SACzC,GAAA,CAAE,KAAM,CAAA,MAAM,MAAS,CAAA;AAAA;AAE5B,MAAO,OAAA,OAAA;AAAA;AACT,GACF;AACF;AACO,MAAM,uBAAuB,oBAAqB,CAAA;AAAA,EACvD,KAAO,EAAA,eAAA;AAAA,EACP,UAAY,EAAA,sBAAA;AAAA;AAAA,EAEZ,YAAA,EAAc,MAAM,QAAA,CAAS,YAAiB,KAAA;AAChD,CAAC;AACM,SAAS,yBAAkC,GAAA;AAChD,EAAA,KAAK,qBAAqB,KAAM,EAAA;AAClC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import*as BackgroundTask from'expo-background-task';import*as TaskManager from'expo-task-manager';import {canReadInBackground}from'./healthConnectReader.js';const HEALTH_CONNECT_TASK = "yantra-health-connect-sync";
|
|
2
|
+
const INTERVAL_MINUTES = 60;
|
|
3
|
+
let onWake = null;
|
|
4
|
+
TaskManager.defineTask(HEALTH_CONNECT_TASK, async () => {
|
|
5
|
+
try {
|
|
6
|
+
if (await canReadInBackground()) await (onWake == null ? void 0 : onWake());
|
|
7
|
+
else await BackgroundTask.unregisterTaskAsync(HEALTH_CONNECT_TASK);
|
|
8
|
+
return BackgroundTask.BackgroundTaskResult.Success;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return BackgroundTask.BackgroundTaskResult.Failed;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
const healthConnectBackground = {
|
|
14
|
+
async enable() {
|
|
15
|
+
if (await canReadInBackground()) {
|
|
16
|
+
await BackgroundTask.registerTaskAsync(HEALTH_CONNECT_TASK, {
|
|
17
|
+
minimumInterval: INTERVAL_MINUTES
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
await BackgroundTask.unregisterTaskAsync(HEALTH_CONNECT_TASK);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
async disable() {
|
|
24
|
+
await BackgroundTask.unregisterTaskAsync(HEALTH_CONNECT_TASK);
|
|
25
|
+
},
|
|
26
|
+
watch(onChange) {
|
|
27
|
+
onWake = onChange;
|
|
28
|
+
return () => {
|
|
29
|
+
if (onWake === onChange) onWake = null;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};export{HEALTH_CONNECT_TASK,INTERVAL_MINUTES,healthConnectBackground};//# sourceMappingURL=healthConnectBackground.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"healthConnectBackground.js","sources":["../../../src/features/health/healthConnectBackground.ts"],"sourcesContent":["/**\n * Health Connect while Yantra is closed. Unlike Apple Health it cannot wake\n * an app when new data arrives, so an hourly job (expo-background-task, run by\n * Android's WorkManager when there is a network) syncs instead. Health Connect\n * only lets it read with the READ_HEALTH_DATA_IN_BACKGROUND permission, asked\n * with the data types (healthConnectReader.ts): without it the job is not\n * scheduled and Yantra syncs each time it is opened, as before.\n *\n * WorkManager skips a run while the app is open, and waits for the sync to\n * finish before scheduling the next.\n *\n * Loaded with a dynamic import on Android only (healthSync.ts): an older build\n * has neither native module, and importing either would throw.\n */\nimport * as BackgroundTask from 'expo-background-task';\nimport * as TaskManager from 'expo-task-manager';\nimport { canReadInBackground } from './healthConnectReader';\nimport type { BackgroundSource } from './healthSyncStore';\n\nexport const HEALTH_CONNECT_TASK = 'yantra-health-connect-sync';\n\n/**\n * Minutes between runs. Android's floor is 15 and it stretches the gap when the\n * phone is idle or the battery low. Hourly matches Apple Health, which\n * delivers steps at most hourly.\n */\nexport const INTERVAL_MINUTES = 60;\n\nlet onWake: (() => Promise<void>) | null = null;\n\n// Defined while this module loads, before TaskManager can hand over the run\n// that launched the app: a run it finds no task for is unregistered for good.\nTaskManager.defineTask(HEALTH_CONNECT_TASK, async () => {\n try {\n if (await canReadInBackground()) await onWake?.();\n // Turned off in Health Connect since: stop waking up for nothing.\n else await BackgroundTask.unregisterTaskAsync(HEALTH_CONNECT_TASK);\n return BackgroundTask.BackgroundTaskResult.Success;\n } catch {\n return BackgroundTask.BackgroundTaskResult.Failed;\n }\n});\n\nexport const healthConnectBackground: BackgroundSource = {\n async enable() {\n // Both are no-ops when already in that state.\n if (await canReadInBackground()) {\n await BackgroundTask.registerTaskAsync(HEALTH_CONNECT_TASK, { minimumInterval: INTERVAL_MINUTES });\n } else {\n await BackgroundTask.unregisterTaskAsync(HEALTH_CONNECT_TASK);\n }\n },\n async disable() {\n await BackgroundTask.unregisterTaskAsync(HEALTH_CONNECT_TASK);\n },\n watch(onChange) {\n onWake = onChange;\n return () => {\n if (onWake === onChange) onWake = null;\n };\n },\n};\n"],"names":[],"mappings":"6JAkBO,MAAM,mBAAsB,GAAA;AAO5B,MAAM,gBAAmB,GAAA;AAChC,IAAI,MAAuC,GAAA,IAAA;AAI3C,WAAY,CAAA,UAAA,CAAW,qBAAqB,YAAY;AACtD,EAAI,IAAA;AACF,IAAI,IAAA,MAAM,mBAAoB,EAAA,EAAS,OAAA,MAAA,IAAA,IAAA,GAAA,KAAA,CAAA,GAAA,MAAA,EAAA,CAAA;AAAA,SAElC,MAAM,cAAe,CAAA,mBAAA,CAAoB,mBAAmB,CAAA;AACjE,IAAA,OAAO,eAAe,oBAAqB,CAAA,OAAA;AAAA,GACrC,CAAA,OAAA,CAAA,EAAA;AACN,IAAA,OAAO,eAAe,oBAAqB,CAAA,MAAA;AAAA;AAE/C,CAAC,CAAA;AACM,MAAM,uBAA4C,GAAA;AAAA,EACvD,MAAM,MAAS,GAAA;AAEb,IAAI,IAAA,MAAM,qBAAuB,EAAA;AAC/B,MAAM,MAAA,cAAA,CAAe,kBAAkB,mBAAqB,EAAA;AAAA,QAC1D,eAAiB,EAAA;AAAA,OAClB,CAAA;AAAA,KACI,MAAA;AACL,MAAM,MAAA,cAAA,CAAe,oBAAoB,mBAAmB,CAAA;AAAA;AAC9D,GACF;AAAA,EACA,MAAM,OAAU,GAAA;AACd,IAAM,MAAA,cAAA,CAAe,oBAAoB,mBAAmB,CAAA;AAAA,GAC9D;AAAA,EACA,MAAM,QAAU,EAAA;AACd,IAAS,MAAA,GAAA,QAAA;AACT,IAAA,OAAO,MAAM;AACX,MAAI,IAAA,MAAA,KAAW,UAAmB,MAAA,GAAA,IAAA;AAAA,KACpC;AAAA;AAEJ"}
|
|
@@ -40,6 +40,10 @@ const PERMISSIONS = [...new Set(Object.values(RECORD_TYPES))].map((recordType) =
|
|
|
40
40
|
accessType: "read",
|
|
41
41
|
recordType
|
|
42
42
|
}));
|
|
43
|
+
const BACKGROUND_READ = {
|
|
44
|
+
accessType: "read",
|
|
45
|
+
recordType: "BackgroundAccessPermission"
|
|
46
|
+
};
|
|
43
47
|
const PAGE_SIZE = 5e3;
|
|
44
48
|
const MAX_PAGES = 200;
|
|
45
49
|
let initPromise = null;
|
|
@@ -56,6 +60,15 @@ function typesFromGranted(granted) {
|
|
|
56
60
|
const read = new Set(granted.filter((p) => p.accessType === "read").map((p) => p.recordType));
|
|
57
61
|
return TYPES.filter((t) => read.has(RECORD_TYPES[t]));
|
|
58
62
|
}
|
|
63
|
+
async function canReadInBackground() {
|
|
64
|
+
try {
|
|
65
|
+
await ensureInitialized();
|
|
66
|
+
const granted = await getGrantedPermissions();
|
|
67
|
+
return granted.some((p) => p.accessType === "read" && p.recordType === BACKGROUND_READ.recordType);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
59
72
|
async function readAll(recordType, start, end) {
|
|
60
73
|
const out = [];
|
|
61
74
|
let pageToken;
|
|
@@ -113,9 +126,10 @@ function createHealthConnectReader() {
|
|
|
113
126
|
},
|
|
114
127
|
async requestAccess() {
|
|
115
128
|
await ensureInitialized();
|
|
116
|
-
const granted = await requestPermission(PERMISSIONS);
|
|
129
|
+
const granted = await requestPermission([...PERMISSIONS, BACKGROUND_READ]);
|
|
117
130
|
return typesFromGranted(granted);
|
|
118
131
|
},
|
|
132
|
+
canReadInBackground,
|
|
119
133
|
async grantedTypes() {
|
|
120
134
|
await ensureInitialized();
|
|
121
135
|
const granted = await getGrantedPermissions();
|
|
@@ -246,4 +260,4 @@ function createHealthConnectReader() {
|
|
|
246
260
|
}
|
|
247
261
|
}
|
|
248
262
|
};
|
|
249
|
-
}export{createHealthConnectReader};//# sourceMappingURL=healthConnectReader.js.map
|
|
263
|
+
}export{canReadInBackground,createHealthConnectReader};//# sourceMappingURL=healthConnectReader.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthConnectReader.js","sources":["../../../src/features/health/healthConnectReader.ts"],"sourcesContent":["/**\n * Health Connect reader, Android only. Yantra writes only readings from\n * devices paired in the app (healthConnectWriter.ts); those are skipped here,\n * the backend already has them as source `device`.\n *\n * - Daily totals use Health Connect's aggregation per local day\n * (aggregateGroupByPeriod, DAYS), which de-duplicates phone + watch. Steps\n * per quarter-hour, for the 15-minute buckets, are the same aggregation per\n * 15 minutes (aggregateGroupByDuration), so two apps counting the same walk\n * are not added together.\n * - Records are paged (`pageToken`) so a long backfill never truncates.\n * - Types the person did not allow are skipped and reported, not retried\n * into a SecurityException on every sync.\n * - Without the READ_HEALTH_DATA_HISTORY permission Health Connect only\n * returns the 30 days before the first grant, which is exactly the\n * backfill, so the app does not ask for history access.\n */\nimport {\n aggregateGroupByDuration,\n aggregateGroupByPeriod,\n aggregateRecord,\n getGrantedPermissions,\n getSdkStatus,\n initialize,\n openHealthConnectSettings,\n readRecords,\n requestPermission,\n revokeAllPermissions,\n SdkAvailabilityStatus,\n type Permission,\n type ReadRecordsOptions,\n type RecordResult,\n type RecordType,\n} from 'react-native-health-connect';\nimport {\n hcBloodPressure,\n hcDailyTotals,\n hcGlucose,\n hcHeartRate,\n hcNotOwn,\n hcRecorded,\n hcSleepSegments,\n hcStepSpans,\n hcValues,\n hcWorkout,\n} from './healthConnectMapping';\nimport { ownAppId } from './ownSource';\nimport { emptyRawHealthData } from './model';\nimport type { HealthAvailability, HealthDataType, HealthReader, ReadResult } from './reader';\nimport { INTRADAY_BUCKET_MINUTES } from './types';\n\nimport { HEALTH_CONNECT_PACKAGE } from './healthConnectLinks';\n\n/**\n * What is read on Android. Health Connect has no record for BMI, waist, time\n * in daylight or (in react-native-health-connect 4.0.0) mindfulness, so those\n * are never read here. Not read on purpose:\n * - Skin temperature: SkinTemperatureRecord is a series of deltas against an\n * optional baseline, not the absolute overnight temperature the coach\n * compares (Apple Health's sleeping wrist temperature).\n * - Resting energy: Health Connect keeps a basal metabolic RATE (kcal/day,\n * BasalMetabolicRateRecord), not the energy burned at rest that day.\n */\nconst RECORD_TYPES: Partial<Record<HealthDataType, RecordType>> = {\n steps: 'Steps',\n distance: 'Distance',\n activeCalories: 'ActiveCaloriesBurned',\n exercise: 'ExerciseSession',\n workouts: 'ExerciseSession',\n elevation: 'ElevationGained',\n heartRate: 'HeartRate',\n restingHr: 'RestingHeartRate',\n hrv: 'HeartRateVariabilityRmssd',\n sleep: 'SleepSession',\n weight: 'Weight',\n vo2Max: 'Vo2Max',\n spo2: 'OxygenSaturation',\n respiratoryRate: 'RespiratoryRate',\n bodyFat: 'BodyFat',\n leanMass: 'LeanBodyMass',\n bodyTemp: 'BodyTemperature',\n bloodPressure: 'BloodPressure',\n bloodGlucose: 'BloodGlucose',\n};\n\nconst TYPES = Object.keys(RECORD_TYPES) as HealthDataType[];\n\nconst PERMISSIONS: Permission[] = [...new Set(Object.values(RECORD_TYPES) as RecordType[])].map((recordType) => ({\n accessType: 'read',\n recordType,\n}));\n\nconst PAGE_SIZE = 5000;\n/** A runaway pager (provider bug) stops here instead of looping forever. */\nconst MAX_PAGES = 200;\n\nlet initPromise: Promise<boolean> | null = null;\nfunction ensureInitialized(): Promise<boolean> {\n if (!initPromise) {\n initPromise = initialize().catch((err) => {\n initPromise = null;\n throw err;\n });\n }\n return initPromise;\n}\n\nfunction typesFromGranted(granted: ReadonlyArray<{ accessType?: string; recordType?: string }>): HealthDataType[] {\n const read = new Set(granted.filter((p) => p.accessType === 'read').map((p) => p.recordType));\n return TYPES.filter((t) => read.has(RECORD_TYPES[t]));\n}\n\nasync function readAll<T extends RecordType>(recordType: T, start: number, end: number): Promise<RecordResult<T>[]> {\n const out: RecordResult<T>[] = [];\n let pageToken: string | undefined;\n for (let page = 0; page < MAX_PAGES; page++) {\n const options: ReadRecordsOptions = {\n timeRangeFilter: {\n operator: 'between',\n startTime: new Date(start).toISOString(),\n endTime: new Date(end).toISOString(),\n },\n ascendingOrder: true,\n pageSize: PAGE_SIZE,\n ...(pageToken ? { pageToken } : {}),\n };\n const res = await readRecords(recordType, options);\n out.push(...res.records);\n if (!res.pageToken || res.pageToken === pageToken) break;\n pageToken = res.pageToken;\n }\n return out;\n}\n\nexport function createHealthConnectReader(): HealthReader {\n return {\n source: 'health-connect',\n storeName: 'Health Connect',\n types: TYPES,\n\n async availability(): Promise<HealthAvailability> {\n let status: number;\n try {\n status = await getSdkStatus(HEALTH_CONNECT_PACKAGE);\n } catch {\n return { status: 'unsupported', reason: 'Health Connect is not available in this build of the app.' };\n }\n if (status === SdkAvailabilityStatus.SDK_UNAVAILABLE) return { status: 'not-installed' };\n if (status === SdkAvailabilityStatus.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED)\n return { status: 'needs-update' };\n try {\n await ensureInitialized();\n } catch {\n return { status: 'unsupported', reason: 'Health Connect could not be started on this phone.' };\n }\n return { status: 'available' };\n },\n\n async requestAccess() {\n await ensureInitialized();\n const granted = await requestPermission(PERMISSIONS);\n return typesFromGranted(granted as Array<{ accessType?: string; recordType?: string }>);\n },\n\n async grantedTypes() {\n await ensureInitialized();\n const granted = await getGrantedPermissions();\n return typesFromGranted(granted as Array<{ accessType?: string; recordType?: string }>);\n },\n\n async read(range, clock): Promise<ReadResult> {\n await ensureInitialized();\n const granted = new Set(\n typesFromGranted(\n (await getGrantedPermissions()) as Array<{ accessType?: string; recordType?: string }>,\n ),\n );\n const unreadable = new Set<HealthDataType>();\n const raw = emptyRawHealthData('rmssd');\n const own = ownAppId('android');\n const filter = {\n operator: 'between' as const,\n startTime: new Date(range.start).toISOString(),\n endTime: new Date(range.end).toISOString(),\n };\n const slicer = { period: 'DAYS' as const, length: 1 };\n\n async function attempt<T>(type: HealthDataType, run: () => Promise<T>): Promise<T | undefined> {\n if (!granted.has(type)) {\n unreadable.add(type);\n return undefined;\n }\n try {\n return await run();\n } catch {\n unreadable.add(type);\n return undefined;\n }\n }\n\n const [steps, activeCalories, distance, exercise, quarterSteps] = await Promise.all([\n attempt('steps', () =>\n aggregateGroupByPeriod({ recordType: 'Steps', timeRangeFilter: filter, timeRangeSlicer: slicer }),\n ),\n attempt('activeCalories', () =>\n aggregateGroupByPeriod({\n recordType: 'ActiveCaloriesBurned',\n timeRangeFilter: filter,\n timeRangeSlicer: slicer,\n }),\n ),\n attempt('distance', () =>\n aggregateGroupByPeriod({\n recordType: 'Distance',\n timeRangeFilter: filter,\n timeRangeSlicer: slicer,\n }),\n ),\n attempt('exercise', () =>\n aggregateGroupByPeriod({\n recordType: 'ExerciseSession',\n timeRangeFilter: filter,\n timeRangeSlicer: slicer,\n }),\n ),\n attempt('steps', () =>\n aggregateGroupByDuration({\n recordType: 'Steps',\n timeRangeFilter: filter,\n timeRangeSlicer: { duration: 'MINUTES', length: INTRADAY_BUCKET_MINUTES },\n }),\n ),\n ]);\n raw.dailyTotals = hcDailyTotals({ steps, activeCalories, distance, exercise }, clock);\n raw.stepSpans = hcStepSpans(quarterSteps ?? []);\n\n const [fat, lean, temp, pressure, glucose] = await Promise.all([\n attempt('bodyFat', () => readAll('BodyFat', range.start, range.end)),\n attempt('leanMass', () => readAll('LeanBodyMass', range.start, range.end)),\n attempt('bodyTemp', () => readAll('BodyTemperature', range.start, range.end)),\n attempt('bloodPressure', () => readAll('BloodPressure', range.start, range.end)),\n attempt('bloodGlucose', () => readAll('BloodGlucose', range.start, range.end)),\n ]);\n raw.bodyFatPct = hcValues(hcNotOwn(fat ?? [], own), (r) => r.percentage);\n raw.leanMassKg = hcValues(hcNotOwn(lean ?? [], own), (r) => r.mass?.inKilograms);\n raw.bodyTempC = hcValues(hcNotOwn(temp ?? [], own), (r) => r.temperature?.inCelsius);\n raw.bloodPressure = hcBloodPressure(hcNotOwn(pressure ?? [], own));\n raw.glucose = hcGlucose(hcNotOwn(glucose ?? [], own));\n\n const [hr, rhr, hrv, sleep, weight, vo2, spo2, resp, sessions] = await Promise.all([\n attempt('heartRate', () => readAll('HeartRate', range.start, range.end)),\n attempt('restingHr', () => readAll('RestingHeartRate', range.start, range.end)),\n attempt('hrv', () => readAll('HeartRateVariabilityRmssd', range.start, range.end)),\n attempt('sleep', () => readAll('SleepSession', range.start, range.end)),\n attempt('weight', () => readAll('Weight', range.start, range.end)),\n attempt('vo2Max', () => readAll('Vo2Max', range.start, range.end)),\n attempt('spo2', () => readAll('OxygenSaturation', range.start, range.end)),\n attempt('respiratoryRate', () => readAll('RespiratoryRate', range.start, range.end)),\n attempt('workouts', () => readAll('ExerciseSession', range.start, range.end)),\n ]);\n raw.heartRate = hcHeartRate(hcNotOwn(hr ?? [], own));\n raw.restingHr = hcValues(rhr ?? [], (r) => r.beatsPerMinute);\n raw.hrv = hcValues(hrv ?? [], (r) => r.heartRateVariabilityMillis);\n raw.sleep = hcSleepSegments(sleep ?? []);\n raw.weightKg = hcValues(hcNotOwn(weight ?? [], own), (r) => r.weight?.inKilograms);\n raw.vo2Max = hcValues(vo2 ?? [], (r) => r.vo2MillilitersPerMinuteKilogram);\n raw.spo2 = hcValues(hcNotOwn(spo2 ?? [], own), (r) => r.percentage);\n raw.respiratoryRate = hcValues(resp ?? [], (r) => r.rate);\n\n for (const s of sessions ?? []) {\n const window = { operator: 'between' as const, startTime: s.startTime, endTime: s.endTime };\n const [cal, dist, elevation] = await Promise.all([\n granted.has('activeCalories')\n ? aggregateRecord({ recordType: 'ActiveCaloriesBurned', timeRangeFilter: window }).catch(\n () => undefined,\n )\n : undefined,\n granted.has('distance')\n ? aggregateRecord({ recordType: 'Distance', timeRangeFilter: window }).catch(() => undefined)\n : undefined,\n granted.has('elevation')\n ? aggregateRecord({ recordType: 'ElevationGained', timeRangeFilter: window }).catch(\n () => undefined,\n )\n : undefined,\n ]);\n const workout = hcWorkout(s, {\n calories: cal?.ACTIVE_CALORIES_TOTAL?.inKilocalories,\n distanceMeters: dist?.DISTANCE?.inMeters,\n elevationMeters: hcRecorded(elevation, (r) => r.ELEVATION_GAINED_TOTAL?.inMeters),\n });\n if (workout) raw.workouts.push(workout);\n }\n\n return { raw, unreadable: [...unreadable] };\n },\n\n async openSettings() {\n openHealthConnectSettings();\n },\n\n async revoke() {\n try {\n await ensureInitialized();\n await revokeAllPermissions();\n } catch {\n // Stopping sync does not depend on it; the person can still\n // remove access in Health Connect.\n }\n },\n };\n}\n"],"names":["_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"healthConnectReader.js","sources":["../../../src/features/health/healthConnectReader.ts"],"sourcesContent":["/**\n * Health Connect reader, Android only. Yantra writes only readings from\n * devices paired in the app (healthConnectWriter.ts); those are skipped here,\n * the backend already has them as source `device`.\n *\n * - Daily totals use Health Connect's aggregation per local day\n * (aggregateGroupByPeriod, DAYS), which de-duplicates phone + watch. Steps\n * per quarter-hour, for the 15-minute buckets, are the same aggregation per\n * 15 minutes (aggregateGroupByDuration), so two apps counting the same walk\n * are not added together.\n * - Records are paged (`pageToken`) so a long backfill never truncates.\n * - Types the person did not allow are skipped and reported, not retried\n * into a SecurityException on every sync.\n * - Without the READ_HEALTH_DATA_HISTORY permission Health Connect only\n * returns the 30 days before the first grant, which is exactly the\n * backfill, so the app does not ask for history access.\n * - READ_HEALTH_DATA_IN_BACKGROUND is asked with the data types: without it\n * the hourly job that syncs while Yantra is closed (healthConnectBackground.ts)\n * cannot read.\n */\nimport {\n aggregateGroupByDuration,\n aggregateGroupByPeriod,\n aggregateRecord,\n getGrantedPermissions,\n getSdkStatus,\n initialize,\n openHealthConnectSettings,\n readRecords,\n requestPermission,\n revokeAllPermissions,\n SdkAvailabilityStatus,\n type BackgroundAccessPermission,\n type Permission,\n type ReadRecordsOptions,\n type RecordResult,\n type RecordType,\n} from 'react-native-health-connect';\nimport {\n hcBloodPressure,\n hcDailyTotals,\n hcGlucose,\n hcHeartRate,\n hcNotOwn,\n hcRecorded,\n hcSleepSegments,\n hcStepSpans,\n hcValues,\n hcWorkout,\n} from './healthConnectMapping';\nimport { ownAppId } from './ownSource';\nimport { emptyRawHealthData } from './model';\nimport type { HealthAvailability, HealthDataType, HealthReader, ReadResult } from './reader';\nimport { INTRADAY_BUCKET_MINUTES } from './types';\n\nimport { HEALTH_CONNECT_PACKAGE } from './healthConnectLinks';\n\n/**\n * What is read on Android. Health Connect has no record for BMI, waist, time\n * in daylight or (in react-native-health-connect 4.0.0) mindfulness, so those\n * are never read here. Not read on purpose:\n * - Skin temperature: SkinTemperatureRecord is a series of deltas against an\n * optional baseline, not the absolute overnight temperature the coach\n * compares (Apple Health's sleeping wrist temperature).\n * - Resting energy: Health Connect keeps a basal metabolic RATE (kcal/day,\n * BasalMetabolicRateRecord), not the energy burned at rest that day.\n */\nconst RECORD_TYPES: Partial<Record<HealthDataType, RecordType>> = {\n steps: 'Steps',\n distance: 'Distance',\n activeCalories: 'ActiveCaloriesBurned',\n exercise: 'ExerciseSession',\n workouts: 'ExerciseSession',\n elevation: 'ElevationGained',\n heartRate: 'HeartRate',\n restingHr: 'RestingHeartRate',\n hrv: 'HeartRateVariabilityRmssd',\n sleep: 'SleepSession',\n weight: 'Weight',\n vo2Max: 'Vo2Max',\n spo2: 'OxygenSaturation',\n respiratoryRate: 'RespiratoryRate',\n bodyFat: 'BodyFat',\n leanMass: 'LeanBodyMass',\n bodyTemp: 'BodyTemperature',\n bloodPressure: 'BloodPressure',\n bloodGlucose: 'BloodGlucose',\n};\n\nconst TYPES = Object.keys(RECORD_TYPES) as HealthDataType[];\n\nconst PERMISSIONS: Permission[] = [...new Set(Object.values(RECORD_TYPES) as RecordType[])].map((recordType) => ({\n accessType: 'read',\n recordType,\n}));\n\n/** Health Connect shows it as an extra page after the data types. */\nconst BACKGROUND_READ: BackgroundAccessPermission = { accessType: 'read', recordType: 'BackgroundAccessPermission' };\n\nconst PAGE_SIZE = 5000;\n/** A runaway pager (provider bug) stops here instead of looping forever. */\nconst MAX_PAGES = 200;\n\nlet initPromise: Promise<boolean> | null = null;\nfunction ensureInitialized(): Promise<boolean> {\n if (!initPromise) {\n initPromise = initialize().catch((err) => {\n initPromise = null;\n throw err;\n });\n }\n return initPromise;\n}\n\nfunction typesFromGranted(granted: ReadonlyArray<{ accessType?: string; recordType?: string }>): HealthDataType[] {\n const read = new Set(granted.filter((p) => p.accessType === 'read').map((p) => p.recordType));\n return TYPES.filter((t) => read.has(RECORD_TYPES[t]));\n}\n\n/** Health Connect lets Yantra read while it is closed. False whenever it cannot say. */\nexport async function canReadInBackground(): Promise<boolean> {\n try {\n await ensureInitialized();\n const granted = (await getGrantedPermissions()) as Array<{ accessType?: string; recordType?: string }>;\n return granted.some((p) => p.accessType === 'read' && p.recordType === BACKGROUND_READ.recordType);\n } catch {\n return false;\n }\n}\n\nasync function readAll<T extends RecordType>(recordType: T, start: number, end: number): Promise<RecordResult<T>[]> {\n const out: RecordResult<T>[] = [];\n let pageToken: string | undefined;\n for (let page = 0; page < MAX_PAGES; page++) {\n const options: ReadRecordsOptions = {\n timeRangeFilter: {\n operator: 'between',\n startTime: new Date(start).toISOString(),\n endTime: new Date(end).toISOString(),\n },\n ascendingOrder: true,\n pageSize: PAGE_SIZE,\n ...(pageToken ? { pageToken } : {}),\n };\n const res = await readRecords(recordType, options);\n out.push(...res.records);\n if (!res.pageToken || res.pageToken === pageToken) break;\n pageToken = res.pageToken;\n }\n return out;\n}\n\nexport function createHealthConnectReader(): HealthReader {\n return {\n source: 'health-connect',\n storeName: 'Health Connect',\n types: TYPES,\n\n async availability(): Promise<HealthAvailability> {\n let status: number;\n try {\n status = await getSdkStatus(HEALTH_CONNECT_PACKAGE);\n } catch {\n return { status: 'unsupported', reason: 'Health Connect is not available in this build of the app.' };\n }\n if (status === SdkAvailabilityStatus.SDK_UNAVAILABLE) return { status: 'not-installed' };\n if (status === SdkAvailabilityStatus.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED)\n return { status: 'needs-update' };\n try {\n await ensureInitialized();\n } catch {\n return { status: 'unsupported', reason: 'Health Connect could not be started on this phone.' };\n }\n return { status: 'available' };\n },\n\n async requestAccess() {\n await ensureInitialized();\n const granted = await requestPermission([...PERMISSIONS, BACKGROUND_READ]);\n return typesFromGranted(granted as Array<{ accessType?: string; recordType?: string }>);\n },\n\n canReadInBackground,\n\n async grantedTypes() {\n await ensureInitialized();\n const granted = await getGrantedPermissions();\n return typesFromGranted(granted as Array<{ accessType?: string; recordType?: string }>);\n },\n\n async read(range, clock): Promise<ReadResult> {\n await ensureInitialized();\n const granted = new Set(\n typesFromGranted(\n (await getGrantedPermissions()) as Array<{ accessType?: string; recordType?: string }>,\n ),\n );\n const unreadable = new Set<HealthDataType>();\n const raw = emptyRawHealthData('rmssd');\n const own = ownAppId('android');\n const filter = {\n operator: 'between' as const,\n startTime: new Date(range.start).toISOString(),\n endTime: new Date(range.end).toISOString(),\n };\n const slicer = { period: 'DAYS' as const, length: 1 };\n\n async function attempt<T>(type: HealthDataType, run: () => Promise<T>): Promise<T | undefined> {\n if (!granted.has(type)) {\n unreadable.add(type);\n return undefined;\n }\n try {\n return await run();\n } catch {\n unreadable.add(type);\n return undefined;\n }\n }\n\n const [steps, activeCalories, distance, exercise, quarterSteps] = await Promise.all([\n attempt('steps', () =>\n aggregateGroupByPeriod({ recordType: 'Steps', timeRangeFilter: filter, timeRangeSlicer: slicer }),\n ),\n attempt('activeCalories', () =>\n aggregateGroupByPeriod({\n recordType: 'ActiveCaloriesBurned',\n timeRangeFilter: filter,\n timeRangeSlicer: slicer,\n }),\n ),\n attempt('distance', () =>\n aggregateGroupByPeriod({\n recordType: 'Distance',\n timeRangeFilter: filter,\n timeRangeSlicer: slicer,\n }),\n ),\n attempt('exercise', () =>\n aggregateGroupByPeriod({\n recordType: 'ExerciseSession',\n timeRangeFilter: filter,\n timeRangeSlicer: slicer,\n }),\n ),\n attempt('steps', () =>\n aggregateGroupByDuration({\n recordType: 'Steps',\n timeRangeFilter: filter,\n timeRangeSlicer: { duration: 'MINUTES', length: INTRADAY_BUCKET_MINUTES },\n }),\n ),\n ]);\n raw.dailyTotals = hcDailyTotals({ steps, activeCalories, distance, exercise }, clock);\n raw.stepSpans = hcStepSpans(quarterSteps ?? []);\n\n const [fat, lean, temp, pressure, glucose] = await Promise.all([\n attempt('bodyFat', () => readAll('BodyFat', range.start, range.end)),\n attempt('leanMass', () => readAll('LeanBodyMass', range.start, range.end)),\n attempt('bodyTemp', () => readAll('BodyTemperature', range.start, range.end)),\n attempt('bloodPressure', () => readAll('BloodPressure', range.start, range.end)),\n attempt('bloodGlucose', () => readAll('BloodGlucose', range.start, range.end)),\n ]);\n raw.bodyFatPct = hcValues(hcNotOwn(fat ?? [], own), (r) => r.percentage);\n raw.leanMassKg = hcValues(hcNotOwn(lean ?? [], own), (r) => r.mass?.inKilograms);\n raw.bodyTempC = hcValues(hcNotOwn(temp ?? [], own), (r) => r.temperature?.inCelsius);\n raw.bloodPressure = hcBloodPressure(hcNotOwn(pressure ?? [], own));\n raw.glucose = hcGlucose(hcNotOwn(glucose ?? [], own));\n\n const [hr, rhr, hrv, sleep, weight, vo2, spo2, resp, sessions] = await Promise.all([\n attempt('heartRate', () => readAll('HeartRate', range.start, range.end)),\n attempt('restingHr', () => readAll('RestingHeartRate', range.start, range.end)),\n attempt('hrv', () => readAll('HeartRateVariabilityRmssd', range.start, range.end)),\n attempt('sleep', () => readAll('SleepSession', range.start, range.end)),\n attempt('weight', () => readAll('Weight', range.start, range.end)),\n attempt('vo2Max', () => readAll('Vo2Max', range.start, range.end)),\n attempt('spo2', () => readAll('OxygenSaturation', range.start, range.end)),\n attempt('respiratoryRate', () => readAll('RespiratoryRate', range.start, range.end)),\n attempt('workouts', () => readAll('ExerciseSession', range.start, range.end)),\n ]);\n raw.heartRate = hcHeartRate(hcNotOwn(hr ?? [], own));\n raw.restingHr = hcValues(rhr ?? [], (r) => r.beatsPerMinute);\n raw.hrv = hcValues(hrv ?? [], (r) => r.heartRateVariabilityMillis);\n raw.sleep = hcSleepSegments(sleep ?? []);\n raw.weightKg = hcValues(hcNotOwn(weight ?? [], own), (r) => r.weight?.inKilograms);\n raw.vo2Max = hcValues(vo2 ?? [], (r) => r.vo2MillilitersPerMinuteKilogram);\n raw.spo2 = hcValues(hcNotOwn(spo2 ?? [], own), (r) => r.percentage);\n raw.respiratoryRate = hcValues(resp ?? [], (r) => r.rate);\n\n for (const s of sessions ?? []) {\n const window = { operator: 'between' as const, startTime: s.startTime, endTime: s.endTime };\n const [cal, dist, elevation] = await Promise.all([\n granted.has('activeCalories')\n ? aggregateRecord({ recordType: 'ActiveCaloriesBurned', timeRangeFilter: window }).catch(\n () => undefined,\n )\n : undefined,\n granted.has('distance')\n ? aggregateRecord({ recordType: 'Distance', timeRangeFilter: window }).catch(() => undefined)\n : undefined,\n granted.has('elevation')\n ? aggregateRecord({ recordType: 'ElevationGained', timeRangeFilter: window }).catch(\n () => undefined,\n )\n : undefined,\n ]);\n const workout = hcWorkout(s, {\n calories: cal?.ACTIVE_CALORIES_TOTAL?.inKilocalories,\n distanceMeters: dist?.DISTANCE?.inMeters,\n elevationMeters: hcRecorded(elevation, (r) => r.ELEVATION_GAINED_TOTAL?.inMeters),\n });\n if (workout) raw.workouts.push(workout);\n }\n\n return { raw, unreadable: [...unreadable] };\n },\n\n async openSettings() {\n openHealthConnectSettings();\n },\n\n async revoke() {\n try {\n await ensureInitialized();\n await revokeAllPermissions();\n } catch {\n // Stopping sync does not depend on it; the person can still\n // remove access in Health Connect.\n }\n },\n };\n}\n"],"names":["_a"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import AsyncStorage from'@react-native-async-storage/async-storage';import {Platform}from'react-native';import {useSyncExternalStore}from'react';import {HealthSyncStore}from'./healthSyncStore.js';async function createPlatformReader() {
|
|
1
|
+
import AsyncStorage from'@react-native-async-storage/async-storage';import {Platform}from'react-native';import {useSyncExternalStore}from'react';import {readAuth0IdToken}from'../../bridge/readAuth0Session.js';import {HealthSyncStore}from'./healthSyncStore.js';import {secureSessionVault}from'./sessionVault.js';async function createPlatformReader() {
|
|
2
2
|
try {
|
|
3
3
|
if (Platform.OS === "ios") return (await import('./healthkitReader.js')).createHealthKitReader();
|
|
4
4
|
if (Platform.OS === "android") return (await import('./healthConnectReader.js')).createHealthConnectReader();
|
|
@@ -6,10 +6,38 @@ import AsyncStorage from'@react-native-async-storage/async-storage';import {Plat
|
|
|
6
6
|
}
|
|
7
7
|
return null;
|
|
8
8
|
}
|
|
9
|
+
let background = null;
|
|
10
|
+
function loadPlatformBackground() {
|
|
11
|
+
if (!background) {
|
|
12
|
+
background = (async () => {
|
|
13
|
+
try {
|
|
14
|
+
if (Platform.OS === "ios") return (await import('./healthkitBackground.js')).healthkitBackground;
|
|
15
|
+
if (Platform.OS === "android") {
|
|
16
|
+
return (await import('./healthConnectBackground.js')).healthConnectBackground;
|
|
17
|
+
}
|
|
18
|
+
} catch (e) {
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
})();
|
|
22
|
+
}
|
|
23
|
+
return background;
|
|
24
|
+
}
|
|
9
25
|
const healthSyncStore = new HealthSyncStore({
|
|
10
26
|
storage: AsyncStorage,
|
|
11
|
-
createReader: createPlatformReader
|
|
27
|
+
createReader: createPlatformReader,
|
|
28
|
+
vault: secureSessionVault,
|
|
29
|
+
background: {
|
|
30
|
+
enable: async () => {
|
|
31
|
+
var _a;
|
|
32
|
+
await ((_a = await loadPlatformBackground()) == null ? void 0 : _a.enable());
|
|
33
|
+
},
|
|
34
|
+
disable: async () => {
|
|
35
|
+
var _a;
|
|
36
|
+
await ((_a = await loadPlatformBackground()) == null ? void 0 : _a.disable());
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
signedIn: async () => Boolean(await readAuth0IdToken())
|
|
12
40
|
});
|
|
13
41
|
function useHealthSync() {
|
|
14
42
|
return useSyncExternalStore(healthSyncStore.subscribe, healthSyncStore.getSnapshot, healthSyncStore.getSnapshot);
|
|
15
|
-
}export{healthSyncStore,useHealthSync};//# sourceMappingURL=healthSync.js.map
|
|
43
|
+
}export{healthSyncStore,loadPlatformBackground,useHealthSync};//# sourceMappingURL=healthSync.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthSync.js","sources":["../../../src/features/health/healthSync.ts"],"sourcesContent":["/**\n * The app's one HealthSyncStore, wired to AsyncStorage and this platform's\n * reader.\n *\n * The readers load with a dynamic import, inside a try: both native\n * libraries create their native objects when first imported, and an app\n * binary built before they were added (an over-the-air JS update reaching an\n * older build) would crash at startup on a static import. Here it only means\n * \"not available in this build\". Rollup keeps `import()` as its own module.\n */\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport { Platform } from 'react-native';\nimport { useSyncExternalStore } from 'react';\nimport type { HealthReader } from './reader';\nimport { HealthSyncStore, type HealthSyncSnapshot } from './healthSyncStore';\n\nasync function createPlatformReader(): Promise<HealthReader | null> {\n try {\n if (Platform.OS === 'ios') return (await import('./healthkitReader')).createHealthKitReader();\n if (Platform.OS === 'android') return (await import('./healthConnectReader')).createHealthConnectReader();\n } catch {\n // This binary does not include the native module.\n }\n return null;\n}\n\nexport const healthSyncStore = new HealthSyncStore({\n storage: AsyncStorage,\n createReader: createPlatformReader,\n});\n\nexport function useHealthSync(): HealthSyncSnapshot {\n return useSyncExternalStore(healthSyncStore.subscribe, healthSyncStore.getSnapshot, healthSyncStore.getSnapshot);\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"healthSync.js","sources":["../../../src/features/health/healthSync.ts"],"sourcesContent":["/**\n * The app's one HealthSyncStore, wired to AsyncStorage and this platform's\n * reader.\n *\n * The readers load with a dynamic import, inside a try: both native\n * libraries create their native objects when first imported, and an app\n * binary built before they were added (an over-the-air JS update reaching an\n * older build) would crash at startup on a static import. Here it only means\n * \"not available in this build\". Rollup keeps `import()` as its own module.\n */\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport { Platform } from 'react-native';\nimport { useSyncExternalStore } from 'react';\nimport { readAuth0IdToken } from '../../bridge/readAuth0Session';\nimport type { HealthReader } from './reader';\nimport { HealthSyncStore, type BackgroundSource, type HealthSyncSnapshot } from './healthSyncStore';\nimport { secureSessionVault } from './sessionVault';\n\nasync function createPlatformReader(): Promise<HealthReader | null> {\n try {\n if (Platform.OS === 'ios') return (await import('./healthkitReader')).createHealthKitReader();\n if (Platform.OS === 'android') return (await import('./healthConnectReader')).createHealthConnectReader();\n } catch {\n // This binary does not include the native module.\n }\n return null;\n}\n\nlet background: Promise<BackgroundSource | null> | null = null;\n\n/**\n * Syncing while closed: Apple Health background delivery, or Health Connect's\n * hourly job. Null where there is neither (web, a build without the module).\n */\nexport function loadPlatformBackground(): Promise<BackgroundSource | null> {\n if (!background) {\n background = (async () => {\n try {\n if (Platform.OS === 'ios') return (await import('./healthkitBackground')).healthkitBackground;\n if (Platform.OS === 'android') {\n return (await import('./healthConnectBackground')).healthConnectBackground;\n }\n } catch {\n // This build has no such native module.\n }\n return null;\n })();\n }\n return background;\n}\n\nexport const healthSyncStore = new HealthSyncStore({\n storage: AsyncStorage,\n createReader: createPlatformReader,\n vault: secureSessionVault,\n background: {\n enable: async () => {\n await (await loadPlatformBackground())?.enable();\n },\n disable: async () => {\n await (await loadPlatformBackground())?.disable();\n },\n },\n signedIn: async () => Boolean(await readAuth0IdToken()),\n});\n\nexport function useHealthSync(): HealthSyncSnapshot {\n return useSyncExternalStore(healthSyncStore.subscribe, healthSyncStore.getSnapshot, healthSyncStore.getSnapshot);\n}\n"],"names":[],"mappings":"uTAiBA,eAAe,oBAAqD,GAAA;AAClE,EAAI,IAAA;AACF,IAAI,IAAA,QAAA,CAAS,OAAO,KAAO,EAAA,OAAA,CAAQ,MAAM,OAAO,sBAAmB,GAAG,qBAAsB,EAAA;AAC5F,IAAI,IAAA,QAAA,CAAS,OAAO,SAAW,EAAA,OAAA,CAAQ,MAAM,OAAO,0BAAuB,GAAG,yBAA0B,EAAA;AAAA,GAClG,CAAA,OAAA,CAAA,EAAA;AAAA;AAGR,EAAO,OAAA,IAAA;AACT;AACA,IAAI,UAAsD,GAAA,IAAA;AAMnD,SAAS,sBAA2D,GAAA;AACzE,EAAA,IAAI,CAAC,UAAY,EAAA;AACf,IAAA,UAAA,GAAA,CAAc,YAAY;AACxB,MAAI,IAAA;AACF,QAAA,IAAI,SAAS,EAAO,KAAA,KAAA,UAAe,MAAM,OAAO,0BAAuB,CAAG,EAAA,mBAAA;AAC1E,QAAI,IAAA,QAAA,CAAS,OAAO,SAAW,EAAA;AAC7B,UAAQ,OAAA,CAAA,MAAM,OAAO,8BAA2B,CAAG,EAAA,uBAAA;AAAA;AACrD,OACM,CAAA,OAAA,CAAA,EAAA;AAAA;AAGR,MAAO,OAAA,IAAA;AAAA,KACN,GAAA;AAAA;AAEL,EAAO,OAAA,UAAA;AACT;AACa,MAAA,eAAA,GAAkB,IAAI,eAAgB,CAAA;AAAA,EACjD,OAAS,EAAA,YAAA;AAAA,EACT,YAAc,EAAA,oBAAA;AAAA,EACd,KAAO,EAAA,kBAAA;AAAA,EACP,UAAY,EAAA;AAAA,IACV,QAAQ,YAAY;AArDxB,MAAA,IAAA,EAAA;AAsDM,MAAO,OAAA,CAAA,EAAA,GAAA,MAAM,sBAAuB,EAAA,KAA7B,IAAiC,GAAA,MAAA,GAAA,EAAA,CAAA,MAAA,EAAA,CAAA;AAAA,KAC1C;AAAA,IACA,SAAS,YAAY;AAxDzB,MAAA,IAAA,EAAA;AAyDM,MAAO,OAAA,CAAA,EAAA,GAAA,MAAM,sBAAuB,EAAA,KAA7B,IAAiC,GAAA,MAAA,GAAA,EAAA,CAAA,OAAA,EAAA,CAAA;AAAA;AAC1C,GACF;AAAA,EACA,QAAU,EAAA,YAAY,OAAQ,CAAA,MAAM,kBAAkB;AACxD,CAAC;AACM,SAAS,aAAoC,GAAA;AAClD,EAAA,OAAO,qBAAqB,eAAgB,CAAA,SAAA,EAAW,eAAgB,CAAA,WAAA,EAAa,gBAAgB,WAAW,CAAA;AACjH"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {createFitnessClient}from'./fitnessClient.js';import {describeSyncError,runHealthSync}from'./syncEngine.js';import {loadSyncState,saveSyncState,INITIAL_SYNC_STATE}from'./syncState.js';import {zoneClock,deviceTimeZone}from'./time.js';var __defProp = Object.defineProperty;
|
|
1
|
+
import {tokenExpiryMs}from'../canvas/sessionIdentity.js';import {createFitnessClient}from'./fitnessClient.js';import {describeSyncError,runHealthSync}from'./syncEngine.js';import {loadSyncState,saveSyncState,INITIAL_SYNC_STATE}from'./syncState.js';import {zoneClock,deviceTimeZone}from'./time.js';var __defProp = Object.defineProperty;
|
|
2
2
|
var __defProps = Object.defineProperties;
|
|
3
3
|
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
4
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
@@ -20,6 +20,8 @@ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
|
20
20
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
21
|
const FOREGROUND_THROTTLE_MS = 15 * 6e4;
|
|
22
22
|
const SESSION_WAIT_MS = 3e4;
|
|
23
|
+
const BACKGROUND_MIN_GAP_MS = 2 * 6e4;
|
|
24
|
+
const SESSION_MARGIN_MS = 5 * 6e4;
|
|
23
25
|
class HealthSyncStore {
|
|
24
26
|
constructor(deps) {
|
|
25
27
|
__publicField(this, "snapshot", {
|
|
@@ -85,16 +87,48 @@ class HealthSyncStore {
|
|
|
85
87
|
}
|
|
86
88
|
/** The root runner hands over the sign-in session (or null when it is gone). */
|
|
87
89
|
setSession(session, error) {
|
|
90
|
+
var _a;
|
|
88
91
|
this.session = session;
|
|
89
92
|
this.set({
|
|
90
93
|
sessionError: session ? null : error != null ? error : null
|
|
91
94
|
});
|
|
95
|
+
if (session) void ((_a = this.deps.vault) == null ? void 0 : _a.write(session).catch(() => void 0));
|
|
92
96
|
if (session) {
|
|
93
97
|
const waiters = this.sessionWaiters;
|
|
94
98
|
this.sessionWaiters = [];
|
|
95
99
|
waiters.forEach((w) => w(session));
|
|
96
100
|
}
|
|
97
101
|
}
|
|
102
|
+
/** Signed out, or sync turned off: no background sync may use the saved session. */
|
|
103
|
+
async forgetSavedSession() {
|
|
104
|
+
var _a;
|
|
105
|
+
await ((_a = this.deps.vault) == null ? void 0 : _a.clear().catch(() => void 0));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Turn background delivery on for someone already connected - at every
|
|
109
|
+
* launch, so people who connected before this existed get it too.
|
|
110
|
+
*/
|
|
111
|
+
async ensureBackgroundDelivery() {
|
|
112
|
+
var _a;
|
|
113
|
+
await this.load();
|
|
114
|
+
if (this.snapshot.sync.connected) await ((_a = this.deps.background) == null ? void 0 : _a.enable().catch(() => void 0));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* After anything that can change what was granted: delivery on (repeating
|
|
118
|
+
* it is harmless), and Health Connect's answer to reading while closed kept
|
|
119
|
+
* for the screen. `enable` schedules or drops Android's hourly job to
|
|
120
|
+
* match, so access given in Health Connect's own settings counts once the
|
|
121
|
+
* screen is back.
|
|
122
|
+
*/
|
|
123
|
+
async refreshBackgroundAccess(reader) {
|
|
124
|
+
var _a;
|
|
125
|
+
if (reader.canReadInBackground) {
|
|
126
|
+
this.set({
|
|
127
|
+
backgroundAccess: await reader.canReadInBackground().catch(() => false)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
await ((_a = this.deps.background) == null ? void 0 : _a.enable().catch(() => void 0));
|
|
131
|
+
}
|
|
98
132
|
waitForSession() {
|
|
99
133
|
if (this.session) return Promise.resolve(this.session);
|
|
100
134
|
return new Promise((resolve) => {
|
|
@@ -138,6 +172,7 @@ class HealthSyncStore {
|
|
|
138
172
|
granted: void 0
|
|
139
173
|
});
|
|
140
174
|
}
|
|
175
|
+
await this.refreshBackgroundAccess(reader);
|
|
141
176
|
}
|
|
142
177
|
}
|
|
143
178
|
/** Ask for access, turn sync on, run the 30-day backfill. Resolves false if not connected. */
|
|
@@ -166,12 +201,15 @@ class HealthSyncStore {
|
|
|
166
201
|
return false;
|
|
167
202
|
}
|
|
168
203
|
const now = new Date(this.now()).toISOString();
|
|
169
|
-
await this.persist(__spreadProps(__spreadValues({}, this.snapshot.sync), {
|
|
204
|
+
await this.persist(__spreadValues(__spreadProps(__spreadValues({}, this.snapshot.sync), {
|
|
170
205
|
connected: true,
|
|
171
206
|
source: reader.source,
|
|
172
207
|
connectedAt: (_a = this.snapshot.sync.connectedAt) != null ? _a : now,
|
|
173
208
|
lastError: void 0
|
|
174
|
-
})
|
|
209
|
+
}), reader.canReadInBackground ? {
|
|
210
|
+
backgroundAskedAt: now
|
|
211
|
+
} : {}));
|
|
212
|
+
void this.refreshBackgroundAccess(reader);
|
|
175
213
|
} catch (err) {
|
|
176
214
|
await this.persist(__spreadProps(__spreadValues({}, this.snapshot.sync), {
|
|
177
215
|
lastError: describeSyncError(err)
|
|
@@ -193,6 +231,7 @@ class HealthSyncStore {
|
|
|
193
231
|
* back; Apple Health access can only be removed in the Health app.
|
|
194
232
|
*/
|
|
195
233
|
async disconnect(opts) {
|
|
234
|
+
var _a;
|
|
196
235
|
await this.load();
|
|
197
236
|
const reader = await this.getReader();
|
|
198
237
|
this.set({
|
|
@@ -222,9 +261,12 @@ class HealthSyncStore {
|
|
|
222
261
|
}
|
|
223
262
|
this.generation += 1;
|
|
224
263
|
await (reader == null ? void 0 : reader.revoke());
|
|
264
|
+
await ((_a = this.deps.background) == null ? void 0 : _a.disable().catch(() => void 0));
|
|
265
|
+
await this.forgetSavedSession();
|
|
225
266
|
await this.persist(__spreadValues({}, INITIAL_SYNC_STATE));
|
|
226
267
|
this.set({
|
|
227
|
-
granted: void 0
|
|
268
|
+
granted: void 0,
|
|
269
|
+
backgroundAccess: void 0
|
|
228
270
|
});
|
|
229
271
|
return {
|
|
230
272
|
ok: true
|
|
@@ -248,6 +290,42 @@ class HealthSyncStore {
|
|
|
248
290
|
interactive: false
|
|
249
291
|
});
|
|
250
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* A background wake: iOS woke the app because the watch saved new data, or
|
|
295
|
+
* Android's hourly job ran, maybe with no screen and no sign-in. Syncs with
|
|
296
|
+
* the session the app last had, unless the phone is locked (iOS: nothing
|
|
297
|
+
* is readable then), the person signed out, the session is about to
|
|
298
|
+
* expire, or this is the same burst as the last attempt. Every "no" is
|
|
299
|
+
* silent: the next open syncs as it always did.
|
|
300
|
+
*/
|
|
301
|
+
async syncInBackground() {
|
|
302
|
+
var _a, _b, _c;
|
|
303
|
+
await this.load();
|
|
304
|
+
const {
|
|
305
|
+
sync
|
|
306
|
+
} = this.snapshot;
|
|
307
|
+
if (!sync.connected) return false;
|
|
308
|
+
const last = sync.lastAttemptAt ? Date.parse(sync.lastAttemptAt) : 0;
|
|
309
|
+
if (this.now() - last < BACKGROUND_MIN_GAP_MS) return false;
|
|
310
|
+
let saved;
|
|
311
|
+
try {
|
|
312
|
+
saved = (_b = await ((_a = this.deps.vault) == null ? void 0 : _a.read())) != null ? _b : null;
|
|
313
|
+
} catch (e) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
if (this.deps.signedIn && !await this.deps.signedIn().catch(() => false)) {
|
|
317
|
+
await this.forgetSavedSession();
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
const session = (_c = this.session) != null ? _c : saved;
|
|
321
|
+
if (!session) return false;
|
|
322
|
+
const expires = tokenExpiryMs(session.token);
|
|
323
|
+
if (expires !== null && expires - this.now() < SESSION_MARGIN_MS) return false;
|
|
324
|
+
if (!this.session) this.session = session;
|
|
325
|
+
return this.syncNow({
|
|
326
|
+
interactive: false
|
|
327
|
+
});
|
|
328
|
+
}
|
|
251
329
|
/**
|
|
252
330
|
* Run a sync now (joins one already running). `interactive` = the person
|
|
253
331
|
* tapped "Sync now": on Apple Health this re-shows the permission sheet
|
|
@@ -275,6 +353,12 @@ class HealthSyncStore {
|
|
|
275
353
|
this.set({
|
|
276
354
|
granted
|
|
277
355
|
});
|
|
356
|
+
if (reader.canReadInBackground) {
|
|
357
|
+
await this.persist(__spreadProps(__spreadValues({}, this.snapshot.sync), {
|
|
358
|
+
backgroundAskedAt: new Date(this.now()).toISOString()
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
void this.refreshBackgroundAccess(reader);
|
|
278
362
|
} catch (err) {
|
|
279
363
|
const message = err instanceof Error ? err.message : String(err);
|
|
280
364
|
await this.persist(__spreadProps(__spreadValues({}, this.snapshot.sync), {
|
|
@@ -321,4 +405,4 @@ class HealthSyncStore {
|
|
|
321
405
|
var _a;
|
|
322
406
|
return ((_a = this.deps.now) != null ? _a : Date.now)();
|
|
323
407
|
}
|
|
324
|
-
}export{FOREGROUND_THROTTLE_MS,HealthSyncStore};//# sourceMappingURL=healthSyncStore.js.map
|
|
408
|
+
}export{BACKGROUND_MIN_GAP_MS,FOREGROUND_THROTTLE_MS,HealthSyncStore,SESSION_MARGIN_MS};//# sourceMappingURL=healthSyncStore.js.map
|