@adminide-stack/yantra-mobile 12.0.125-alpha.1 → 12.0.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/lib/features/canvas/AudioEditorExperience.js +207 -109
  2. package/lib/features/canvas/AudioEditorExperience.js.map +1 -1
  3. package/lib/features/canvas/CanvasAgentChatPane.js +87 -72
  4. package/lib/features/canvas/CanvasAgentChatPane.js.map +1 -1
  5. package/lib/features/canvas/CanvasBoardsGrid.js +494 -387
  6. package/lib/features/canvas/CanvasBoardsGrid.js.map +1 -1
  7. package/lib/features/canvas/CanvasChatResultCard.js +78 -59
  8. package/lib/features/canvas/CanvasChatResultCard.js.map +1 -1
  9. package/lib/features/canvas/CanvasEditToolsPanel.js +126 -84
  10. package/lib/features/canvas/CanvasEditToolsPanel.js.map +1 -1
  11. package/lib/features/canvas/CanvasExperience.js +108 -126
  12. package/lib/features/canvas/CanvasExperience.js.map +1 -1
  13. package/lib/features/canvas/CanvasGeneratePanel.js +178 -166
  14. package/lib/features/canvas/CanvasGeneratePanel.js.map +1 -1
  15. package/lib/features/canvas/CanvasLibrarySheet.js +119 -123
  16. package/lib/features/canvas/CanvasLibrarySheet.js.map +1 -1
  17. package/lib/features/canvas/CanvasLooksPanel.js +113 -89
  18. package/lib/features/canvas/CanvasLooksPanel.js.map +1 -1
  19. package/lib/features/canvas/CanvasModelPicker.js +79 -79
  20. package/lib/features/canvas/CanvasModelPicker.js.map +1 -1
  21. package/lib/features/canvas/CanvasTasksPanel.js +193 -128
  22. package/lib/features/canvas/CanvasTasksPanel.js.map +1 -1
  23. package/lib/features/canvas/CanvasTemplatesPanel.js +106 -94
  24. package/lib/features/canvas/CanvasTemplatesPanel.js.map +1 -1
  25. package/lib/features/canvas/CanvasToolDock.js +195 -127
  26. package/lib/features/canvas/CanvasToolDock.js.map +1 -1
  27. package/lib/features/canvas/FrameExtractorExperience.js +163 -100
  28. package/lib/features/canvas/FrameExtractorExperience.js.map +1 -1
  29. package/lib/features/canvas/GeneratorReferenceSlots.js +132 -69
  30. package/lib/features/canvas/GeneratorReferenceSlots.js.map +1 -1
  31. package/lib/features/canvas/ImageEditorExperience.js +278 -209
  32. package/lib/features/canvas/ImageEditorExperience.js.map +1 -1
  33. package/lib/features/canvas/InfluencerStudioExperience.js +167 -125
  34. package/lib/features/canvas/InfluencerStudioExperience.js.map +1 -1
  35. package/lib/features/canvas/MoodboardExperience.js +102 -85
  36. package/lib/features/canvas/MoodboardExperience.js.map +1 -1
  37. package/lib/features/canvas/NativeCanvasViewer.js +203 -192
  38. package/lib/features/canvas/NativeCanvasViewer.js.map +1 -1
  39. package/lib/features/canvas/PromoContextFields.js +125 -92
  40. package/lib/features/canvas/PromoContextFields.js.map +1 -1
  41. package/lib/features/canvas/SetStudioExperience.js +225 -143
  42. package/lib/features/canvas/SetStudioExperience.js.map +1 -1
  43. package/lib/features/canvas/VideoClipsExperience.js +126 -92
  44. package/lib/features/canvas/VideoClipsExperience.js.map +1 -1
  45. package/lib/features/canvas/VideoEditExperience.js +154 -142
  46. package/lib/features/canvas/VideoEditExperience.js.map +1 -1
  47. package/lib/features/canvas/VideoEditorExperience.js +551 -492
  48. package/lib/features/canvas/VideoEditorExperience.js.map +1 -1
  49. package/lib/features/canvas/VideoGeneratorExperience.js +284 -131
  50. package/lib/features/canvas/VideoGeneratorExperience.js.map +1 -1
  51. package/lib/features/canvas/canvasTasks.js +7 -3
  52. package/lib/features/canvas/canvasTasks.js.map +1 -1
  53. package/lib/features/canvas/canvasTheme.js +265 -0
  54. package/lib/features/canvas/canvasTheme.js.map +1 -0
  55. package/lib/features/canvas/canvasUi.js +381 -0
  56. package/lib/features/canvas/canvasUi.js.map +1 -0
  57. package/lib/features/canvas/recorder/CameraRecorderExperience.js +109 -52
  58. package/lib/features/canvas/recorder/CameraRecorderExperience.js.map +1 -1
  59. package/lib/features/canvas/serverTasks.js +1 -1
  60. package/lib/features/canvas/serverTasks.js.map +1 -1
  61. package/lib/features/canvas/set/SetViewportWebView.js +1 -1
  62. package/lib/features/canvas/set/SetViewportWebView.js.map +1 -1
  63. package/lib/features/canvas/videoEditor/BlurBoxOverlay.js +8 -7
  64. package/lib/features/canvas/videoEditor/BlurBoxOverlay.js.map +1 -1
  65. package/lib/features/canvas/videoEditor/ExportTaskCard.js +44 -41
  66. package/lib/features/canvas/videoEditor/ExportTaskCard.js.map +1 -1
  67. package/lib/features/canvas/videoEditor/agent/AgentCutPane.js +50 -45
  68. package/lib/features/canvas/videoEditor/agent/AgentCutPane.js.map +1 -1
  69. package/lib/features/canvas/videoEditor/exportTask.js +3 -2
  70. package/lib/features/canvas/videoEditor/exportTask.js.map +1 -1
  71. package/lib/features/canvas/videoEditor/formWidgets.js +116 -64
  72. package/lib/features/canvas/videoEditor/formWidgets.js.map +1 -1
  73. package/lib/features/canvas/videoEditor/produce/ProduceVideoSheet.js +77 -63
  74. package/lib/features/canvas/videoEditor/produce/ProduceVideoSheet.js.map +1 -1
  75. package/lib/features/canvas/videoEditor/tools/ToolsDrawer.js +95 -79
  76. package/lib/features/canvas/videoEditor/tools/ToolsDrawer.js.map +1 -1
  77. package/lib/features/design/DesignEditorExperience.js +241 -160
  78. package/lib/features/design/DesignEditorExperience.js.map +1 -1
  79. package/lib/features/design/DesignExperience.js +300 -143
  80. package/lib/features/design/DesignExperience.js.map +1 -1
  81. package/lib/features/health/HealthSyncRunner.js +9 -1
  82. package/lib/features/health/HealthSyncRunner.js.map +1 -1
  83. package/lib/features/health/backgroundSync.js +41 -0
  84. package/lib/features/health/backgroundSync.js.map +1 -0
  85. package/lib/features/health/healthConnectBackground.js +32 -0
  86. package/lib/features/health/healthConnectBackground.js.map +1 -0
  87. package/lib/features/health/healthConnectReader.js +16 -2
  88. package/lib/features/health/healthConnectReader.js.map +1 -1
  89. package/lib/features/health/healthSync.js +31 -3
  90. package/lib/features/health/healthSync.js.map +1 -1
  91. package/lib/features/health/healthSyncStore.js +89 -5
  92. package/lib/features/health/healthSyncStore.js.map +1 -1
  93. package/lib/features/health/healthkitBackground.js +17 -0
  94. package/lib/features/health/healthkitBackground.js.map +1 -0
  95. package/lib/features/health/reader.js.map +1 -1
  96. package/lib/features/health/sessionVault.js +38 -0
  97. package/lib/features/health/sessionVault.js.map +1 -0
  98. package/lib/features/health/syncState.js.map +1 -1
  99. package/lib/features/webbuilder/WebBuilderExperience.js +1 -1
  100. package/lib/features/webbuilder/WebBuilderExperience.js.map +1 -1
  101. package/lib/features/webbuilder/buildFragments.js +3 -2
  102. package/lib/features/webbuilder/buildFragments.js.map +1 -1
  103. package/lib/features/webbuilder/components/PublishSheet.js +1 -1
  104. package/lib/features/webbuilder/components/PublishSheet.js.map +1 -1
  105. package/lib/features/webbuilder/webbuilderPrompts.js +2 -5
  106. package/lib/features/webbuilder/webbuilderPrompts.js.map +1 -1
  107. package/lib/index.js +1 -1
  108. package/lib/index.js.map +1 -1
  109. package/lib/screens/Health/index.js +12 -2
  110. package/lib/screens/Health/index.js.map +1 -1
  111. package/lib/screens/WebBuilderNative/sandboxProxyApi.js +120 -0
  112. package/lib/screens/WebBuilderNative/sandboxProxyApi.js.map +1 -0
  113. package/lib/screens/WebBuilderNative/useRestoredSandboxes.js.map +1 -1
  114. package/lib/screens/WebBuilderNative/useWebBuilderHost.js +26 -4
  115. package/lib/screens/WebBuilderNative/useWebBuilderHost.js.map +1 -1
  116. package/lib/screens/WebBuilderNative/webbuilderTransport.js +110 -7
  117. package/lib/screens/WebBuilderNative/webbuilderTransport.js.map +1 -1
  118. package/package.json +6 -4
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"healthSyncStore.js","sources":["../../../src/features/health/healthSyncStore.ts"],"sourcesContent":["/**\n * App-wide health sync state: one instance shared by the Health screen and\n * the root HealthSyncRunner (which owns the session and the foreground\n * trigger). A plain external store (useSyncExternalStore) instead of a React\n * context, so the runner can live at the app root while the screen mounts and\n * unmounts.\n *\n * Every sync is single-flight: a second trigger while one runs joins it.\n */\nimport { createFitnessClient } from './fitnessClient';\nimport type { HealthAvailability, HealthDataType, HealthReader } from './reader';\nimport { describeSyncError, runHealthSync } from './syncEngine';\nimport { INITIAL_SYNC_STATE, loadSyncState, saveSyncState, type HealthSyncState, type StorageLike } from './syncState';\nimport { deviceTimeZone, zoneClock } from './time';\n\n/** Foreground syncs are at most this frequent. \"Sync now\" ignores it. */\nexport const FOREGROUND_THROTTLE_MS = 15 * 60_000;\n/** How long a sync waits for the sign-in session before giving up. */\nconst SESSION_WAIT_MS = 30_000;\n\nexport interface HealthSession {\n token: string;\n url: string;\n /** Account the token belongs to. */\n owner?: string;\n}\n\nexport interface HealthSyncSnapshot {\n loaded: boolean;\n sync: HealthSyncState;\n running: boolean;\n availability?: HealthAvailability;\n /** Granted types; null = the platform does not tell (Apple Health). */\n granted?: HealthDataType[] | null;\n /** The data types this phone's health store is read for (platform and OS version). */\n types?: readonly HealthDataType[];\n /** Plain words, when signing in for sync failed. */\n sessionError?: string | null;\n /** A connect / disconnect action in progress. */\n busy?: 'connecting' | 'disconnecting';\n}\n\nexport interface HealthSyncStoreDeps {\n storage: StorageLike;\n /** Resolves null when this build has no health store (no native module, unsupported OS). */\n createReader: () => Promise<HealthReader | null>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n timeZone?: () => string;\n}\n\nexport class HealthSyncStore {\n private snapshot: HealthSyncSnapshot = { loaded: false, sync: { ...INITIAL_SYNC_STATE }, running: false };\n\n private listeners = new Set<() => void>();\n\n private loading: Promise<void> | null = null;\n\n private inFlight: Promise<boolean> | null = null;\n\n private reader: Promise<HealthReader | null> | null = null;\n\n private session: HealthSession | null = null;\n\n private sessionWaiters: Array<(s: HealthSession | null) => void> = [];\n\n /** Bumped by disconnect, so a sync still finishing cannot turn sync back on. */\n private generation = 0;\n\n private readonly deps: HealthSyncStoreDeps;\n\n constructor(deps: HealthSyncStoreDeps) {\n this.deps = deps;\n }\n\n subscribe = (listener: () => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): HealthSyncSnapshot => this.snapshot;\n\n private set(patch: Partial<HealthSyncSnapshot>) {\n this.snapshot = { ...this.snapshot, ...patch };\n this.listeners.forEach((l) => l());\n }\n\n private getReader(): Promise<HealthReader | null> {\n if (!this.reader) this.reader = this.deps.createReader().catch(() => null);\n return this.reader;\n }\n\n /** Opens the platform's own permission settings (Health Connect); false when there is none. */\n async openStoreSettings(): Promise<boolean> {\n const reader = await this.getReader();\n if (!reader?.openSettings) return false;\n try {\n await reader.openSettings();\n return true;\n } catch {\n return false;\n }\n }\n\n /** Load the saved state once; safe to call from many places. */\n load(): Promise<void> {\n if (!this.loading) {\n this.loading = (async () => {\n const sync = await loadSyncState(this.deps.storage);\n this.set({ loaded: true, sync });\n })();\n }\n return this.loading;\n }\n\n private async persist(sync: HealthSyncState) {\n this.set({ sync });\n await saveSyncState(this.deps.storage, sync);\n }\n\n /** The root runner hands over the sign-in session (or null when it is gone). */\n setSession(session: HealthSession | null, error?: string | null) {\n this.session = session;\n this.set({ sessionError: session ? null : (error ?? null) });\n if (session) {\n const waiters = this.sessionWaiters;\n this.sessionWaiters = [];\n waiters.forEach((w) => w(session));\n }\n }\n\n private waitForSession(): Promise<HealthSession | null> {\n if (this.session) return Promise.resolve(this.session);\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n this.sessionWaiters = this.sessionWaiters.filter((w) => w !== done);\n resolve(null);\n }, SESSION_WAIT_MS);\n const done = (s: HealthSession | null) => {\n clearTimeout(timer);\n resolve(s);\n };\n this.sessionWaiters.push(done);\n });\n }\n\n async refreshStatus(): Promise<void> {\n await this.load();\n const reader = await this.getReader();\n if (!reader) {\n this.set({\n availability: {\n status: 'unsupported',\n reason: 'Health data is not available in this version of the app. Update Yantra to connect.',\n },\n });\n return;\n }\n this.set({ types: reader.types });\n const availability = await reader.availability();\n this.set({ availability });\n if (availability.status === 'available' && this.snapshot.sync.connected) {\n try {\n this.set({ granted: await reader.grantedTypes() });\n } catch {\n this.set({ granted: undefined });\n }\n }\n }\n\n /** Ask for access, turn sync on, run the 30-day backfill. Resolves false if not connected. */\n async connect(): Promise<boolean> {\n await this.load();\n const reader = await this.getReader();\n if (!reader) return false;\n this.set({ busy: 'connecting' });\n try {\n const availability = await reader.availability();\n this.set({ availability });\n if (availability.status !== 'available') return false;\n const granted = await reader.requestAccess();\n this.set({ granted });\n if (granted && granted.length === 0) {\n await this.persist({\n ...this.snapshot.sync,\n lastError: `No data types were allowed. Allow them in ${reader.storeName} to sync.`,\n });\n return false;\n }\n const now = new Date(this.now()).toISOString();\n await this.persist({\n ...this.snapshot.sync,\n connected: true,\n source: reader.source,\n connectedAt: this.snapshot.sync.connectedAt ?? now,\n lastError: undefined,\n });\n } catch (err) {\n await this.persist({ ...this.snapshot.sync, lastError: describeSyncError(err) });\n return false;\n } finally {\n this.set({ busy: undefined });\n }\n void this.syncNow({ interactive: false });\n return true;\n }\n\n /**\n * Stop syncing. With `deleteData`, also delete everything this phone sent\n * to Yantra (the coach then forgets it). Health Connect access is given\n * back; Apple Health access can only be removed in the Health app.\n */\n async disconnect(opts: { deleteData: boolean }): Promise<{ ok: boolean; message?: string }> {\n await this.load();\n const reader = await this.getReader();\n this.set({ busy: 'disconnecting' });\n try {\n if (opts.deleteData && reader) {\n const session = await this.waitForSession();\n if (!session) {\n return { ok: false, message: 'Could not sign in to delete the data. Try again in a moment.' };\n }\n try {\n await createFitnessClient({\n url: session.url,\n token: session.token,\n fetchImpl: this.deps.fetchImpl,\n }).deleteSource(reader.source);\n } catch (err) {\n return { ok: false, message: describeSyncError(err) };\n }\n }\n this.generation += 1;\n await reader?.revoke();\n await this.persist({ ...INITIAL_SYNC_STATE });\n this.set({ granted: undefined });\n return { ok: true };\n } finally {\n this.set({ busy: undefined });\n }\n }\n\n /** Foreground trigger: syncs when connected and the last attempt is old enough. */\n async syncIfDue(): Promise<void> {\n await this.load();\n const { sync } = this.snapshot;\n if (!sync.connected) return;\n const last = sync.lastAttemptAt ? Date.parse(sync.lastAttemptAt) : 0;\n if (this.now() - last < FOREGROUND_THROTTLE_MS) return;\n await this.syncNow({ interactive: false });\n }\n\n /**\n * Run a sync now (joins one already running). `interactive` = the person\n * tapped \"Sync now\": on Apple Health this re-shows the permission sheet\n * if HealthKit still needs it, which an automatic sync never does.\n */\n syncNow(opts: { interactive: boolean } = { interactive: true }): Promise<boolean> {\n if (this.inFlight) return this.inFlight;\n this.inFlight = (async () => {\n this.set({ running: true });\n const { generation } = this;\n try {\n await this.load();\n const reader = await this.getReader();\n if (!reader || !this.snapshot.sync.connected) return false;\n if (opts.interactive) {\n try {\n const granted = await reader.requestAccess();\n this.set({ granted });\n } catch (err) {\n // The read would only repeat \"tap Sync now\"; say why the sheet did not open.\n const message = err instanceof Error ? err.message : String(err);\n await this.persist({\n ...this.snapshot.sync,\n lastAttemptAt: new Date(this.now()).toISOString(),\n lastError: `Could not ask ${reader.storeName} for access: ${message}`,\n });\n return false;\n }\n }\n const session = await this.waitForSession();\n if (generation !== this.generation) return false;\n if (!session) {\n await this.persist({\n ...this.snapshot.sync,\n lastAttemptAt: new Date(this.now()).toISOString(),\n lastError:\n this.snapshot.sessionError ||\n 'Could not sign in to Yantra to sync. Check your connection and try again.',\n });\n return false;\n }\n const outcome = await runHealthSync(this.snapshot.sync, {\n reader,\n client: createFitnessClient({\n url: session.url,\n token: session.token,\n fetchImpl: this.deps.fetchImpl,\n }),\n clock: zoneClock((this.deps.timeZone ?? deviceTimeZone)()),\n owner: session.owner,\n now: () => this.now(),\n onProgress: async (state) => {\n if (generation === this.generation) await this.persist(state);\n },\n });\n return outcome.ok;\n } finally {\n this.inFlight = null;\n this.set({ running: false });\n }\n })();\n return this.inFlight;\n }\n\n private now(): number {\n return (this.deps.now ?? Date.now)();\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"healthSyncStore.js","sources":["../../../src/features/health/healthSyncStore.ts"],"sourcesContent":["/**\n * App-wide health sync state: one instance shared by the Health screen and\n * the root HealthSyncRunner (which owns the session and the foreground\n * trigger). A plain external store (useSyncExternalStore) instead of a React\n * context, so the runner can live at the app root while the screen mounts and\n * unmounts.\n *\n * Every sync is single-flight: a second trigger while one runs joins it.\n *\n * In the background (backgroundSync.ts: iOS woke the app because the watch\n * wrote new sleep, HRV or a workout, or Android's hourly job ran) there is no\n * React tree and no way to sign in, so `syncInBackground` uses the session the\n * runner last handed over, kept in the secure store (`vault`). The platform\n * system token lasts days; one close to expiring waits for the next time the\n * app is opened, as before.\n */\nimport { tokenExpiryMs } from '../canvas/sessionIdentity';\nimport { createFitnessClient } from './fitnessClient';\nimport type { HealthAvailability, HealthDataType, HealthReader } from './reader';\nimport { describeSyncError, runHealthSync } from './syncEngine';\nimport { INITIAL_SYNC_STATE, loadSyncState, saveSyncState, type HealthSyncState, type StorageLike } from './syncState';\nimport { deviceTimeZone, zoneClock } from './time';\n\n/** Foreground syncs are at most this frequent. \"Sync now\" ignores it. */\nexport const FOREGROUND_THROTTLE_MS = 15 * 60_000;\n/** How long a sync waits for the sign-in session before giving up. */\nconst SESSION_WAIT_MS = 30_000;\n/**\n * A background wake this soon after the last attempt is the same burst: sleep,\n * HRV and resting heart rate land within seconds of each other.\n */\nexport const BACKGROUND_MIN_GAP_MS = 2 * 60_000;\n/** A saved session this close to expiring is left for the app to replace. */\nexport const SESSION_MARGIN_MS = 5 * 60_000;\n\nexport interface HealthSession {\n token: string;\n url: string;\n /** Account the token belongs to. */\n owner?: string;\n}\n\nexport interface HealthSyncSnapshot {\n loaded: boolean;\n sync: HealthSyncState;\n running: boolean;\n availability?: HealthAvailability;\n /** Granted types; null = the platform does not tell (Apple Health). */\n granted?: HealthDataType[] | null;\n /** Health Connect lets Yantra read while closed. Unset where that needs no permission (Apple Health). */\n backgroundAccess?: boolean;\n /** The data types this phone's health store is read for (platform and OS version). */\n types?: readonly HealthDataType[];\n /** Plain words, when signing in for sync failed. */\n sessionError?: string | null;\n /** A connect / disconnect action in progress. */\n busy?: 'connecting' | 'disconnecting';\n}\n\n/**\n * Where the last session is kept for background syncs. `read` THROWS when the\n * phone is locked (the secure store is unreadable then, and so is Apple\n * Health), which is how a background wake knows to wait.\n */\nexport interface SessionVault {\n read(): Promise<HealthSession | null>;\n write(session: HealthSession): Promise<void>;\n clear(): Promise<void>;\n}\n\n/** The platform's background delivery: on after a connect, off after a disconnect. */\nexport interface BackgroundDelivery {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\n/** Background delivery, and the wakes it brings (backgroundSync.ts). */\nexport interface BackgroundSource extends BackgroundDelivery {\n /**\n * Calls onChange whenever there may be new data: a watched type got samples\n * (iOS), the hourly job ran (Android). It resolves once the sync is done,\n * for a job that must not finish before it. Returns the unsubscribe.\n */\n watch(onChange: () => Promise<void>): () => void;\n}\n\nexport interface HealthSyncStoreDeps {\n storage: StorageLike;\n /** Resolves null when this build has no health store (no native module, unsupported OS). */\n createReader: () => Promise<HealthReader | null>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n timeZone?: () => string;\n vault?: SessionVault;\n background?: BackgroundDelivery;\n /** Still signed in to the app (the login token is there). Only asked when the phone is unlocked. */\n signedIn?: () => Promise<boolean>;\n}\n\nexport class HealthSyncStore {\n private snapshot: HealthSyncSnapshot = { loaded: false, sync: { ...INITIAL_SYNC_STATE }, running: false };\n\n private listeners = new Set<() => void>();\n\n private loading: Promise<void> | null = null;\n\n private inFlight: Promise<boolean> | null = null;\n\n private reader: Promise<HealthReader | null> | null = null;\n\n private session: HealthSession | null = null;\n\n private sessionWaiters: Array<(s: HealthSession | null) => void> = [];\n\n /** Bumped by disconnect, so a sync still finishing cannot turn sync back on. */\n private generation = 0;\n\n private readonly deps: HealthSyncStoreDeps;\n\n constructor(deps: HealthSyncStoreDeps) {\n this.deps = deps;\n }\n\n subscribe = (listener: () => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): HealthSyncSnapshot => this.snapshot;\n\n private set(patch: Partial<HealthSyncSnapshot>) {\n this.snapshot = { ...this.snapshot, ...patch };\n this.listeners.forEach((l) => l());\n }\n\n private getReader(): Promise<HealthReader | null> {\n if (!this.reader) this.reader = this.deps.createReader().catch(() => null);\n return this.reader;\n }\n\n /** Opens the platform's own permission settings (Health Connect); false when there is none. */\n async openStoreSettings(): Promise<boolean> {\n const reader = await this.getReader();\n if (!reader?.openSettings) return false;\n try {\n await reader.openSettings();\n return true;\n } catch {\n return false;\n }\n }\n\n /** Load the saved state once; safe to call from many places. */\n load(): Promise<void> {\n if (!this.loading) {\n this.loading = (async () => {\n const sync = await loadSyncState(this.deps.storage);\n this.set({ loaded: true, sync });\n })();\n }\n return this.loading;\n }\n\n private async persist(sync: HealthSyncState) {\n this.set({ sync });\n await saveSyncState(this.deps.storage, sync);\n }\n\n /** The root runner hands over the sign-in session (or null when it is gone). */\n setSession(session: HealthSession | null, error?: string | null) {\n this.session = session;\n this.set({ sessionError: session ? null : (error ?? null) });\n // Kept for background syncs. A null session is a failed or pending mint, not a sign-out:\n // the saved one stays until it expires, is replaced, or forgetSavedSession clears it.\n if (session) void this.deps.vault?.write(session).catch(() => undefined);\n if (session) {\n const waiters = this.sessionWaiters;\n this.sessionWaiters = [];\n waiters.forEach((w) => w(session));\n }\n }\n\n /** Signed out, or sync turned off: no background sync may use the saved session. */\n async forgetSavedSession(): Promise<void> {\n await this.deps.vault?.clear().catch(() => undefined);\n }\n\n /**\n * Turn background delivery on for someone already connected - at every\n * launch, so people who connected before this existed get it too.\n */\n async ensureBackgroundDelivery(): Promise<void> {\n await this.load();\n if (this.snapshot.sync.connected) await this.deps.background?.enable().catch(() => undefined);\n }\n\n /**\n * After anything that can change what was granted: delivery on (repeating\n * it is harmless), and Health Connect's answer to reading while closed kept\n * for the screen. `enable` schedules or drops Android's hourly job to\n * match, so access given in Health Connect's own settings counts once the\n * screen is back.\n */\n private async refreshBackgroundAccess(reader: HealthReader): Promise<void> {\n if (reader.canReadInBackground) {\n this.set({ backgroundAccess: await reader.canReadInBackground().catch(() => false) });\n }\n await this.deps.background?.enable().catch(() => undefined);\n }\n\n private waitForSession(): Promise<HealthSession | null> {\n if (this.session) return Promise.resolve(this.session);\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n this.sessionWaiters = this.sessionWaiters.filter((w) => w !== done);\n resolve(null);\n }, SESSION_WAIT_MS);\n const done = (s: HealthSession | null) => {\n clearTimeout(timer);\n resolve(s);\n };\n this.sessionWaiters.push(done);\n });\n }\n\n async refreshStatus(): Promise<void> {\n await this.load();\n const reader = await this.getReader();\n if (!reader) {\n this.set({\n availability: {\n status: 'unsupported',\n reason: 'Health data is not available in this version of the app. Update Yantra to connect.',\n },\n });\n return;\n }\n this.set({ types: reader.types });\n const availability = await reader.availability();\n this.set({ availability });\n if (availability.status === 'available' && this.snapshot.sync.connected) {\n try {\n this.set({ granted: await reader.grantedTypes() });\n } catch {\n this.set({ granted: undefined });\n }\n await this.refreshBackgroundAccess(reader);\n }\n }\n\n /** Ask for access, turn sync on, run the 30-day backfill. Resolves false if not connected. */\n async connect(): Promise<boolean> {\n await this.load();\n const reader = await this.getReader();\n if (!reader) return false;\n this.set({ busy: 'connecting' });\n try {\n const availability = await reader.availability();\n this.set({ availability });\n if (availability.status !== 'available') return false;\n const granted = await reader.requestAccess();\n this.set({ granted });\n if (granted && granted.length === 0) {\n await this.persist({\n ...this.snapshot.sync,\n lastError: `No data types were allowed. Allow them in ${reader.storeName} to sync.`,\n });\n return false;\n }\n const now = new Date(this.now()).toISOString();\n await this.persist({\n ...this.snapshot.sync,\n connected: true,\n source: reader.source,\n connectedAt: this.snapshot.sync.connectedAt ?? now,\n lastError: undefined,\n ...(reader.canReadInBackground ? { backgroundAskedAt: now } : {}),\n });\n void this.refreshBackgroundAccess(reader);\n } catch (err) {\n await this.persist({ ...this.snapshot.sync, lastError: describeSyncError(err) });\n return false;\n } finally {\n this.set({ busy: undefined });\n }\n void this.syncNow({ interactive: false });\n return true;\n }\n\n /**\n * Stop syncing. With `deleteData`, also delete everything this phone sent\n * to Yantra (the coach then forgets it). Health Connect access is given\n * back; Apple Health access can only be removed in the Health app.\n */\n async disconnect(opts: { deleteData: boolean }): Promise<{ ok: boolean; message?: string }> {\n await this.load();\n const reader = await this.getReader();\n this.set({ busy: 'disconnecting' });\n try {\n if (opts.deleteData && reader) {\n const session = await this.waitForSession();\n if (!session) {\n return { ok: false, message: 'Could not sign in to delete the data. Try again in a moment.' };\n }\n try {\n await createFitnessClient({\n url: session.url,\n token: session.token,\n fetchImpl: this.deps.fetchImpl,\n }).deleteSource(reader.source);\n } catch (err) {\n return { ok: false, message: describeSyncError(err) };\n }\n }\n this.generation += 1;\n await reader?.revoke();\n await this.deps.background?.disable().catch(() => undefined);\n await this.forgetSavedSession();\n await this.persist({ ...INITIAL_SYNC_STATE });\n this.set({ granted: undefined, backgroundAccess: undefined });\n return { ok: true };\n } finally {\n this.set({ busy: undefined });\n }\n }\n\n /** Foreground trigger: syncs when connected and the last attempt is old enough. */\n async syncIfDue(): Promise<void> {\n await this.load();\n const { sync } = this.snapshot;\n if (!sync.connected) return;\n const last = sync.lastAttemptAt ? Date.parse(sync.lastAttemptAt) : 0;\n if (this.now() - last < FOREGROUND_THROTTLE_MS) return;\n await this.syncNow({ interactive: false });\n }\n\n /**\n * A background wake: iOS woke the app because the watch saved new data, or\n * Android's hourly job ran, maybe with no screen and no sign-in. Syncs with\n * the session the app last had, unless the phone is locked (iOS: nothing\n * is readable then), the person signed out, the session is about to\n * expire, or this is the same burst as the last attempt. Every \"no\" is\n * silent: the next open syncs as it always did.\n */\n async syncInBackground(): Promise<boolean> {\n await this.load();\n const { sync } = this.snapshot;\n if (!sync.connected) return false;\n const last = sync.lastAttemptAt ? Date.parse(sync.lastAttemptAt) : 0;\n if (this.now() - last < BACKGROUND_MIN_GAP_MS) return false;\n let saved: HealthSession | null;\n try {\n saved = (await this.deps.vault?.read()) ?? null;\n } catch {\n // Locked: the secure store and Apple Health are both unreadable until it is unlocked.\n return false;\n }\n // Unlocked from here on, so a missing login token really means signed out.\n if (this.deps.signedIn && !(await this.deps.signedIn().catch(() => false))) {\n await this.forgetSavedSession();\n return false;\n }\n const session = this.session ?? saved;\n if (!session) return false;\n const expires = tokenExpiryMs(session.token);\n if (expires !== null && expires - this.now() < SESSION_MARGIN_MS) return false;\n if (!this.session) this.session = session;\n return this.syncNow({ interactive: false });\n }\n\n /**\n * Run a sync now (joins one already running). `interactive` = the person\n * tapped \"Sync now\": on Apple Health this re-shows the permission sheet\n * if HealthKit still needs it, which an automatic sync never does.\n */\n syncNow(opts: { interactive: boolean } = { interactive: true }): Promise<boolean> {\n if (this.inFlight) return this.inFlight;\n this.inFlight = (async () => {\n this.set({ running: true });\n const { generation } = this;\n try {\n await this.load();\n const reader = await this.getReader();\n if (!reader || !this.snapshot.sync.connected) return false;\n if (opts.interactive) {\n try {\n const granted = await reader.requestAccess();\n this.set({ granted });\n if (reader.canReadInBackground) {\n await this.persist({\n ...this.snapshot.sync,\n backgroundAskedAt: new Date(this.now()).toISOString(),\n });\n }\n void this.refreshBackgroundAccess(reader);\n } catch (err) {\n // The read would only repeat \"tap Sync now\"; say why the sheet did not open.\n const message = err instanceof Error ? err.message : String(err);\n await this.persist({\n ...this.snapshot.sync,\n lastAttemptAt: new Date(this.now()).toISOString(),\n lastError: `Could not ask ${reader.storeName} for access: ${message}`,\n });\n return false;\n }\n }\n const session = await this.waitForSession();\n if (generation !== this.generation) return false;\n if (!session) {\n await this.persist({\n ...this.snapshot.sync,\n lastAttemptAt: new Date(this.now()).toISOString(),\n lastError:\n this.snapshot.sessionError ||\n 'Could not sign in to Yantra to sync. Check your connection and try again.',\n });\n return false;\n }\n const outcome = await runHealthSync(this.snapshot.sync, {\n reader,\n client: createFitnessClient({\n url: session.url,\n token: session.token,\n fetchImpl: this.deps.fetchImpl,\n }),\n clock: zoneClock((this.deps.timeZone ?? deviceTimeZone)()),\n owner: session.owner,\n now: () => this.now(),\n onProgress: async (state) => {\n if (generation === this.generation) await this.persist(state);\n },\n });\n return outcome.ok;\n } finally {\n this.inFlight = null;\n this.set({ running: false });\n }\n })();\n return this.inFlight;\n }\n\n private now(): number {\n return (this.deps.now ?? Date.now)();\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import {subscribeToChanges,clearBackgroundTypes,disableAllBackgroundDelivery,configureBackgroundTypes}from'@kingstinct/react-native-healthkit';const BACKGROUND_TYPES = ["HKCategoryTypeIdentifierSleepAnalysis", "HKQuantityTypeIdentifierHeartRateVariabilitySDNN", "HKQuantityTypeIdentifierRestingHeartRate", "HKWorkoutTypeIdentifier", "HKQuantityTypeIdentifierStepCount", "HKQuantityTypeIdentifierBodyMass"];
2
+ const IMMEDIATE = 1;
3
+ const healthkitBackground = {
4
+ async enable() {
5
+ await configureBackgroundTypes([...BACKGROUND_TYPES], IMMEDIATE);
6
+ },
7
+ async disable() {
8
+ await clearBackgroundTypes();
9
+ await disableAllBackgroundDelivery();
10
+ },
11
+ watch(onChange) {
12
+ const subs = BACKGROUND_TYPES.map((type) => subscribeToChanges(type, () => {
13
+ void onChange();
14
+ }));
15
+ return () => subs.forEach((s) => s.remove());
16
+ }
17
+ };export{BACKGROUND_TYPES,healthkitBackground};//# sourceMappingURL=healthkitBackground.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthkitBackground.js","sources":["../../../src/features/health/healthkitBackground.ts"],"sourcesContent":["/**\n * Apple Health background delivery: iOS wakes Yantra when one of these types\n * gets new samples (the watch syncs last night's sleep, a workout ends, a\n * scale weighs in), the way Bevel stays current without being opened.\n *\n * The library does the native half: `configureBackgroundTypes` saves the list\n * and registers observer queries at every launch before JS loads, queuing\n * what fires until `subscribeToChanges` is called, and holds each wake open\n * about 25 seconds. It needs the background-delivery entitlement (the\n * HealthKit config plugin's `background` in app.json) - a build without it\n * still runs the observers in the foreground only.\n *\n * Loaded with a dynamic import on iOS only (healthSync.ts), like the reader.\n */\nimport {\n clearBackgroundTypes,\n configureBackgroundTypes,\n disableAllBackgroundDelivery,\n subscribeToChanges,\n} from '@kingstinct/react-native-healthkit';\n\ntype Frequency = Parameters<typeof configureBackgroundTypes>[1];\ntype SampleType = Parameters<typeof subscribeToChanges>[0];\n\n/**\n * What is worth waking for. Steps keep the day's strain current (iOS delivers\n * them at most hourly whatever is asked); the rest arrive a few times a day.\n */\nexport const BACKGROUND_TYPES = [\n 'HKCategoryTypeIdentifierSleepAnalysis',\n 'HKQuantityTypeIdentifierHeartRateVariabilitySDNN',\n 'HKQuantityTypeIdentifierRestingHeartRate',\n 'HKWorkoutTypeIdentifier',\n 'HKQuantityTypeIdentifierStepCount',\n 'HKQuantityTypeIdentifierBodyMass',\n] as const;\n\n/** HKUpdateFrequency.immediate. */\nconst IMMEDIATE = 1 as Frequency;\n\nexport const healthkitBackground = {\n async enable(): Promise<void> {\n // Safe to repeat: the library stops the observers it had before registering again.\n await configureBackgroundTypes([...BACKGROUND_TYPES], IMMEDIATE);\n },\n async disable(): Promise<void> {\n // Nothing is registered at the next launch, and HealthKit stops waking the app. Only\n // health sync uses background delivery, so all of it can go.\n await clearBackgroundTypes();\n await disableAllBackgroundDelivery();\n },\n watch(onChange: () => Promise<void>): () => void {\n // The library holds each wake open on its own, so nothing waits for the sync here.\n const subs = BACKGROUND_TYPES.map((type) =>\n subscribeToChanges(type as SampleType, () => {\n void onChange();\n }),\n );\n return () => subs.forEach((s) => s.remove());\n },\n};\n"],"names":[],"mappings":"+IAsBO,MAAM,mBAAmB,CAAC,uCAAA,EAAyC,oDAAoD,0CAA4C,EAAA,yBAAA,EAA2B,qCAAqC,kCAAkC;AAG5Q,MAAM,SAAY,GAAA,CAAA;AACX,MAAM,mBAAsB,GAAA;AAAA,EACjC,MAAM,MAAwB,GAAA;AAE5B,IAAA,MAAM,wBAAyB,CAAA,CAAC,GAAG,gBAAgB,GAAG,SAAS,CAAA;AAAA,GACjE;AAAA,EACA,MAAM,OAAyB,GAAA;AAG7B,IAAA,MAAM,oBAAqB,EAAA;AAC3B,IAAA,MAAM,4BAA6B,EAAA;AAAA,GACrC;AAAA,EACA,MAAM,QAA2C,EAAA;AAE/C,IAAA,MAAM,OAAO,gBAAiB,CAAA,GAAA,CAAI,CAAQ,IAAA,KAAA,kBAAA,CAAmB,MAAoB,MAAM;AACrF,MAAA,KAAK,QAAS,EAAA;AAAA,KACf,CAAC,CAAA;AACF,IAAA,OAAO,MAAM,IAAK,CAAA,OAAA,CAAQ,CAAK,CAAA,KAAA,CAAA,CAAE,QAAQ,CAAA;AAAA;AAE7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"reader.js","sources":["../../../src/features/health/reader.ts"],"sourcesContent":["/**\n * What the sync engine needs from a platform. healthkitReader.ts (iOS) and\n * healthConnectReader.ts (Android) implement it; tests use fakes.\n */\nimport type { RawHealthData } from './model';\nimport type { ZoneClock } from './time';\nimport type { DeviceHealthSource } from './types';\n\nexport type HealthDataType =\n | 'steps'\n | 'distance'\n | 'activeCalories'\n | 'restingCalories'\n | 'exercise'\n | 'heartRate'\n | 'restingHr'\n | 'hrv'\n | 'sleep'\n | 'wristTemp'\n | 'workouts'\n | 'elevation'\n | 'daylight'\n | 'mindful'\n | 'weight'\n | 'vo2Max'\n | 'spo2'\n | 'respiratoryRate'\n | 'bodyFat'\n | 'leanMass'\n | 'bmi'\n | 'waist'\n | 'bodyTemp'\n | 'bloodPressure'\n | 'bloodGlucose';\n\n/**\n * What the screen lists under \"Data Yantra reads\", in this order. Each\n * platform reads some of them (`HealthReader.types`); the screen lists those.\n */\nexport const HEALTH_DATA_TYPES: ReadonlyArray<{ id: HealthDataType; label: string }> = [\n { id: 'sleep', label: 'Sleep' },\n { id: 'wristTemp', label: 'Wrist temperature' },\n { id: 'heartRate', label: 'Heart rate' },\n { id: 'restingHr', label: 'Resting heart rate' },\n { id: 'hrv', label: 'Heart rate variability' },\n { id: 'steps', label: 'Steps' },\n { id: 'distance', label: 'Distance' },\n { id: 'activeCalories', label: 'Active energy' },\n { id: 'restingCalories', label: 'Resting energy' },\n { id: 'exercise', label: 'Exercise minutes' },\n { id: 'workouts', label: 'Workouts' },\n { id: 'elevation', label: 'Elevation gained' },\n { id: 'daylight', label: 'Time in daylight' },\n { id: 'mindful', label: 'Mindful minutes' },\n { id: 'weight', label: 'Weight' },\n { id: 'vo2Max', label: 'VO2 max' },\n { id: 'spo2', label: 'Blood oxygen' },\n { id: 'respiratoryRate', label: 'Respiratory rate' },\n { id: 'bodyFat', label: 'Body fat' },\n { id: 'leanMass', label: 'Lean body mass' },\n { id: 'bmi', label: 'BMI' },\n { id: 'waist', label: 'Waist' },\n { id: 'bodyTemp', label: 'Body temperature' },\n { id: 'bloodPressure', label: 'Blood pressure' },\n { id: 'bloodGlucose', label: 'Blood glucose' },\n];\n\nexport function labelOf(type: HealthDataType): string {\n return HEALTH_DATA_TYPES.find((t) => t.id === type)?.label ?? type;\n}\n\nexport type HealthAvailability =\n | { status: 'available' }\n /** Android: the Health Connect app is not installed (Android 13 and older). */\n | { status: 'not-installed' }\n /** Android: Health Connect is installed but needs an update. */\n | { status: 'needs-update' }\n | { status: 'unsupported'; reason: string };\n\nexport interface ReadResult {\n raw: RawHealthData;\n /** Types that could not be read (permission off, provider error). The rest still syncs. */\n unreadable: HealthDataType[];\n /**\n * Types this version of the app reads that the person was never asked\n * about (added in an update, or by a newer OS). They are not read until\n * the permission sheet shows them; the rest still syncs.\n */\n needsAccess?: HealthDataType[];\n}\n\nexport interface HealthReader {\n readonly source: DeviceHealthSource;\n /** What the platform calls its health store, for the screen. */\n readonly storeName: string;\n /** The data types read on this device (platform and OS version). */\n readonly types: readonly HealthDataType[];\n availability(): Promise<HealthAvailability>;\n /**\n * Shows the system permission sheet. Resolves with the types granted, or\n * null when the platform does not tell apps (HealthKit hides read access).\n */\n requestAccess(): Promise<HealthDataType[] | null>;\n /** Granted read types, or null when the platform does not tell apps. */\n grantedTypes(): Promise<HealthDataType[] | null>;\n /** Everything recorded between two instants. */\n read(range: { start: number; end: number }, clock: ZoneClock): Promise<ReadResult>;\n /** Give the permissions back where the platform allows it (Health Connect); a no-op on iOS. */\n revoke(): Promise<void>;\n /** The platform's own screen for this app's permissions, when it has one. */\n openSettings?(): Promise<void>;\n}\n"],"names":[],"mappings":"AAaO,MAAM,oBAGR,CAAC;AAAA,EACJ,EAAI,EAAA,OAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,WAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,WAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,WAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,KAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,OAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,gBAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,iBAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,WAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,SAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,QAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,QAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,MAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,iBAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,SAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,KAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,OAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,eAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,cAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAC;AACM,SAAS,QAAQ,IAA8B,EAAA;AA5FtD,EAAA,IAAA,EAAA,EAAA,EAAA;AA6FE,EAAO,OAAA,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,iBAAA,CAAkB,KAAK,CAAK,CAAA,KAAA,CAAA,CAAE,OAAO,IAAI,CAAA,KAAzC,IAA4C,GAAA,MAAA,GAAA,EAAA,CAAA,KAAA,KAA5C,IAAqD,GAAA,EAAA,GAAA,IAAA;AAC9D"}
1
+ {"version":3,"file":"reader.js","sources":["../../../src/features/health/reader.ts"],"sourcesContent":["/**\n * What the sync engine needs from a platform. healthkitReader.ts (iOS) and\n * healthConnectReader.ts (Android) implement it; tests use fakes.\n */\nimport type { RawHealthData } from './model';\nimport type { ZoneClock } from './time';\nimport type { DeviceHealthSource } from './types';\n\nexport type HealthDataType =\n | 'steps'\n | 'distance'\n | 'activeCalories'\n | 'restingCalories'\n | 'exercise'\n | 'heartRate'\n | 'restingHr'\n | 'hrv'\n | 'sleep'\n | 'wristTemp'\n | 'workouts'\n | 'elevation'\n | 'daylight'\n | 'mindful'\n | 'weight'\n | 'vo2Max'\n | 'spo2'\n | 'respiratoryRate'\n | 'bodyFat'\n | 'leanMass'\n | 'bmi'\n | 'waist'\n | 'bodyTemp'\n | 'bloodPressure'\n | 'bloodGlucose';\n\n/**\n * What the screen lists under \"Data Yantra reads\", in this order. Each\n * platform reads some of them (`HealthReader.types`); the screen lists those.\n */\nexport const HEALTH_DATA_TYPES: ReadonlyArray<{ id: HealthDataType; label: string }> = [\n { id: 'sleep', label: 'Sleep' },\n { id: 'wristTemp', label: 'Wrist temperature' },\n { id: 'heartRate', label: 'Heart rate' },\n { id: 'restingHr', label: 'Resting heart rate' },\n { id: 'hrv', label: 'Heart rate variability' },\n { id: 'steps', label: 'Steps' },\n { id: 'distance', label: 'Distance' },\n { id: 'activeCalories', label: 'Active energy' },\n { id: 'restingCalories', label: 'Resting energy' },\n { id: 'exercise', label: 'Exercise minutes' },\n { id: 'workouts', label: 'Workouts' },\n { id: 'elevation', label: 'Elevation gained' },\n { id: 'daylight', label: 'Time in daylight' },\n { id: 'mindful', label: 'Mindful minutes' },\n { id: 'weight', label: 'Weight' },\n { id: 'vo2Max', label: 'VO2 max' },\n { id: 'spo2', label: 'Blood oxygen' },\n { id: 'respiratoryRate', label: 'Respiratory rate' },\n { id: 'bodyFat', label: 'Body fat' },\n { id: 'leanMass', label: 'Lean body mass' },\n { id: 'bmi', label: 'BMI' },\n { id: 'waist', label: 'Waist' },\n { id: 'bodyTemp', label: 'Body temperature' },\n { id: 'bloodPressure', label: 'Blood pressure' },\n { id: 'bloodGlucose', label: 'Blood glucose' },\n];\n\nexport function labelOf(type: HealthDataType): string {\n return HEALTH_DATA_TYPES.find((t) => t.id === type)?.label ?? type;\n}\n\nexport type HealthAvailability =\n | { status: 'available' }\n /** Android: the Health Connect app is not installed (Android 13 and older). */\n | { status: 'not-installed' }\n /** Android: Health Connect is installed but needs an update. */\n | { status: 'needs-update' }\n | { status: 'unsupported'; reason: string };\n\nexport interface ReadResult {\n raw: RawHealthData;\n /** Types that could not be read (permission off, provider error). The rest still syncs. */\n unreadable: HealthDataType[];\n /**\n * Types this version of the app reads that the person was never asked\n * about (added in an update, or by a newer OS). They are not read until\n * the permission sheet shows them; the rest still syncs.\n */\n needsAccess?: HealthDataType[];\n}\n\nexport interface HealthReader {\n readonly source: DeviceHealthSource;\n /** What the platform calls its health store, for the screen. */\n readonly storeName: string;\n /** The data types read on this device (platform and OS version). */\n readonly types: readonly HealthDataType[];\n availability(): Promise<HealthAvailability>;\n /**\n * Shows the system permission sheet. Resolves with the types granted, or\n * null when the platform does not tell apps (HealthKit hides read access).\n */\n requestAccess(): Promise<HealthDataType[] | null>;\n /** Granted read types, or null when the platform does not tell apps. */\n grantedTypes(): Promise<HealthDataType[] | null>;\n /** Everything recorded between two instants. */\n read(range: { start: number; end: number }, clock: ZoneClock): Promise<ReadResult>;\n /** Give the permissions back where the platform allows it (Health Connect); a no-op on iOS. */\n revoke(): Promise<void>;\n /**\n * Whether the platform lets Yantra read while it is closed, where that is a\n * permission of its own (Health Connect). Apple Health's background\n * delivery needs none, so its reader leaves this out.\n */\n canReadInBackground?(): Promise<boolean>;\n /** The platform's own screen for this app's permissions, when it has one. */\n openSettings?(): Promise<void>;\n}\n"],"names":[],"mappings":"AAaO,MAAM,oBAGR,CAAC;AAAA,EACJ,EAAI,EAAA,OAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,WAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,WAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,WAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,KAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,OAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,gBAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,iBAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,WAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,SAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,QAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,QAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,MAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,iBAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,SAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,KAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,OAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,UAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,eAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAG,EAAA;AAAA,EACD,EAAI,EAAA,cAAA;AAAA,EACJ,KAAO,EAAA;AACT,CAAC;AACM,SAAS,QAAQ,IAA8B,EAAA;AA5FtD,EAAA,IAAA,EAAA,EAAA,EAAA;AA6FE,EAAO,OAAA,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,iBAAA,CAAkB,KAAK,CAAK,CAAA,KAAA,CAAA,CAAE,OAAO,IAAI,CAAA,KAAzC,IAA4C,GAAA,MAAA,GAAA,EAAA,CAAA,KAAA,KAA5C,IAAqD,GAAA,EAAA,GAAA,IAAA;AAC9D"}
@@ -0,0 +1,38 @@
1
+ import*as SecureStore from'expo-secure-store';var __defProp = Object.defineProperty;
2
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __spreadValues = (a, b) => {
7
+ for (var prop in b || (b = {}))
8
+ if (__hasOwnProp.call(b, prop))
9
+ __defNormalProp(a, prop, b[prop]);
10
+ if (__getOwnPropSymbols)
11
+ for (var prop of __getOwnPropSymbols(b)) {
12
+ if (__propIsEnum.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ }
15
+ return a;
16
+ };
17
+ const KEY = "yantra.health.session";
18
+ function parseSession(raw) {
19
+ if (!raw) return null;
20
+ try {
21
+ const o = JSON.parse(raw);
22
+ if (typeof o.token !== "string" || !o.token || typeof o.url !== "string" || !o.url) return null;
23
+ return __spreadValues({
24
+ token: o.token,
25
+ url: o.url
26
+ }, typeof o.owner === "string" ? {
27
+ owner: o.owner
28
+ } : {});
29
+ } catch (e) {
30
+ return null;
31
+ }
32
+ }
33
+ const secureSessionVault = {
34
+ // Not caught: a locked phone must surface as a throw.
35
+ read: async () => parseSession(await SecureStore.getItemAsync(KEY)),
36
+ write: (session) => SecureStore.setItemAsync(KEY, JSON.stringify(session)),
37
+ clear: () => SecureStore.deleteItemAsync(KEY)
38
+ };export{parseSession,secureSessionVault};//# sourceMappingURL=sessionVault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionVault.js","sources":["../../../src/features/health/sessionVault.ts"],"sourcesContent":["/**\n * The Fitness session, kept in the secure store for syncs that run while\n * Yantra is closed (healthSyncStore `syncInBackground`).\n *\n * The secure store's default protection (readable only while the phone is\n * unlocked) is deliberate: Apple Health is unreadable while locked anyway, and\n * a read that throws is how a background wake knows to wait.\n */\nimport * as SecureStore from 'expo-secure-store';\nimport type { HealthSession, SessionVault } from './healthSyncStore';\n\nconst KEY = 'yantra.health.session';\n\nexport function parseSession(raw: string | null): HealthSession | null {\n if (!raw) return null;\n try {\n const o = JSON.parse(raw) as Partial<HealthSession>;\n if (typeof o.token !== 'string' || !o.token || typeof o.url !== 'string' || !o.url) return null;\n return { token: o.token, url: o.url, ...(typeof o.owner === 'string' ? { owner: o.owner } : {}) };\n } catch {\n return null;\n }\n}\n\nexport const secureSessionVault: SessionVault = {\n // Not caught: a locked phone must surface as a throw.\n read: async () => parseSession(await SecureStore.getItemAsync(KEY)),\n write: (session) => SecureStore.setItemAsync(KEY, JSON.stringify(session)),\n clear: () => SecureStore.deleteItemAsync(KEY),\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAUA,MAAM,GAAM,GAAA,uBAAA;AACL,SAAS,aAAa,GAA0C,EAAA;AACrE,EAAI,IAAA,CAAC,KAAY,OAAA,IAAA;AACjB,EAAI,IAAA;AACF,IAAM,MAAA,CAAA,GAAI,IAAK,CAAA,KAAA,CAAM,GAAG,CAAA;AACxB,IAAA,IAAI,OAAO,CAAA,CAAE,KAAU,KAAA,QAAA,IAAY,CAAC,CAAE,CAAA,KAAA,IAAS,OAAO,CAAA,CAAE,GAAQ,KAAA,QAAA,IAAY,CAAC,CAAA,CAAE,KAAY,OAAA,IAAA;AAC3F,IAAO,OAAA,cAAA,CAAA;AAAA,MACL,OAAO,CAAE,CAAA,KAAA;AAAA,MACT,KAAK,CAAE,CAAA;AAAA,KACH,EAAA,OAAO,CAAE,CAAA,KAAA,KAAU,QAAW,GAAA;AAAA,MAChC,OAAO,CAAE,CAAA;AAAA,QACP,EAAC,CAAA;AAAA,GAED,CAAA,OAAA,CAAA,EAAA;AACN,IAAO,OAAA,IAAA;AAAA;AAEX;AACO,MAAM,kBAAmC,GAAA;AAAA;AAAA,EAE9C,MAAM,YAAY,YAAA,CAAa,MAAM,WAAY,CAAA,YAAA,CAAa,GAAG,CAAC,CAAA;AAAA,EAClE,KAAA,EAAO,aAAW,WAAY,CAAA,YAAA,CAAa,KAAK,IAAK,CAAA,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA,EACvE,KAAO,EAAA,MAAM,WAAY,CAAA,eAAA,CAAgB,GAAG;AAC9C"}
@@ -1 +1 @@
1
- {"version":3,"file":"syncState.js","sources":["../../../src/features/health/syncState.ts"],"sourcesContent":["/**\n * The small local record that drives health sync: whether the person turned\n * it on, the cursor (last local date fully sent), and what the screen shows\n * (last sync, days synced, the last problem in plain words).\n *\n * The cursor is scoped to the signed-in account (`owner`): another account\n * on the same phone starts its own backfill instead of inheriting one.\n */\nimport type { DeviceHealthSource } from './types';\nimport { addDays, daysBetween } from './time';\n\nexport const HEALTH_SYNC_STORAGE_KEY = 'yantra.health.sync.v1';\n\n/** First connect: this many days, today included. */\nexport const BACKFILL_DAYS = 30;\n/** Every sync re-sends this many days before the cursor (watches sync late). */\nexport const RESEND_DAYS = 2;\n/** After a long absence, catch up at most this far back. */\nexport const MAX_CATCH_UP_DAYS = 90;\n/** Dates remembered for the \"days synced\" count. */\nconst MAX_REMEMBERED_DATES = 400;\n\nexport interface HealthSyncState {\n version: 1;\n connected: boolean;\n source?: DeviceHealthSource;\n connectedAt?: string;\n /** Account the cursor belongs to (token subject). */\n owner?: string;\n /** Last local date a sync covered; the next sync re-sends from RESEND_DAYS before it. */\n lastSyncedDate?: string;\n /** When the last sync finished without error (ISO). */\n lastSyncAt?: string;\n lastAttemptAt?: string;\n /** Days the backend stored in the last sync. */\n lastRunDays?: number;\n /** Distinct dates the backend has stored from this phone (bounded). */\n syncedDates: string[];\n /** Plain-words problem from the last attempt; cleared by a good sync. */\n lastError?: string;\n /** Data types that could not be read last time (permission off, provider error). */\n unreadableTypes?: string[];\n /** Data types the person was never asked about (added since they connected); \"Sync now\" asks. */\n needsAccessTypes?: string[];\n /** Values the app or the backend dropped last time, for the \"why\" line. */\n lastDropped?: string[];\n}\n\nexport const INITIAL_SYNC_STATE: HealthSyncState = { version: 1, connected: false, syncedDates: [] };\n\nexport interface StorageLike {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\nexport async function loadSyncState(storage: StorageLike): Promise<HealthSyncState> {\n try {\n const raw = await storage.getItem(HEALTH_SYNC_STORAGE_KEY);\n if (!raw) return { ...INITIAL_SYNC_STATE };\n const parsed = JSON.parse(raw) as Partial<HealthSyncState>;\n if (parsed?.version !== 1) return { ...INITIAL_SYNC_STATE };\n return {\n ...INITIAL_SYNC_STATE,\n ...parsed,\n syncedDates: Array.isArray(parsed.syncedDates) ? parsed.syncedDates.filter(isDateString) : [],\n };\n } catch {\n return { ...INITIAL_SYNC_STATE };\n }\n}\n\nexport async function saveSyncState(storage: StorageLike, state: HealthSyncState): Promise<void> {\n try {\n await storage.setItem(HEALTH_SYNC_STORAGE_KEY, JSON.stringify(state));\n } catch {\n // A lost write only costs a re-send next time (upserts are idempotent).\n }\n}\n\nconst isDateString = (v: unknown): v is string => typeof v === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(v);\n\nexport function rememberDates(existing: readonly string[], added: readonly string[]): string[] {\n const all = [...new Set([...existing, ...added])].sort();\n return all.slice(Math.max(0, all.length - MAX_REMEMBERED_DATES));\n}\n\n/**\n * Which local dates the next sync covers, both included.\n * - Never synced: the last BACKFILL_DAYS days.\n * - Otherwise: RESEND_DAYS before the cursor through today, but no further\n * back than MAX_CATCH_UP_DAYS.\n */\nexport function syncWindow(\n state: Pick<HealthSyncState, 'lastSyncedDate'>,\n today: string,\n): { from: string; to: string } {\n const backfillFrom = addDays(today, -(BACKFILL_DAYS - 1));\n const cursor = state.lastSyncedDate;\n if (!cursor || cursor > today) return { from: backfillFrom, to: today };\n let from = addDays(cursor, -RESEND_DAYS);\n if (daysBetween(from, today) >= MAX_CATCH_UP_DAYS) from = addDays(today, -(MAX_CATCH_UP_DAYS - 1));\n return { from, to: today };\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAUO,MAAM,uBAA0B,GAAA;AAOhC,MAAM,iBAAoB,GAAA;AAEjC,MAAM,oBAAuB,GAAA,GAAA;AA0BtB,MAAM,kBAAsC,GAAA;AAAA,EACjD,OAAS,EAAA,CAAA;AAAA,EACT,SAAW,EAAA,KAAA;AAAA,EACX,aAAa;AACf;AAMA,eAAsB,cAAc,OAAgD,EAAA;AAClF,EAAI,IAAA;AACF,IAAA,MAAM,GAAM,GAAA,MAAM,OAAQ,CAAA,OAAA,CAAQ,uBAAuB,CAAA;AACzD,IAAI,IAAA,CAAC,GAAK,EAAA,OAAO,cACZ,CAAA,EAAA,EAAA,kBAAA,CAAA;AAEL,IAAM,MAAA,MAAA,GAAS,IAAK,CAAA,KAAA,CAAM,GAAG,CAAA;AAC7B,IAAA,IAAA,CAAI,MAAQ,IAAA,IAAA,GAAA,KAAA,CAAA,GAAA,MAAA,CAAA,OAAA,MAAY,CAAG,EAAA,OAAO,cAC7B,CAAA,EAAA,EAAA,kBAAA,CAAA;AAEL,IAAO,OAAA,aAAA,CAAA,cAAA,CAAA,cAAA,CAAA,EAAA,EACF,qBACA,MAFE,CAAA,EAAA;AAAA,MAGL,WAAA,EAAa,KAAM,CAAA,OAAA,CAAQ,MAAO,CAAA,WAAW,CAAI,GAAA,MAAA,CAAO,WAAY,CAAA,MAAA,CAAO,YAAY,CAAA,GAAI;AAAC,KAC9F,CAAA;AAAA,GACM,CAAA,OAAA,CAAA,EAAA;AACN,IAAA,OAAO,cACF,CAAA,EAAA,EAAA,kBAAA,CAAA;AAAA;AAGT;AACsB,eAAA,aAAA,CAAc,SAAsB,KAAuC,EAAA;AAC/F,EAAI,IAAA;AACF,IAAA,MAAM,QAAQ,OAAQ,CAAA,uBAAA,EAAyB,IAAK,CAAA,SAAA,CAAU,KAAK,CAAC,CAAA;AAAA,GAC9D,CAAA,OAAA,CAAA,EAAA;AAAA;AAGV;AACA,MAAM,YAAA,GAAe,CAAC,CAA4B,KAAA,OAAO,MAAM,QAAY,IAAA,qBAAA,CAAsB,KAAK,CAAC,CAAA;AACvF,SAAA,aAAA,CAAc,UAA6B,KAAoC,EAAA;AAC7F,EAAA,MAAM,GAAM,GAAA,CAAC,mBAAG,IAAI,GAAI,CAAA,CAAC,GAAG,QAAA,EAAU,GAAG,KAAK,CAAC,CAAC,EAAE,IAAK,EAAA;AACvD,EAAO,OAAA,GAAA,CAAI,MAAM,IAAK,CAAA,GAAA,CAAI,GAAG,GAAI,CAAA,MAAA,GAAS,oBAAoB,CAAC,CAAA;AACjE;AAQgB,SAAA,UAAA,CAAW,OAAgD,KAGzE,EAAA;AACA,EAAA,MAAM,YAAe,GAAA,OAAA,CAAQ,KAAO,EAAA,GAAoB,CAAA;AACxD,EAAA,MAAM,SAAS,KAAM,CAAA,cAAA;AACrB,EAAA,IAAI,CAAC,MAAA,IAAU,MAAS,GAAA,KAAA,EAAc,OAAA;AAAA,IACpC,IAAM,EAAA,YAAA;AAAA,IACN,EAAI,EAAA;AAAA,GACN;AACA,EAAA,IAAI,IAAO,GAAA,OAAA,CAAQ,MAAQ,EAAA,EAAY,CAAA;AACvC,EAAI,IAAA,WAAA,CAAY,IAAM,EAAA,KAAK,CAAK,IAAA,iBAAA,SAA0B,OAAQ,CAAA,KAAA,EAAO,GAAwB,CAAA;AACjG,EAAO,OAAA;AAAA,IACL,IAAA;AAAA,IACA,EAAI,EAAA;AAAA,GACN;AACF"}
1
+ {"version":3,"file":"syncState.js","sources":["../../../src/features/health/syncState.ts"],"sourcesContent":["/**\n * The small local record that drives health sync: whether the person turned\n * it on, the cursor (last local date fully sent), and what the screen shows\n * (last sync, days synced, the last problem in plain words).\n *\n * The cursor is scoped to the signed-in account (`owner`): another account\n * on the same phone starts its own backfill instead of inheriting one.\n */\nimport type { DeviceHealthSource } from './types';\nimport { addDays, daysBetween } from './time';\n\nexport const HEALTH_SYNC_STORAGE_KEY = 'yantra.health.sync.v1';\n\n/** First connect: this many days, today included. */\nexport const BACKFILL_DAYS = 30;\n/** Every sync re-sends this many days before the cursor (watches sync late). */\nexport const RESEND_DAYS = 2;\n/** After a long absence, catch up at most this far back. */\nexport const MAX_CATCH_UP_DAYS = 90;\n/** Dates remembered for the \"days synced\" count. */\nconst MAX_REMEMBERED_DATES = 400;\n\nexport interface HealthSyncState {\n version: 1;\n connected: boolean;\n source?: DeviceHealthSource;\n connectedAt?: string;\n /** Account the cursor belongs to (token subject). */\n owner?: string;\n /** Last local date a sync covered; the next sync re-sends from RESEND_DAYS before it. */\n lastSyncedDate?: string;\n /** When the last sync finished without error (ISO). */\n lastSyncAt?: string;\n lastAttemptAt?: string;\n /** Days the backend stored in the last sync. */\n lastRunDays?: number;\n /** Distinct dates the backend has stored from this phone (bounded). */\n syncedDates: string[];\n /** Plain-words problem from the last attempt; cleared by a good sync. */\n lastError?: string;\n /** Data types that could not be read last time (permission off, provider error). */\n unreadableTypes?: string[];\n /** Data types the person was never asked about (added since they connected); \"Sync now\" asks. */\n needsAccessTypes?: string[];\n /** Health Connect: when it was asked to let Yantra read while closed (connect, \"Sync now\"). */\n backgroundAskedAt?: string;\n /** Values the app or the backend dropped last time, for the \"why\" line. */\n lastDropped?: string[];\n}\n\nexport const INITIAL_SYNC_STATE: HealthSyncState = { version: 1, connected: false, syncedDates: [] };\n\nexport interface StorageLike {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\nexport async function loadSyncState(storage: StorageLike): Promise<HealthSyncState> {\n try {\n const raw = await storage.getItem(HEALTH_SYNC_STORAGE_KEY);\n if (!raw) return { ...INITIAL_SYNC_STATE };\n const parsed = JSON.parse(raw) as Partial<HealthSyncState>;\n if (parsed?.version !== 1) return { ...INITIAL_SYNC_STATE };\n return {\n ...INITIAL_SYNC_STATE,\n ...parsed,\n syncedDates: Array.isArray(parsed.syncedDates) ? parsed.syncedDates.filter(isDateString) : [],\n };\n } catch {\n return { ...INITIAL_SYNC_STATE };\n }\n}\n\nexport async function saveSyncState(storage: StorageLike, state: HealthSyncState): Promise<void> {\n try {\n await storage.setItem(HEALTH_SYNC_STORAGE_KEY, JSON.stringify(state));\n } catch {\n // A lost write only costs a re-send next time (upserts are idempotent).\n }\n}\n\nconst isDateString = (v: unknown): v is string => typeof v === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(v);\n\nexport function rememberDates(existing: readonly string[], added: readonly string[]): string[] {\n const all = [...new Set([...existing, ...added])].sort();\n return all.slice(Math.max(0, all.length - MAX_REMEMBERED_DATES));\n}\n\n/**\n * Which local dates the next sync covers, both included.\n * - Never synced: the last BACKFILL_DAYS days.\n * - Otherwise: RESEND_DAYS before the cursor through today, but no further\n * back than MAX_CATCH_UP_DAYS.\n */\nexport function syncWindow(\n state: Pick<HealthSyncState, 'lastSyncedDate'>,\n today: string,\n): { from: string; to: string } {\n const backfillFrom = addDays(today, -(BACKFILL_DAYS - 1));\n const cursor = state.lastSyncedDate;\n if (!cursor || cursor > today) return { from: backfillFrom, to: today };\n let from = addDays(cursor, -RESEND_DAYS);\n if (daysBetween(from, today) >= MAX_CATCH_UP_DAYS) from = addDays(today, -(MAX_CATCH_UP_DAYS - 1));\n return { from, to: today };\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAUO,MAAM,uBAA0B,GAAA;AAOhC,MAAM,iBAAoB,GAAA;AAEjC,MAAM,oBAAuB,GAAA,GAAA;AA4BtB,MAAM,kBAAsC,GAAA;AAAA,EACjD,OAAS,EAAA,CAAA;AAAA,EACT,SAAW,EAAA,KAAA;AAAA,EACX,aAAa;AACf;AAMA,eAAsB,cAAc,OAAgD,EAAA;AAClF,EAAI,IAAA;AACF,IAAA,MAAM,GAAM,GAAA,MAAM,OAAQ,CAAA,OAAA,CAAQ,uBAAuB,CAAA;AACzD,IAAI,IAAA,CAAC,GAAK,EAAA,OAAO,cACZ,CAAA,EAAA,EAAA,kBAAA,CAAA;AAEL,IAAM,MAAA,MAAA,GAAS,IAAK,CAAA,KAAA,CAAM,GAAG,CAAA;AAC7B,IAAA,IAAA,CAAI,MAAQ,IAAA,IAAA,GAAA,KAAA,CAAA,GAAA,MAAA,CAAA,OAAA,MAAY,CAAG,EAAA,OAAO,cAC7B,CAAA,EAAA,EAAA,kBAAA,CAAA;AAEL,IAAO,OAAA,aAAA,CAAA,cAAA,CAAA,cAAA,CAAA,EAAA,EACF,qBACA,MAFE,CAAA,EAAA;AAAA,MAGL,WAAA,EAAa,KAAM,CAAA,OAAA,CAAQ,MAAO,CAAA,WAAW,CAAI,GAAA,MAAA,CAAO,WAAY,CAAA,MAAA,CAAO,YAAY,CAAA,GAAI;AAAC,KAC9F,CAAA;AAAA,GACM,CAAA,OAAA,CAAA,EAAA;AACN,IAAA,OAAO,cACF,CAAA,EAAA,EAAA,kBAAA,CAAA;AAAA;AAGT;AACsB,eAAA,aAAA,CAAc,SAAsB,KAAuC,EAAA;AAC/F,EAAI,IAAA;AACF,IAAA,MAAM,QAAQ,OAAQ,CAAA,uBAAA,EAAyB,IAAK,CAAA,SAAA,CAAU,KAAK,CAAC,CAAA;AAAA,GAC9D,CAAA,OAAA,CAAA,EAAA;AAAA;AAGV;AACA,MAAM,YAAA,GAAe,CAAC,CAA4B,KAAA,OAAO,MAAM,QAAY,IAAA,qBAAA,CAAsB,KAAK,CAAC,CAAA;AACvF,SAAA,aAAA,CAAc,UAA6B,KAAoC,EAAA;AAC7F,EAAA,MAAM,GAAM,GAAA,CAAC,mBAAG,IAAI,GAAI,CAAA,CAAC,GAAG,QAAA,EAAU,GAAG,KAAK,CAAC,CAAC,EAAE,IAAK,EAAA;AACvD,EAAO,OAAA,GAAA,CAAI,MAAM,IAAK,CAAA,GAAA,CAAI,GAAG,GAAI,CAAA,MAAA,GAAS,oBAAoB,CAAC,CAAA;AACjE;AAQgB,SAAA,UAAA,CAAW,OAAgD,KAGzE,EAAA;AACA,EAAA,MAAM,YAAe,GAAA,OAAA,CAAQ,KAAO,EAAA,GAAoB,CAAA;AACxD,EAAA,MAAM,SAAS,KAAM,CAAA,cAAA;AACrB,EAAA,IAAI,CAAC,MAAA,IAAU,MAAS,GAAA,KAAA,EAAc,OAAA;AAAA,IACpC,IAAM,EAAA,YAAA;AAAA,IACN,EAAI,EAAA;AAAA,GACN;AACA,EAAA,IAAI,IAAO,GAAA,OAAA,CAAQ,MAAQ,EAAA,EAAY,CAAA;AACvC,EAAI,IAAA,WAAA,CAAY,IAAM,EAAA,KAAK,CAAK,IAAA,iBAAA,SAA0B,OAAQ,CAAA,KAAA,EAAO,GAAwB,CAAA;AACjG,EAAO,OAAA;AAAA,IACL,IAAA;AAAA,IACA,EAAI,EAAA;AAAA,GACN;AACF"}
@@ -156,7 +156,7 @@ function WebBuilderExperience({
156
156
  const resume = onResumePreview ? onResumePreview(build) : Promise.resolve("failed");
157
157
  resume.catch(() => "failed").then((outcome) => {
158
158
  if (outcome === "ok") setPreviewNonce((n) => n + 1);
159
- else if (hostChat && !hostChat.busy) hostChat.send(wakePreviewPrompt(build), void 0, build);
159
+ else if (hostChat && !hostChat.busy) hostChat.send(wakePreviewPrompt(), void 0, build);
160
160
  }).finally(() => setWaking(false));
161
161
  }, [build, waking, onResumePreview, hostChat]);
162
162
  const [publishOpen, setPublishOpen] = useState(false);