@adminide-stack/yantra-mobile 12.0.104 → 12.0.105
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/bridge/handlers/SurfaceHostBridgeHandler.js +15 -3
- package/lib/bridge/handlers/SurfaceHostBridgeHandler.js.map +1 -1
- package/lib/bridge/types.js.map +1 -1
- package/lib/features/apps/mobileAppCatalog.js +11 -1
- package/lib/features/apps/mobileAppCatalog.js.map +1 -1
- package/lib/features/fitness-widgets/FitnessDeepLink.js +90 -0
- package/lib/features/fitness-widgets/FitnessDeepLink.js.map +1 -0
- package/lib/features/fitness-widgets/FitnessWidgetRunner.js +71 -0
- package/lib/features/fitness-widgets/FitnessWidgetRunner.js.map +1 -0
- package/lib/features/fitness-widgets/fitnessLink.js +38 -0
- package/lib/features/fitness-widgets/fitnessLink.js.map +1 -0
- package/lib/features/fitness-widgets/fitnessWidgetStore.js +86 -0
- package/lib/features/fitness-widgets/fitnessWidgetStore.js.map +1 -0
- package/lib/features/fitness-widgets/fitnessWidgetsNative.js +13 -0
- package/lib/features/fitness-widgets/fitnessWidgetsNative.js.map +1 -0
- package/lib/features/fitness-widgets/hostLiveActivity.js +5 -0
- package/lib/features/fitness-widgets/hostLiveActivity.js.map +1 -0
- package/lib/features/fitness-widgets/liveActivity.js +76 -0
- package/lib/features/fitness-widgets/liveActivity.js.map +1 -0
- package/lib/features/fitness-widgets/widgetSnapshot.js +56 -0
- package/lib/features/fitness-widgets/widgetSnapshot.js.map +1 -0
- package/lib/features/health/fitnessClient.js +15 -0
- package/lib/features/health/fitnessClient.js.map +1 -1
- package/lib/hooks/useChatApi.js +2 -1
- package/lib/hooks/useChatApi.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/screens/WebBuilder/index.js +4 -2
- package/lib/screens/WebBuilder/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {createFitnessClient}from'../health/fitnessClient.js';import {zoneClock,deviceTimeZone}from'../health/time.js';import {buildWidgetSnapshot}from'./widgetSnapshot.js';var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
const WIDGET_REFRESH_THROTTLE_MS = 15 * 6e4;
|
|
5
|
+
function defaultLog(message, error) {
|
|
6
|
+
console.warn(message, error instanceof Error ? error.message : error != null ? error : "");
|
|
7
|
+
}
|
|
8
|
+
class FitnessWidgetStore {
|
|
9
|
+
constructor(deps) {
|
|
10
|
+
__publicField(this, "session", null);
|
|
11
|
+
/** When the last fetch started; 0 = never (or since the snapshot was cleared). */
|
|
12
|
+
__publicField(this, "lastAttemptAt", 0);
|
|
13
|
+
__publicField(this, "inFlight", null);
|
|
14
|
+
__publicField(this, "deps");
|
|
15
|
+
this.deps = deps;
|
|
16
|
+
}
|
|
17
|
+
setSession(session) {
|
|
18
|
+
this.session = session;
|
|
19
|
+
}
|
|
20
|
+
/** Foreground trigger: refreshes when the last fetch is at least 15 minutes old. */
|
|
21
|
+
refreshIfDue() {
|
|
22
|
+
if (this.inFlight) return this.inFlight;
|
|
23
|
+
if (this.lastAttemptAt && this.now() - this.lastAttemptAt < WIDGET_REFRESH_THROTTLE_MS) {
|
|
24
|
+
return Promise.resolve(false);
|
|
25
|
+
}
|
|
26
|
+
return this.refresh();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fetch today's report and write the snapshot now. Resolves true when a
|
|
30
|
+
* snapshot was written; never rejects.
|
|
31
|
+
*/
|
|
32
|
+
refresh() {
|
|
33
|
+
if (this.inFlight) return this.inFlight;
|
|
34
|
+
const {
|
|
35
|
+
writer
|
|
36
|
+
} = this.deps;
|
|
37
|
+
const {
|
|
38
|
+
session
|
|
39
|
+
} = this;
|
|
40
|
+
if (!writer || !session) return Promise.resolve(false);
|
|
41
|
+
this.lastAttemptAt = this.now();
|
|
42
|
+
this.inFlight = (async () => {
|
|
43
|
+
var _a;
|
|
44
|
+
try {
|
|
45
|
+
const now = this.now();
|
|
46
|
+
const date = zoneClock(((_a = this.deps.timeZone) != null ? _a : deviceTimeZone)()).dateOf(now);
|
|
47
|
+
const report = await createFitnessClient({
|
|
48
|
+
url: session.url,
|
|
49
|
+
token: session.token,
|
|
50
|
+
fetchImpl: this.deps.fetchImpl
|
|
51
|
+
}).health(date);
|
|
52
|
+
if (!this.session) return false;
|
|
53
|
+
const snapshot = buildWidgetSnapshot(report, {
|
|
54
|
+
date,
|
|
55
|
+
now: this.now()
|
|
56
|
+
});
|
|
57
|
+
return writer.writeSnapshot(JSON.stringify(snapshot)) !== false;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
this.log("[FitnessWidgets] could not refresh the widget snapshot:", error);
|
|
60
|
+
return false;
|
|
61
|
+
} finally {
|
|
62
|
+
this.inFlight = null;
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
return this.inFlight;
|
|
66
|
+
}
|
|
67
|
+
/** Signed out: the widgets must not keep showing this account's numbers. */
|
|
68
|
+
clear() {
|
|
69
|
+
var _a;
|
|
70
|
+
this.session = null;
|
|
71
|
+
this.lastAttemptAt = 0;
|
|
72
|
+
try {
|
|
73
|
+
(_a = this.deps.writer) == null ? void 0 : _a.writeSnapshot(null);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.log("[FitnessWidgets] could not clear the widget snapshot:", error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
now() {
|
|
79
|
+
var _a;
|
|
80
|
+
return ((_a = this.deps.now) != null ? _a : Date.now)();
|
|
81
|
+
}
|
|
82
|
+
log(message, error) {
|
|
83
|
+
var _a;
|
|
84
|
+
((_a = this.deps.log) != null ? _a : defaultLog)(message, error);
|
|
85
|
+
}
|
|
86
|
+
}export{FitnessWidgetStore,WIDGET_REFRESH_THROTTLE_MS};//# sourceMappingURL=fitnessWidgetStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fitnessWidgetStore.js","sources":["../../../src/features/fitness-widgets/fitnessWidgetStore.ts"],"sourcesContent":["/**\n * Keeps the iOS Fitness widgets' snapshot fresh: fetches today's report from\n * the Fitness backend and writes the compact snapshot to the shared App Group,\n * which reloads the widgets.\n *\n * Triggers (FitnessWidgetRunner): every return to the foreground, at most\n * every 15 minutes (`refreshIfDue`), and right after a health sync finishes\n * (`refresh`, which ignores the throttle - new data just arrived). One fetch\n * at a time; a second trigger joins the one running.\n *\n * Never in the way: no session or no native module means nothing happens, and\n * a failed fetch is logged and dropped - the widget keeps the last snapshot\n * and says how old it is.\n */\nimport { createFitnessClient } from '../health/fitnessClient';\nimport { deviceTimeZone, zoneClock } from '../health/time';\nimport { buildWidgetSnapshot } from './widgetSnapshot';\n\n/** Foreground refreshes are at most this frequent. */\nexport const WIDGET_REFRESH_THROTTLE_MS = 15 * 60_000;\n\nexport interface WidgetSession {\n /** Fitness backend GraphQL URL. */\n url: string;\n /** The same platform session token health sync uses. */\n token: string;\n}\n\nexport interface SnapshotWriter {\n writeSnapshot(json: string | null): boolean;\n}\n\nexport interface FitnessWidgetStoreDeps {\n /** Null when this binary has no widgets module (Android, or an older build). */\n writer: SnapshotWriter | null;\n fetchImpl?: typeof fetch;\n now?: () => number;\n timeZone?: () => string;\n log?: (message: string, error?: unknown) => void;\n}\n\nfunction defaultLog(message: string, error?: unknown) {\n // eslint-disable-next-line no-console\n console.warn(message, error instanceof Error ? error.message : (error ?? ''));\n}\n\nexport class FitnessWidgetStore {\n private session: WidgetSession | null = null;\n\n /** When the last fetch started; 0 = never (or since the snapshot was cleared). */\n private lastAttemptAt = 0;\n\n private inFlight: Promise<boolean> | null = null;\n\n private readonly deps: FitnessWidgetStoreDeps;\n\n constructor(deps: FitnessWidgetStoreDeps) {\n this.deps = deps;\n }\n\n setSession(session: WidgetSession | null): void {\n this.session = session;\n }\n\n /** Foreground trigger: refreshes when the last fetch is at least 15 minutes old. */\n refreshIfDue(): Promise<boolean> {\n if (this.inFlight) return this.inFlight;\n if (this.lastAttemptAt && this.now() - this.lastAttemptAt < WIDGET_REFRESH_THROTTLE_MS) {\n return Promise.resolve(false);\n }\n return this.refresh();\n }\n\n /**\n * Fetch today's report and write the snapshot now. Resolves true when a\n * snapshot was written; never rejects.\n */\n refresh(): Promise<boolean> {\n if (this.inFlight) return this.inFlight;\n const { writer } = this.deps;\n const { session } = this;\n // Waiting for a session is not an attempt: the throttle starts at the first real fetch.\n if (!writer || !session) return Promise.resolve(false);\n this.lastAttemptAt = this.now();\n this.inFlight = (async () => {\n try {\n const now = this.now();\n const date = zoneClock((this.deps.timeZone ?? deviceTimeZone)()).dateOf(now);\n const report = await createFitnessClient({\n url: session.url,\n token: session.token,\n fetchImpl: this.deps.fetchImpl,\n }).health(date);\n // Signed out while the fetch ran: do not write the old session's numbers back.\n if (!this.session) return false;\n const snapshot = buildWidgetSnapshot(report, { date, now: this.now() });\n return writer.writeSnapshot(JSON.stringify(snapshot)) !== false;\n } catch (error) {\n this.log('[FitnessWidgets] could not refresh the widget snapshot:', error);\n return false;\n } finally {\n this.inFlight = null;\n }\n })();\n return this.inFlight;\n }\n\n /** Signed out: the widgets must not keep showing this account's numbers. */\n clear(): void {\n this.session = null;\n this.lastAttemptAt = 0;\n try {\n this.deps.writer?.writeSnapshot(null);\n } catch (error) {\n this.log('[FitnessWidgets] could not clear the widget snapshot:', error);\n }\n }\n\n private now(): number {\n return (this.deps.now ?? Date.now)();\n }\n\n private log(message: string, error?: unknown) {\n (this.deps.log ?? defaultLog)(message, error);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {requireOptionalNativeModule}from'expo-modules-core';import {Platform}from'react-native';let cached;
|
|
2
|
+
function fitnessWidgetsNative() {
|
|
3
|
+
var _a;
|
|
4
|
+
if (cached !== void 0) return cached;
|
|
5
|
+
cached = null;
|
|
6
|
+
if (Platform.OS !== "ios") return cached;
|
|
7
|
+
try {
|
|
8
|
+
cached = (_a = requireOptionalNativeModule("YantraFitnessWidgets")) != null ? _a : null;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
cached = null;
|
|
11
|
+
}
|
|
12
|
+
return cached;
|
|
13
|
+
}export{fitnessWidgetsNative};//# sourceMappingURL=fitnessWidgetsNative.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fitnessWidgetsNative.js","sources":["../../../src/features/fitness-widgets/fitnessWidgetsNative.ts"],"sourcesContent":["/**\n * The app's native Fitness widgets module (portable-devices/mobile/modules/\n * yantra-fitness-widgets, iOS only): the widget snapshot in the shared App\n * Group, and the workout Live Activity.\n *\n * Optional on purpose: an over-the-air JS update can reach a binary built\n * before the module existed, and Android has none. Both read as \"no module\",\n * and every caller then does nothing.\n */\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport { Platform } from 'react-native';\n\n/** The workout as the Live Activity shows it; times are epoch milliseconds. */\nexport interface WorkoutActivityState {\n name: string;\n startedAtMs: number;\n setsDone: number;\n setsTotal: number;\n exercise: string | null;\n restEndsAtMs: number | null;\n}\n\nexport interface FitnessWidgetsNative {\n /** Writes today's snapshot JSON (null removes it) and reloads the widgets. */\n writeSnapshot(json: string | null): boolean;\n /** Yantra widgets on the home and lock screens right now. */\n installedWidgetCount(): Promise<number>;\n /** iOS 16.2+ and Live Activities allowed for Yantra in Settings. */\n liveActivitiesEnabled(): boolean;\n /** Resolves the activity id. Replaces any other workout activity. */\n startWorkoutActivity(state: WorkoutActivityState): Promise<string>;\n /** False when no workout activity is running. */\n updateWorkoutActivity(state: WorkoutActivityState): Promise<boolean>;\n /** Resolves how many were running. */\n endWorkoutActivity(): Promise<number>;\n}\n\nlet cached: FitnessWidgetsNative | null | undefined;\n\nexport function fitnessWidgetsNative(): FitnessWidgetsNative | null {\n if (cached !== undefined) return cached;\n cached = null;\n if (Platform.OS !== 'ios') return cached;\n try {\n cached = requireOptionalNativeModule<FitnessWidgetsNative>('YantraFitnessWidgets') ?? null;\n } catch {\n cached = null;\n }\n return cached;\n}\n"],"names":[],"mappings":"gGAmCA,IAAI,MAAA;AACG,SAAS,oBAAoD,GAAA;AApCpE,EAAA,IAAA,EAAA;AAqCE,EAAI,IAAA,MAAA,KAAW,QAAkB,OAAA,MAAA;AACjC,EAAS,MAAA,GAAA,IAAA;AACT,EAAI,IAAA,QAAA,CAAS,EAAO,KAAA,KAAA,EAAc,OAAA,MAAA;AAClC,EAAI,IAAA;AACF,IAAS,MAAA,GAAA,CAAA,EAAA,GAAA,2BAAA,CAAkD,sBAAsB,CAAA,KAAxE,IAA6E,GAAA,EAAA,GAAA,IAAA;AAAA,GAChF,CAAA,OAAA,CAAA,EAAA;AACN,IAAS,MAAA,GAAA,IAAA;AAAA;AAEX,EAAO,OAAA,MAAA;AACT"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import {fitnessWidgetsNative}from'./fitnessWidgetsNative.js';import {createHostLiveActivity}from'./liveActivity.js';let shared;
|
|
2
|
+
function hostLiveActivity() {
|
|
3
|
+
if (shared === void 0) shared = createHostLiveActivity(fitnessWidgetsNative());
|
|
4
|
+
return shared;
|
|
5
|
+
}export{hostLiveActivity};//# sourceMappingURL=hostLiveActivity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostLiveActivity.js","sources":["../../../src/features/fitness-widgets/hostLiveActivity.ts"],"sourcesContent":["/**\n * The one workout Live Activity controller the app's surface WebViews share,\n * so calls from any open surface run in one queue.\n */\nimport { fitnessWidgetsNative } from './fitnessWidgetsNative';\nimport { createHostLiveActivity, type HostLiveActivity } from './liveActivity';\n\nlet shared: HostLiveActivity | null | undefined;\n\n/** Null on Android and on binaries without the native module. */\nexport function hostLiveActivity(): HostLiveActivity | null {\n if (shared === undefined) shared = createHostLiveActivity(fitnessWidgetsNative());\n return shared;\n}\n"],"names":[],"mappings":"oHAMA,IAAI,MAAA;AAGG,SAAS,gBAA4C,GAAA;AAC1D,EAAA,IAAI,MAAW,KAAA,MAAA,EAAoB,MAAA,GAAA,sBAAA,CAAuB,sBAAsB,CAAA;AAChF,EAAO,OAAA,MAAA;AACT"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const MAX_TEXT = 64;
|
|
2
|
+
const MAX_WORKOUT_MS = 12 * 60 * 6e4;
|
|
3
|
+
function text(value, max = MAX_TEXT) {
|
|
4
|
+
if (typeof value !== "string") return null;
|
|
5
|
+
const trimmed = value.replace(/\s+/g, " ").trim();
|
|
6
|
+
if (!trimmed) return null;
|
|
7
|
+
return trimmed.length > max ? `${trimmed.slice(0, max - 1)}\u2026` : trimmed;
|
|
8
|
+
}
|
|
9
|
+
function count(value) {
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(999, Math.round(value))) : 0;
|
|
11
|
+
}
|
|
12
|
+
function instant(value) {
|
|
13
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
14
|
+
const ms = Date.parse(value);
|
|
15
|
+
return Number.isFinite(ms) ? ms : null;
|
|
16
|
+
}
|
|
17
|
+
function parseLiveActivityPayload(raw, now) {
|
|
18
|
+
var _a;
|
|
19
|
+
const payload = raw && typeof raw === "object" ? raw : {};
|
|
20
|
+
const startedAt = instant(payload.startedAt);
|
|
21
|
+
if (startedAt === null) throw new Error("startedAt must be an ISO date and time.");
|
|
22
|
+
if (now - startedAt > MAX_WORKOUT_MS) throw new Error("That workout started too long ago to show.");
|
|
23
|
+
const setsTotal = count(payload.setsTotal);
|
|
24
|
+
const restEndsAt = instant(payload.restEndsAt);
|
|
25
|
+
return {
|
|
26
|
+
name: (_a = text(payload.name)) != null ? _a : "Workout",
|
|
27
|
+
// A phone clock a little behind the page's must not show a negative time.
|
|
28
|
+
startedAtMs: Math.min(startedAt, now),
|
|
29
|
+
setsDone: Math.min(count(payload.setsDone), setsTotal),
|
|
30
|
+
setsTotal,
|
|
31
|
+
exercise: text(payload.exercise),
|
|
32
|
+
restEndsAtMs: restEndsAt !== null && restEndsAt > now ? restEndsAt : null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function createHostLiveActivity(native, now = Date.now) {
|
|
36
|
+
if (!native) return null;
|
|
37
|
+
let queue = Promise.resolve();
|
|
38
|
+
const serial = (run) => {
|
|
39
|
+
const next = queue.then(run, run);
|
|
40
|
+
queue = next.catch(() => void 0);
|
|
41
|
+
return next;
|
|
42
|
+
};
|
|
43
|
+
const supported = () => {
|
|
44
|
+
try {
|
|
45
|
+
return native.liveActivitiesEnabled() === true;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const requireSupported = () => {
|
|
51
|
+
if (!supported()) throw new Error("Live Activities are turned off or not available on this phone.");
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
supported,
|
|
55
|
+
async start(payload) {
|
|
56
|
+
const state = parseLiveActivityPayload(payload, now());
|
|
57
|
+
requireSupported();
|
|
58
|
+
return serial(async () => ({
|
|
59
|
+
started: true,
|
|
60
|
+
id: await native.startWorkoutActivity(state)
|
|
61
|
+
}));
|
|
62
|
+
},
|
|
63
|
+
async update(payload) {
|
|
64
|
+
const state = parseLiveActivityPayload(payload, now());
|
|
65
|
+
requireSupported();
|
|
66
|
+
return serial(async () => ({
|
|
67
|
+
updated: await native.updateWorkoutActivity(state)
|
|
68
|
+
}));
|
|
69
|
+
},
|
|
70
|
+
async end() {
|
|
71
|
+
return serial(async () => ({
|
|
72
|
+
ended: await native.endWorkoutActivity()
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}export{createHostLiveActivity,parseLiveActivityPayload};//# sourceMappingURL=liveActivity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liveActivity.js","sources":["../../../src/features/fitness-widgets/liveActivity.ts"],"sourcesContent":["/**\n * The workout Live Activity a web surface drives through the host bridge\n * (`liveActivity.start` / `.update` / `.end`, see bridge/handlers/\n * SurfaceHostBridgeHandler). The Fitness surface's live workout logger\n * (yantra-surface packages/fitness/src/utils/liveActivity.ts) is the caller.\n *\n * Payload for start and update (end takes `{}`):\n * { name: string, startedAt: ISO string, setsDone: number, setsTotal: number,\n * exercise: string | null, restEndsAt?: ISO string }\n *\n * - start is idempotent and replaces any workout activity already running\n * (reopening the workout page sends start again for the same workout).\n * - update is called often (the surface debounces to about 500 ms); with no\n * activity running (the person dismissed it) it does nothing.\n * - Calls run one at a time, in order, so an update never overtakes the start\n * it follows.\n */\nimport type { FitnessWidgetsNative, WorkoutActivityState } from './fitnessWidgetsNative';\n\n/** What the surface sends for `liveActivity.start` and `liveActivity.update`. */\nexport interface LiveActivityPayload {\n name: string;\n /** ISO; the lock screen counts the elapsed time up from it. */\n startedAt: string;\n setsDone: number;\n setsTotal: number;\n /** The current exercise (the first with a set still to do); null once all are done. */\n exercise: string | null;\n /** ISO end of the rest countdown, while resting. */\n restEndsAt?: string;\n}\n\nconst MAX_TEXT = 64;\n/** A workout older than this is not \"in progress\" any more (the system ends activities after 8 hours anyway). */\nconst MAX_WORKOUT_MS = 12 * 60 * 60_000;\n\nfunction text(value: unknown, max = MAX_TEXT): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.replace(/\\s+/g, ' ').trim();\n if (!trimmed) return null;\n return trimmed.length > max ? `${trimmed.slice(0, max - 1)}…` : trimmed;\n}\n\nfunction count(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.min(999, Math.round(value))) : 0;\n}\n\nfunction instant(value: unknown): number | null {\n if (typeof value !== 'string' || !value.trim()) return null;\n const ms = Date.parse(value);\n return Number.isFinite(ms) ? ms : null;\n}\n\n/**\n * Checks and clamps a bridge payload into what the Live Activity shows.\n * Throws a plain sentence (the bridge's error message) when it cannot be shown.\n */\nexport function parseLiveActivityPayload(raw: unknown, now: number): WorkoutActivityState {\n const payload = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;\n const startedAt = instant(payload.startedAt);\n if (startedAt === null) throw new Error('startedAt must be an ISO date and time.');\n if (now - startedAt > MAX_WORKOUT_MS) throw new Error('That workout started too long ago to show.');\n const setsTotal = count(payload.setsTotal);\n const restEndsAt = instant(payload.restEndsAt);\n return {\n name: text(payload.name) ?? 'Workout',\n // A phone clock a little behind the page's must not show a negative time.\n startedAtMs: Math.min(startedAt, now),\n setsDone: Math.min(count(payload.setsDone), setsTotal),\n setsTotal,\n exercise: text(payload.exercise),\n restEndsAtMs: restEndsAt !== null && restEndsAt > now ? restEndsAt : null,\n };\n}\n\nexport interface HostLiveActivity {\n /** iOS 16.2+ with Live Activities allowed: only then does the host offer the actions. */\n supported(): boolean;\n start(payload: unknown): Promise<{ started: true; id: string }>;\n update(payload: unknown): Promise<{ updated: boolean }>;\n end(): Promise<{ ended: number }>;\n}\n\ntype LiveActivityNative = Pick<\n FitnessWidgetsNative,\n 'liveActivitiesEnabled' | 'startWorkoutActivity' | 'updateWorkoutActivity' | 'endWorkoutActivity'\n>;\n\nexport function createHostLiveActivity(\n native: LiveActivityNative | null,\n now: () => number = Date.now,\n): HostLiveActivity | null {\n if (!native) return null;\n let queue: Promise<unknown> = Promise.resolve();\n const serial = <T>(run: () => Promise<T>): Promise<T> => {\n const next = queue.then(run, run);\n queue = next.catch(() => undefined);\n return next;\n };\n const supported = () => {\n try {\n return native.liveActivitiesEnabled() === true;\n } catch {\n return false;\n }\n };\n const requireSupported = () => {\n if (!supported()) throw new Error('Live Activities are turned off or not available on this phone.');\n };\n return {\n supported,\n async start(payload) {\n const state = parseLiveActivityPayload(payload, now());\n requireSupported();\n return serial(async () => ({ started: true as const, id: await native.startWorkoutActivity(state) }));\n },\n async update(payload) {\n const state = parseLiveActivityPayload(payload, now());\n requireSupported();\n return serial(async () => ({ updated: await native.updateWorkoutActivity(state) }));\n },\n async end() {\n return serial(async () => ({ ended: await native.endWorkoutActivity() }));\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const ADVICE = {
|
|
2
|
+
push: {
|
|
3
|
+
title: "Ready to push",
|
|
4
|
+
detail: "Your body has recovered well. A good day for the hard session."
|
|
5
|
+
},
|
|
6
|
+
steady: {
|
|
7
|
+
title: "Steady day",
|
|
8
|
+
detail: "Recovered enough to train as planned. Keep the easy parts easy."
|
|
9
|
+
},
|
|
10
|
+
easy: {
|
|
11
|
+
title: "Take it easy",
|
|
12
|
+
detail: "Recovery is low. Swap hard work for an easy session or rest, and sleep early."
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const STATUS_ADVICE = {
|
|
16
|
+
sick: {
|
|
17
|
+
title: "Rest and recover",
|
|
18
|
+
detail: "No strain target while you are sick. Sleep, fluids, and only gentle movement if it helps."
|
|
19
|
+
},
|
|
20
|
+
injured: {
|
|
21
|
+
title: "Work around it",
|
|
22
|
+
detail: "No strain target while you heal. Keep moving what does not hurt."
|
|
23
|
+
},
|
|
24
|
+
break: {
|
|
25
|
+
title: "Enjoy the break",
|
|
26
|
+
detail: "No strain target while you are off. Walk, sleep well, and come back fresh."
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
function widgetPercent(value) {
|
|
30
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
31
|
+
return Math.round(Math.min(100, Math.max(0, value)));
|
|
32
|
+
}
|
|
33
|
+
function adviceFor(report) {
|
|
34
|
+
var _a;
|
|
35
|
+
const status = (_a = report.status) == null ? void 0 : _a.kind;
|
|
36
|
+
if (status && STATUS_ADVICE[status]) return STATUS_ADVICE[status];
|
|
37
|
+
const advice = typeof report.advice === "string" ? ADVICE[report.advice] : void 0;
|
|
38
|
+
return advice != null ? advice : null;
|
|
39
|
+
}
|
|
40
|
+
function buildWidgetSnapshot(report, opts) {
|
|
41
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
42
|
+
const advice = report ? adviceFor(report) : null;
|
|
43
|
+
const energy = (_a = report == null ? void 0 : report.stress) == null ? void 0 : _a.energy;
|
|
44
|
+
return {
|
|
45
|
+
v: 1,
|
|
46
|
+
date: opts.date,
|
|
47
|
+
strain: widgetPercent((_b = report == null ? void 0 : report.strain) == null ? void 0 : _b.value),
|
|
48
|
+
recovery: widgetPercent((_c = report == null ? void 0 : report.recovery) == null ? void 0 : _c.value),
|
|
49
|
+
sleep: widgetPercent((_d = report == null ? void 0 : report.sleep) == null ? void 0 : _d.value),
|
|
50
|
+
// The bank as it stands now; `start` is where the day began.
|
|
51
|
+
energy: widgetPercent((_e = energy == null ? void 0 : energy.end) != null ? _e : energy == null ? void 0 : energy.start),
|
|
52
|
+
advice: (_f = advice == null ? void 0 : advice.title) != null ? _f : null,
|
|
53
|
+
adviceDetail: (_g = advice == null ? void 0 : advice.detail) != null ? _g : null,
|
|
54
|
+
updatedAt: new Date(opts.now).toISOString()
|
|
55
|
+
};
|
|
56
|
+
}export{buildWidgetSnapshot,widgetPercent};//# sourceMappingURL=widgetSnapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widgetSnapshot.js","sources":["../../../src/features/fitness-widgets/widgetSnapshot.ts"],"sourcesContent":["/**\n * The compact snapshot the iOS Fitness widgets draw, built from the backend's\n * daily report (`fitnessHealth`). The widget extension decodes exactly this\n * shape (portable-devices/mobile/targets/widgets/FitnessSnapshot.swift), so a\n * field added here is ignored there until the Swift reads it.\n */\nimport type { FitnessDayReport } from '../health/fitnessClient';\n\nexport interface FitnessWidgetSnapshot {\n /** Shape version, for a future change the widget must tell apart. */\n v: 1;\n /** YYYY-MM-DD where the phone is: the day the numbers are for. */\n date: string;\n /** 0-100, or null when there is not enough data for the score. */\n strain: number | null;\n recovery: number | null;\n sleep: number | null;\n /** Energy Bank now, 0-100. */\n energy: number | null;\n /** The advice's headline, e.g. \"Ready to push\". */\n advice: string | null;\n /** A sentence or two under it. */\n adviceDetail: string | null;\n /** ISO time the app fetched the numbers. */\n updatedAt: string;\n}\n\n/**\n * After the Fitness surface's wording (yantra-surface fitness HealthCard), so the widget and the\n * page agree; a little shorter where the page's sentence would not fit the widget.\n */\nconst ADVICE: Record<string, { title: string; detail: string }> = {\n push: { title: 'Ready to push', detail: 'Your body has recovered well. A good day for the hard session.' },\n steady: { title: 'Steady day', detail: 'Recovered enough to train as planned. Keep the easy parts easy.' },\n easy: {\n title: 'Take it easy',\n detail: 'Recovery is low. Swap hard work for an easy session or rest, and sleep early.',\n },\n};\n\nconst STATUS_ADVICE: Record<string, { title: string; detail: string }> = {\n sick: {\n title: 'Rest and recover',\n detail: 'No strain target while you are sick. Sleep, fluids, and only gentle movement if it helps.',\n },\n injured: {\n title: 'Work around it',\n detail: 'No strain target while you heal. Keep moving what does not hurt.',\n },\n break: {\n title: 'Enjoy the break',\n detail: 'No strain target while you are off. Walk, sleep well, and come back fresh.',\n },\n};\n\n/** A 0-100 score, rounded; null for anything else. */\nexport function widgetPercent(value: unknown): number | null {\n if (typeof value !== 'number' || !Number.isFinite(value)) return null;\n return Math.round(Math.min(100, Math.max(0, value)));\n}\n\nfunction adviceFor(report: FitnessDayReport): { title: string; detail: string } | null {\n const status = report.status?.kind;\n if (status && STATUS_ADVICE[status]) return STATUS_ADVICE[status];\n const advice = typeof report.advice === 'string' ? ADVICE[report.advice] : undefined;\n return advice ?? null;\n}\n\nexport function buildWidgetSnapshot(\n report: FitnessDayReport | null,\n opts: { date: string; now: number },\n): FitnessWidgetSnapshot {\n const advice = report ? adviceFor(report) : null;\n const energy = report?.stress?.energy;\n return {\n v: 1,\n date: opts.date,\n strain: widgetPercent(report?.strain?.value),\n recovery: widgetPercent(report?.recovery?.value),\n sleep: widgetPercent(report?.sleep?.value),\n // The bank as it stands now; `start` is where the day began.\n energy: widgetPercent(energy?.end ?? energy?.start),\n advice: advice?.title ?? null,\n adviceDetail: advice?.detail ?? null,\n updatedAt: new Date(opts.now).toISOString(),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -54,6 +54,14 @@ const DELETE = (
|
|
|
54
54
|
}
|
|
55
55
|
`
|
|
56
56
|
);
|
|
57
|
+
const HEALTH = (
|
|
58
|
+
/* GraphQL */
|
|
59
|
+
`
|
|
60
|
+
query FitnessHealthForWidgets($date: String) {
|
|
61
|
+
fitnessHealth(date: $date)
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
);
|
|
57
65
|
function resolveFitnessGraphqlUrl(explicit, surfaceUrlTemplate) {
|
|
58
66
|
const override = (explicit || "").trim();
|
|
59
67
|
if (override) return override;
|
|
@@ -160,6 +168,13 @@ function createFitnessClient(opts) {
|
|
|
160
168
|
source
|
|
161
169
|
});
|
|
162
170
|
return (_a2 = data.deleteFitnessHealth) != null ? _a2 : 0;
|
|
171
|
+
},
|
|
172
|
+
async health(date) {
|
|
173
|
+
const data = await call(HEALTH, {
|
|
174
|
+
date
|
|
175
|
+
});
|
|
176
|
+
const report = data.fitnessHealth;
|
|
177
|
+
return report && typeof report === "object" ? report : null;
|
|
163
178
|
}
|
|
164
179
|
};
|
|
165
180
|
}export{FitnessApiError,createFitnessClient,resolveFitnessGraphqlUrl};//# sourceMappingURL=fitnessClient.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fitnessClient.js","sources":["../../../src/features/health/fitnessClient.ts"],"sourcesContent":["/**\n * The Fitness backend (fitness-subgraph-server) calls health sync needs:\n * who the person is (birth date for heart-rate zones), ingest, and delete a\n * source. Plain fetch, bearer = the platform system JWT, a deadline on every\n * call, and failures classified so the screen can say what went wrong.\n */\nimport type { DeviceHealthSource, HealthDay } from './types';\n\nexport type FitnessErrorKind = 'auth' | 'network' | 'timeout' | 'unavailable' | 'rejected';\n\nexport class FitnessApiError extends Error {\n constructor(\n readonly kind: FitnessErrorKind,\n message: string,\n ) {\n super(message);\n this.name = 'FitnessApiError';\n }\n}\n\nexport interface FitnessPerson {\n /** YYYY-MM-DD from the saved profile, else from the account. */\n birthDate?: string;\n timezone?: string;\n /** What a body-composition scale needs to compute body fat (drivers.ts). */\n sex?: 'male' | 'female' | 'unspecified';\n heightCm?: number;\n units?: 'metric' | 'imperial';\n}\n\nexport interface IngestResult {\n stored: number;\n dropped: string[];\n}\n\nexport interface FitnessClient {\n me(): Promise<FitnessPerson>;\n ingest(days: HealthDay[]): Promise<IngestResult>;\n /** `device`: every reading from Bluetooth devices paired in this app. */\n deleteSource(source: DeviceHealthSource | 'device'): Promise<number>;\n}\n\nexport interface FitnessClientOptions {\n url: string;\n token: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n}\n\nconst ME = /* GraphQL */ `\n query FitnessMeForHealthSync {\n fitnessMe {\n account {\n birthDate\n sex\n }\n profile\n }\n }\n`;\n\nconst INGEST = /* GraphQL */ `\n mutation IngestFitnessHealth($days: JSON!) {\n ingestFitnessHealth(days: $days) {\n stored\n dropped\n }\n }\n`;\n\nconst DELETE = /* GraphQL */ `\n mutation DeleteFitnessHealth($source: String!) {\n deleteFitnessHealth(source: $source)\n }\n`;\n\n/**\n * Where the Fitness backend lives: an explicit URL wins; otherwise it is the\n * `fitness-backend` sibling of the deployed surfaces, read off\n * SURFACE_URL_TEMPLATE (`https://{slug}-surface.yantra-app-v1.cdebase.dev/`\n * -> `https://fitness-backend.yantra-app-v1.cdebase.dev/graphql`; prod\n * `https://{slug}-surface.yantra.app/` -> `https://fitness-backend.yantra.app/graphql`).\n */\nexport function resolveFitnessGraphqlUrl(explicit: string | undefined, surfaceUrlTemplate: string): string {\n const override = (explicit || '').trim();\n if (override) return override;\n const match = (surfaceUrlTemplate || '').trim().match(/^(https?:)\\/\\/\\{slug\\}-surface\\.([^/]+)/i);\n if (!match) return '';\n return `${match[1]}//fitness-backend.${match[2]}/graphql`;\n}\n\nfunction isAuthError(err: { message?: string; extensions?: { code?: string } } | undefined): boolean {\n if (!err) return false;\n return err.extensions?.code === 'UNAUTHENTICATED' || /unauthori[sz]ed/i.test(err.message ?? '');\n}\n\nexport function createFitnessClient(opts: FitnessClientOptions): FitnessClient {\n const doFetch = opts.fetchImpl ?? fetch;\n const timeoutMs = opts.timeoutMs ?? 30_000;\n\n async function call<T>(query: string, variables?: Record<string, unknown>): Promise<T> {\n if (!opts.url) throw new FitnessApiError('unavailable', 'The Fitness service address is not configured.');\n const controller = typeof AbortController === 'function' ? new AbortController() : undefined;\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n controller?.abort();\n }, timeoutMs);\n let res: Response;\n try {\n res = await doFetch(opts.url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', authorization: `Bearer ${opts.token}` },\n body: JSON.stringify({ query, variables }),\n signal: controller?.signal,\n });\n } catch (err) {\n if (timedOut) throw new FitnessApiError('timeout', 'The Fitness service took too long to answer.');\n throw new FitnessApiError(\n 'network',\n `Could not reach the Fitness service${err instanceof Error && err.message ? ` (${err.message})` : ''}.`,\n );\n } finally {\n clearTimeout(timer);\n }\n let body: { data?: T; errors?: Array<{ message?: string; extensions?: { code?: string } }> } | null = null;\n try {\n body = (await res.json()) as typeof body;\n } catch {\n body = null;\n }\n const firstError = body?.errors?.[0];\n if (res.status === 401 || res.status === 403 || isAuthError(firstError)) {\n throw new FitnessApiError('auth', 'Your sign-in was not accepted by the Fitness service.');\n }\n if (firstError) {\n const code = firstError.extensions?.code;\n if (code === 'GRAPHQL_VALIDATION_FAILED' || /Cannot query field/i.test(firstError.message ?? '')) {\n throw new FitnessApiError('unavailable', 'The Fitness service does not take health data yet.');\n }\n throw new FitnessApiError('rejected', firstError.message || 'The Fitness service refused the request.');\n }\n if (!res.ok || !body?.data) {\n throw new FitnessApiError(\n 'unavailable',\n `The Fitness service answered with an error (HTTP ${res.status}).`,\n );\n }\n return body.data;\n }\n\n return {\n async me() {\n const data = await call<{\n fitnessMe?: {\n account?: { birthDate?: string | null; sex?: string | null } | null;\n profile?: Record<string, unknown> | null;\n };\n }>(ME);\n const profile = data.fitnessMe?.profile ?? null;\n const profileBirth = typeof profile?.birthDate === 'string' ? profile.birthDate : undefined;\n const accountBirth = data.fitnessMe?.account?.birthDate ?? undefined;\n const timezone = typeof profile?.timezone === 'string' ? profile.timezone : undefined;\n const sexRaw = (typeof profile?.sex === 'string' ? profile.sex : undefined) ?? data.fitnessMe?.account?.sex;\n const sex = sexRaw === 'male' || sexRaw === 'female' || sexRaw === 'unspecified' ? sexRaw : undefined;\n const heightCm =\n typeof profile?.heightCm === 'number' && Number.isFinite(profile.heightCm)\n ? profile.heightCm\n : undefined;\n const units = profile?.units === 'imperial' || profile?.units === 'metric' ? profile.units : undefined;\n return {\n birthDate: profileBirth || accountBirth || undefined,\n timezone,\n ...(sex ? { sex } : {}),\n ...(heightCm ? { heightCm } : {}),\n ...(units ? { units } : {}),\n };\n },\n async ingest(days) {\n const data = await call<{ ingestFitnessHealth?: { stored?: number; dropped?: string[] } }>(INGEST, {\n days,\n });\n return {\n stored: data.ingestFitnessHealth?.stored ?? 0,\n dropped: data.ingestFitnessHealth?.dropped ?? [],\n };\n },\n async deleteSource(source) {\n const data = await call<{ deleteFitnessHealth?: number }>(DELETE, { source });\n return data.deleteFitnessHealth ?? 0;\n },\n };\n}\n"],"names":["_a","_b"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fitnessClient.js","sources":["../../../src/features/health/fitnessClient.ts"],"sourcesContent":["/**\n * The Fitness backend (fitness-subgraph-server) calls health sync needs:\n * who the person is (birth date for heart-rate zones), ingest, and delete a\n * source - and the day's report the home-screen widgets draw. Plain fetch, bearer = the platform system JWT, a deadline on every\n * call, and failures classified so the screen can say what went wrong.\n */\nimport type { DeviceHealthSource, HealthDay } from './types';\n\nexport type FitnessErrorKind = 'auth' | 'network' | 'timeout' | 'unavailable' | 'rejected';\n\nexport class FitnessApiError extends Error {\n constructor(\n readonly kind: FitnessErrorKind,\n message: string,\n ) {\n super(message);\n this.name = 'FitnessApiError';\n }\n}\n\nexport interface FitnessPerson {\n /** YYYY-MM-DD from the saved profile, else from the account. */\n birthDate?: string;\n timezone?: string;\n /** What a body-composition scale needs to compute body fat (drivers.ts). */\n sex?: 'male' | 'female' | 'unspecified';\n heightCm?: number;\n units?: 'metric' | 'imperial';\n}\n\nexport interface IngestResult {\n stored: number;\n dropped: string[];\n}\n\n/**\n * The parts of the backend's daily report (`fitnessHealth`) the app reads.\n * Scores are 0-100, null when there is not enough data for them.\n */\nexport interface FitnessDayReport {\n date?: string;\n strain?: { value?: number | null } | null;\n recovery?: { value?: number | null } | null;\n sleep?: { value?: number | null } | null;\n /** push | steady | easy, or null without a Recovery score. */\n advice?: string | null;\n /** active | sick | injured | break */\n status?: { kind?: string | null } | null;\n /** Today's stress and Energy Bank; null without a resting heart rate baseline. */\n stress?: { average?: number | null; energy?: { start?: number | null; end?: number | null } | null } | null;\n day?: { steps?: number | null } | null;\n}\n\nexport interface FitnessClient {\n me(): Promise<FitnessPerson>;\n ingest(days: HealthDay[]): Promise<IngestResult>;\n /** `device`: every reading from Bluetooth devices paired in this app. */\n deleteSource(source: DeviceHealthSource | 'device'): Promise<number>;\n /** The day's report (YYYY-MM-DD where the phone is); null when the backend has none. */\n health(date: string): Promise<FitnessDayReport | null>;\n}\n\nexport interface FitnessClientOptions {\n url: string;\n token: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n}\n\nconst ME = /* GraphQL */ `\n query FitnessMeForHealthSync {\n fitnessMe {\n account {\n birthDate\n sex\n }\n profile\n }\n }\n`;\n\nconst INGEST = /* GraphQL */ `\n mutation IngestFitnessHealth($days: JSON!) {\n ingestFitnessHealth(days: $days) {\n stored\n dropped\n }\n }\n`;\n\nconst DELETE = /* GraphQL */ `\n mutation DeleteFitnessHealth($source: String!) {\n deleteFitnessHealth(source: $source)\n }\n`;\n\nconst HEALTH = /* GraphQL */ `\n query FitnessHealthForWidgets($date: String) {\n fitnessHealth(date: $date)\n }\n`;\n\n/**\n * Where the Fitness backend lives: an explicit URL wins; otherwise it is the\n * `fitness-backend` sibling of the deployed surfaces, read off\n * SURFACE_URL_TEMPLATE (`https://{slug}-surface.yantra-app-v1.cdebase.dev/`\n * -> `https://fitness-backend.yantra-app-v1.cdebase.dev/graphql`; prod\n * `https://{slug}-surface.yantra.app/` -> `https://fitness-backend.yantra.app/graphql`).\n */\nexport function resolveFitnessGraphqlUrl(explicit: string | undefined, surfaceUrlTemplate: string): string {\n const override = (explicit || '').trim();\n if (override) return override;\n const match = (surfaceUrlTemplate || '').trim().match(/^(https?:)\\/\\/\\{slug\\}-surface\\.([^/]+)/i);\n if (!match) return '';\n return `${match[1]}//fitness-backend.${match[2]}/graphql`;\n}\n\nfunction isAuthError(err: { message?: string; extensions?: { code?: string } } | undefined): boolean {\n if (!err) return false;\n return err.extensions?.code === 'UNAUTHENTICATED' || /unauthori[sz]ed/i.test(err.message ?? '');\n}\n\nexport function createFitnessClient(opts: FitnessClientOptions): FitnessClient {\n const doFetch = opts.fetchImpl ?? fetch;\n const timeoutMs = opts.timeoutMs ?? 30_000;\n\n async function call<T>(query: string, variables?: Record<string, unknown>): Promise<T> {\n if (!opts.url) throw new FitnessApiError('unavailable', 'The Fitness service address is not configured.');\n const controller = typeof AbortController === 'function' ? new AbortController() : undefined;\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n controller?.abort();\n }, timeoutMs);\n let res: Response;\n try {\n res = await doFetch(opts.url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', authorization: `Bearer ${opts.token}` },\n body: JSON.stringify({ query, variables }),\n signal: controller?.signal,\n });\n } catch (err) {\n if (timedOut) throw new FitnessApiError('timeout', 'The Fitness service took too long to answer.');\n throw new FitnessApiError(\n 'network',\n `Could not reach the Fitness service${err instanceof Error && err.message ? ` (${err.message})` : ''}.`,\n );\n } finally {\n clearTimeout(timer);\n }\n let body: { data?: T; errors?: Array<{ message?: string; extensions?: { code?: string } }> } | null = null;\n try {\n body = (await res.json()) as typeof body;\n } catch {\n body = null;\n }\n const firstError = body?.errors?.[0];\n if (res.status === 401 || res.status === 403 || isAuthError(firstError)) {\n throw new FitnessApiError('auth', 'Your sign-in was not accepted by the Fitness service.');\n }\n if (firstError) {\n const code = firstError.extensions?.code;\n if (code === 'GRAPHQL_VALIDATION_FAILED' || /Cannot query field/i.test(firstError.message ?? '')) {\n throw new FitnessApiError('unavailable', 'The Fitness service does not take health data yet.');\n }\n throw new FitnessApiError('rejected', firstError.message || 'The Fitness service refused the request.');\n }\n if (!res.ok || !body?.data) {\n throw new FitnessApiError(\n 'unavailable',\n `The Fitness service answered with an error (HTTP ${res.status}).`,\n );\n }\n return body.data;\n }\n\n return {\n async me() {\n const data = await call<{\n fitnessMe?: {\n account?: { birthDate?: string | null; sex?: string | null } | null;\n profile?: Record<string, unknown> | null;\n };\n }>(ME);\n const profile = data.fitnessMe?.profile ?? null;\n const profileBirth = typeof profile?.birthDate === 'string' ? profile.birthDate : undefined;\n const accountBirth = data.fitnessMe?.account?.birthDate ?? undefined;\n const timezone = typeof profile?.timezone === 'string' ? profile.timezone : undefined;\n const sexRaw = (typeof profile?.sex === 'string' ? profile.sex : undefined) ?? data.fitnessMe?.account?.sex;\n const sex = sexRaw === 'male' || sexRaw === 'female' || sexRaw === 'unspecified' ? sexRaw : undefined;\n const heightCm =\n typeof profile?.heightCm === 'number' && Number.isFinite(profile.heightCm)\n ? profile.heightCm\n : undefined;\n const units = profile?.units === 'imperial' || profile?.units === 'metric' ? profile.units : undefined;\n return {\n birthDate: profileBirth || accountBirth || undefined,\n timezone,\n ...(sex ? { sex } : {}),\n ...(heightCm ? { heightCm } : {}),\n ...(units ? { units } : {}),\n };\n },\n async ingest(days) {\n const data = await call<{ ingestFitnessHealth?: { stored?: number; dropped?: string[] } }>(INGEST, {\n days,\n });\n return {\n stored: data.ingestFitnessHealth?.stored ?? 0,\n dropped: data.ingestFitnessHealth?.dropped ?? [],\n };\n },\n async deleteSource(source) {\n const data = await call<{ deleteFitnessHealth?: number }>(DELETE, { source });\n return data.deleteFitnessHealth ?? 0;\n },\n async health(date) {\n const data = await call<{ fitnessHealth?: unknown }>(HEALTH, { date });\n const report = data.fitnessHealth;\n return report && typeof report === 'object' ? (report as FitnessDayReport) : null;\n },\n };\n}\n"],"names":["_a","_b"],"mappings":"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"}
|
package/lib/hooks/useChatApi.js
CHANGED
|
@@ -276,8 +276,9 @@ function cleanChannelTitle(rawTitle) {
|
|
|
276
276
|
return t;
|
|
277
277
|
}
|
|
278
278
|
const ACTIVE_CONNECTORS_PREFIX_RE = /^\s*\[Active connectors:[^\]]*\]\s*/i;
|
|
279
|
+
const SURFACE_CONTEXT_HEADER_RE = /^\s*\[[a-z][a-z ]{0,40} context:[^\]\n]*\]\s*/i;
|
|
279
280
|
function cleanMessageText(raw) {
|
|
280
|
-
return raw.replace(ACTIVE_CONNECTORS_PREFIX_RE, "").replace(/\s+/g, " ").trim();
|
|
281
|
+
return raw.replace(SURFACE_CONTEXT_HEADER_RE, "").replace(ACTIVE_CONNECTORS_PREFIX_RE, "").replace(/\s+/g, " ").trim();
|
|
281
282
|
}
|
|
282
283
|
function isTransientHistoryText(text) {
|
|
283
284
|
if (isConnectorReply(text)) return true;
|