@hipnation-truth/sdk 0.7.2 → 0.7.3
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/dist/react.js +1 -16
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +7 -6
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use strict";
|
|
3
|
-
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
5
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
7
|
var __pow = Math.pow;
|
|
10
8
|
var __export = (target, all) => {
|
|
@@ -19,14 +17,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
19
17
|
}
|
|
20
18
|
return to;
|
|
21
19
|
};
|
|
22
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
23
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
24
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
25
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
26
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
27
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
28
|
-
mod
|
|
29
|
-
));
|
|
30
20
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
31
21
|
var __async = (__this, __arguments, generator) => {
|
|
32
22
|
return new Promise((resolve, reject) => {
|
|
@@ -403,12 +393,7 @@ function urlBase64ToUint8Array(base64String) {
|
|
|
403
393
|
function loadExpo() {
|
|
404
394
|
return __async(this, null, function* () {
|
|
405
395
|
try {
|
|
406
|
-
return
|
|
407
|
-
/* @vite-ignore */
|
|
408
|
-
/* webpackIgnore: true */
|
|
409
|
-
// @ts-ignore — expo-notifications is an optional peer dep
|
|
410
|
-
"expo-notifications"
|
|
411
|
-
);
|
|
396
|
+
return require("expo-notifications");
|
|
412
397
|
} catch (e) {
|
|
413
398
|
return null;
|
|
414
399
|
}
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react.ts","../src/react/conversations.ts","../src/react/hooks.ts","../src/react/notifications.ts","../src/web-push.ts","../src/react/provider.ts","../src/react/tracking.ts","../src/tracking/tracker.ts"],"sourcesContent":["/**\n * @hipnation-truth/sdk/react\n *\n * React hooks and provider for Truth SDK — real-time Convex-backed data.\n *\n * @example\n * ```tsx\n * import { TruthProvider, usePatients, useAppointments } from '@hipnation-truth/sdk/react';\n *\n * function App() {\n * return (\n * <TruthProvider environment=\"sandbox\">\n * <PatientList />\n * </TruthProvider>\n * );\n * }\n *\n * function PatientList() {\n * const patients = usePatients({ limit: 20 });\n * if (!patients) return <div>Loading...</div>;\n * return patients.map(p => <div key={p._id}>{p.firstName}</div>);\n * }\n * ```\n */\n\n// Reactive conversation / message hooks (Convex-backed, PR #110 schema)\nexport type {\n ConversationListItem,\n ConversationMessageRow,\n ConversationRow,\n UseConversationsFilters,\n UseMessagesOptions,\n UseQueryResult,\n} from \"./react/conversations\";\nexport {\n useConversationByPhonePair,\n useConversations,\n useMessages,\n useUnreadCount,\n} from \"./react/conversations\";\n// Re-export types consumers commonly need\nexport type {\n ConversationMessage,\n Physician,\n UseAppointmentListOptions,\n UseConversationMessagesOptions,\n UsePatientBasicOptions,\n UsePatientBasicResult,\n UsePatientListOptions,\n UsePatientMedicalOptions,\n UsePatientPhotoOptions,\n} from \"./react/hooks\";\n// Patient / Appointment / Physician / Medical / Messages hooks\nexport {\n useAppointment,\n useAppointmentByElationId,\n useAppointments,\n useConversationMessages,\n usePatient,\n usePatientBasic,\n usePatientByElationId,\n usePatientByHintId,\n usePatientMedical,\n usePatientPhoto,\n usePatients,\n usePharmacyByNcpdpId,\n usePhysicianByElationId,\n usePhysiciansByElationIds,\n} from \"./react/hooks\";\n// Notifications\nexport type {\n PermissionStatus,\n UseNotificationsOptions,\n UseNotificationsResult,\n} from \"./react/notifications\";\nexport { useNotifications } from \"./react/notifications\";\nexport type { TruthProviderProps } from \"./react/provider\";\n// Provider\nexport { TruthProvider } from \"./react/provider\";\nexport type {\n TruthTrackingContextValue,\n TruthTrackingProviderProps,\n} from \"./react/tracking\";\n// Tracking\nexport { TruthTrackingProvider, useTruth } from \"./react/tracking\";\n// Re-export event types for track() calls\nexport type {\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"./tracking/events\";\nexport type { Appointment, AppointmentListOptions } from \"./types/appointment\";\nexport type { Patient, PatientListOptions } from \"./types/patient\";\n","/**\n * React hooks for Truth SDK — reactive conversation + message data.\n *\n * Wraps Convex queries owned by the Truth backend so consumers (CommHub\n * Expo frontend) get live-updating conversation lists, message threads,\n * and unread totals without managing subscriptions themselves.\n *\n * **Hook contract:** every hook in this file returns\n * `{ data, loading, error }`. `data` is `undefined` while loading and\n * also when the underlying query is `\"skip\"`'d (caller didn't pass the\n * required arg yet). `loading` is `true` only while a real subscription\n * is in-flight; once `data` resolves (even to `null`) `loading` flips\n * to `false`. `error` is reserved for SDK-side validation — Convex\n * itself throws on subscribe rather than returning an error value, so\n * unhandled query errors propagate as React errors and should be caught\n * with an error boundary.\n *\n * **Backed by PR #110 schema (commit `41dbb59` on\n * `feat/migrate-dialpad-webhook-and-messages-to-truth`):**\n * - `conversations:listForUser` → `useConversations`\n * - `conversations:getUnreadTotalForUser` → `useUnreadCount`\n * - `conversations:getByPhonePair` → `useConversationByPhonePair`\n * - `conversationMessages:getByConversationId` → `useMessages`\n *\n * **Deliberately NOT shipped here (no backend query yet — flagged in\n * PR #111 body for follow-up):**\n * - Single-conversation-by-id lookup. Convex `conversations` has no\n * `get(id)` query — only `getByPhonePair`. CommHub uses phonePair as\n * the primary handle, so the byPair flavor is what consumers need;\n * if id-based lookup is needed later we can add a `conversations:get`\n * in Truth.\n * - Participants / \"seen by\" hook. The migrated schema has no\n * `conversationParticipants` table — read state is per-user via\n * `conversationReads` and there's no public list query for it yet.\n * - `markRead` mutation. PR #110 declares `markRead` as\n * `internalMutation`, so the frontend can't invoke it directly — it\n * has to go through a Truth HTTP endpoint, or webhook-migrator needs\n * to flip it to a public `mutation`. Flagged for a follow-up.\n *\n * Must be used within `<TruthProvider />` (see `./provider`).\n *\n * @example\n * ```tsx\n * import {\n * useConversations,\n * useConversationByPhonePair,\n * useMessages,\n * useUnreadCount,\n * } from '@hipnation-truth/sdk/react';\n *\n * function Inbox({ userId }: { userId: string }) {\n * const { data: convos, loading } = useConversations({ userId });\n * const { data: unread } = useUnreadCount(userId);\n * if (loading) return <Spinner />;\n * return (\n * <div>\n * <Badge count={unread ?? 0} />\n * {convos?.map((c) => <ConvoRow key={c.id} convo={c} />)}\n * </div>\n * );\n * }\n * ```\n */\n\n\"use client\";\n\nimport { useQuery } from \"convex/react\";\nimport type { FunctionReference } from \"convex/server\";\nimport { makeFunctionReference } from \"convex/server\";\n\n// ---------------------------------------------------------------------------\n// Shared types\n// ---------------------------------------------------------------------------\n//\n// Mirror the shapes returned by the Convex queries on PR #110. Once\n// the consuming app regenerates `convex/_generated/api.d.ts` against\n// the merged schema these can be replaced with inferred types — for\n// today they give consumers useful autocomplete.\n\n/**\n * Row shape returned by `conversations:listForUser` — joins the base\n * `conversations` row with the caller's per-user `conversationReads`\n * entry so unread state lives on each item.\n */\nexport interface ConversationListItem {\n /** Convex id of the conversation row. */\n id: string;\n patientPhone: string;\n providerPhone: string;\n /** Sorted-digit-pair key (commutative — same regardless of direction). */\n phonePair: string;\n /** Truth-side patient id, when the kinesis pipeline could resolve it. */\n patientId: string | null;\n /** ISO timestamp of the most recent message in the conversation. */\n lastMessageAt: string;\n /** Caller's unread count for this conversation. */\n unreadCount: number;\n /** ISO timestamp of when the caller last marked the conversation read. */\n lastReadAt: string | null;\n}\n\n/**\n * Raw `conversations` table row — what `getByPhonePair` returns. No\n * unread / read joining; for that, prefer `useConversations`.\n */\nexport interface ConversationRow {\n _id: string;\n _creationTime: number;\n patientPhone: string;\n providerPhone: string;\n phonePair: string;\n patientId: string | null;\n lastEventId: string | null;\n lastMessageAt: string;\n createdAt: string;\n}\n\n/**\n * Message row returned by `conversationMessages:getByConversationId` —\n * calls + SMS merged chronologically. Mirrors `ConversationMessage`\n * already exported from the SDK; re-exported here under a clearer name\n * for the new hook surface.\n */\nexport interface ConversationMessageRow {\n kind: \"call\" | \"sms\";\n id: string;\n providerId: string;\n state: string | null;\n direction: string | null;\n fromNumber: string | null;\n toNumber: string | null;\n voicemailLink: string | null;\n duration: number | null;\n text: string | null;\n mms: boolean;\n mmsUrl: string | null;\n messageStatus: string | null;\n occurredAt: string;\n conversationId: string | null;\n patientId: string | null;\n}\n\nexport interface UseQueryResult<T> {\n /**\n * Query data. `undefined` while loading or while the query is\n * intentionally skipped (e.g. caller hasn't supplied `userId` yet).\n */\n data: T | undefined;\n /** True only while a real subscription is in-flight. */\n loading: boolean;\n /**\n * Reserved for client-side validation errors surfaced by the SDK\n * itself. Convex query errors propagate as React errors and should\n * be caught with an error boundary.\n */\n error: Error | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Convex function references — string-keyed, decoupled from the\n// consuming app's generated `convex/_generated/api`. Names below match\n// the queries on PR #110 (commit 41dbb59) verbatim.\n// ---------------------------------------------------------------------------\n\nconst conversationsListForUserRef = makeFunctionReference<\n \"query\",\n { userId: string; limit?: number },\n ConversationListItem[]\n>(\"conversations:listForUser\");\n\nconst conversationsGetUnreadTotalForUserRef = makeFunctionReference<\n \"query\",\n { userId: string },\n number\n>(\"conversations:getUnreadTotalForUser\");\n\nconst conversationsGetByPhonePairRef = makeFunctionReference<\n \"query\",\n { phonePair: string },\n ConversationRow | null\n>(\"conversations:getByPhonePair\");\n\n// Lives on the existing `conversationMessages` module (pre-#110). PR\n// #110 added `conversationId` indexing on `messageSms` / `messageCalls`,\n// so this query is now reliable for the new conversations table.\nconst conversationMessagesGetByConversationIdRef = makeFunctionReference<\n \"query\",\n { conversationId: string; limit?: number },\n ConversationMessageRow[]\n>(\"conversationMessages:getByConversationId\");\n\n// ---------------------------------------------------------------------------\n// Skipped-query sentinel + result shaping\n// ---------------------------------------------------------------------------\n\nconst SKIP = \"skip\" as const;\n\nfunction toResult<T>(\n value: T | undefined,\n skipped: boolean,\n): UseQueryResult<T> {\n if (skipped) {\n return { data: undefined, loading: false, error: undefined };\n }\n return {\n data: value,\n loading: value === undefined,\n error: undefined,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Filters + options\n// ---------------------------------------------------------------------------\n\nexport interface UseConversationsFilters {\n /**\n * Truth user id (the Better Auth subject). Pass `undefined` to skip\n * the query — useful when the auth session is still loading.\n */\n userId: string | null | undefined;\n /** Page size. Server caps at 100 by default. */\n limit?: number;\n}\n\nexport interface UseMessagesOptions {\n /** Page size. Server caps at 200 by default. */\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to a user's conversations — joined with their per-conversation\n * unread count, sorted by most recent message. Updates live as new\n * SMS / calls land in Convex and the webhook bumps `unreadCount`.\n *\n * Returns `{ data: undefined, loading: false }` while the auth session\n * resolves (`userId === undefined`); not an error, just a skip.\n */\nfunction useConversations(\n filters: UseConversationsFilters,\n): UseQueryResult<ConversationListItem[]> {\n const skipped = !filters.userId;\n const result = useQuery(\n conversationsListForUserRef as FunctionReference<\"query\">,\n skipped\n ? SKIP\n : {\n userId: filters.userId as string,\n limit: filters.limit,\n },\n ) as ConversationListItem[] | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Look up a single conversation by its normalized phonePair (sorted\n * digit-pair, e.g. `\"5125550123|6505551234\"`). Returns `null` if no\n * conversation exists yet for that pair.\n *\n * Use the `phonePairKey(patientPhone, providerPhone)` helper exposed by\n * the Truth Convex module to derive the key from raw phone strings.\n */\nfunction useConversationByPhonePair(\n phonePair: string | null | undefined,\n): UseQueryResult<ConversationRow | null> {\n const skipped = !phonePair;\n const result = useQuery(\n conversationsGetByPhonePairRef as FunctionReference<\"query\">,\n skipped ? SKIP : { phonePair: phonePair as string },\n ) as ConversationRow | null | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Subscribe to a paginated message stream for a single conversation.\n * Calls + SMS merged chronologically, newest-first. Live as the\n * kinesis consumer writes to `messageCalls` / `messageSms`.\n *\n * Pass the Convex `_id` of the conversation row (from\n * `useConversations` / `useConversationByPhonePair`) as\n * `conversationId`.\n */\nfunction useMessages(\n conversationId: string | null | undefined,\n options?: UseMessagesOptions,\n): UseQueryResult<ConversationMessageRow[]> {\n const skipped = !conversationId;\n const result = useQuery(\n conversationMessagesGetByConversationIdRef as FunctionReference<\"query\">,\n skipped\n ? SKIP\n : {\n conversationId: conversationId as string,\n limit: options?.limit,\n },\n ) as ConversationMessageRow[] | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Subscribe to the user's total unread message count across all\n * conversations. Backs the inbox tab badge in CommHub.\n *\n * Returns `0` when the underlying query resolves with no unread\n * messages, `undefined` while loading or when `userId` is missing.\n */\nfunction useUnreadCount(\n userId: string | null | undefined,\n): UseQueryResult<number> {\n const skipped = !userId;\n const result = useQuery(\n conversationsGetUnreadTotalForUserRef as FunctionReference<\"query\">,\n skipped ? SKIP : { userId: userId as string },\n ) as number | undefined;\n\n return toResult(result, skipped);\n}\n\nexport {\n useConversationByPhonePair,\n useConversations,\n useMessages,\n useUnreadCount,\n};\n","/**\n * React hooks for Truth SDK — real-time Convex-backed data access.\n *\n * These hooks use Convex React subscriptions for live-updating data.\n * Must be used within a ConvexProvider (see TruthProvider).\n *\n * @example\n * ```tsx\n * import { usePatients, useAppointments } from '@hipnation-truth/sdk/react';\n *\n * function PatientList() {\n * const patients = usePatients({ limit: 20 });\n * return patients?.map(p => <div key={p._id}>{p.firstName} {p.lastName}</div>);\n * }\n * ```\n */\n\n\"use client\";\n\nimport { useQuery } from \"convex/react\";\nimport { useEffect } from \"react\";\n\n// ---------------------------------------------------------------------------\n// Convex function references\n// ---------------------------------------------------------------------------\n// We use string-based references to avoid importing the generated API\n// (which lives in the consuming app, not the SDK). Convex supports this\n// via the `api` module pattern, but for a published SDK we use\n// makeFunctionReference to stay decoupled from the app's codegen.\n\nimport type { FunctionReference } from \"convex/server\";\nimport { makeFunctionReference } from \"convex/server\";\n\n// Patient query references\nconst patientsListRef = makeFunctionReference<\n \"query\",\n {\n search?: string;\n lastName?: string;\n cursor?: string;\n limit?: number;\n },\n unknown[]\n>(\"patients:list\");\n\nconst patientsGetRef = makeFunctionReference<\n \"query\",\n { id: string },\n unknown | null\n>(\"patients:get\");\n\nconst patientsByElationIdRef = makeFunctionReference<\n \"query\",\n { elationId: string },\n unknown | null\n>(\"patients:getByElationId\");\n\nconst patientsByHintIdRef = makeFunctionReference<\n \"query\",\n { hintId: string },\n unknown | null\n>(\"patients:getByHintId\");\n\n// Appointment query references\nconst appointmentsListRef = makeFunctionReference<\n \"query\",\n {\n patientId?: string;\n status?: string;\n startDate?: string;\n endDate?: string;\n limit?: number;\n },\n unknown[]\n>(\"appointments:list\");\n\nconst appointmentsGetRef = makeFunctionReference<\n \"query\",\n { id: string },\n unknown | null\n>(\"appointments:get\");\n\nconst appointmentsByElationIdRef = makeFunctionReference<\n \"query\",\n { elationId: string },\n unknown | null\n>(\"appointments:getByElationId\");\n\n// ---------------------------------------------------------------------------\n// Patient hooks\n// ---------------------------------------------------------------------------\n\ninterface UsePatientListOptions {\n search?: string;\n lastName?: string;\n limit?: number;\n}\n\n/**\n * Subscribe to a list of patients with optional search and filtering.\n * Returns undefined while loading, then an array of patients.\n */\nfunction usePatients(options?: UsePatientListOptions) {\n return useQuery(patientsListRef as FunctionReference<\"query\">, options ?? {});\n}\n\n/**\n * Subscribe to a single patient by Convex document ID.\n */\nfunction usePatient(id: string) {\n return useQuery(patientsGetRef as FunctionReference<\"query\">, { id });\n}\n\n/**\n * Subscribe to a patient by their Elation ID.\n */\nfunction usePatientByElationId(elationId: string) {\n return useQuery(patientsByElationIdRef as FunctionReference<\"query\">, {\n elationId,\n });\n}\n\n/**\n * Subscribe to a patient by their Hint ID.\n */\nfunction usePatientByHintId(hintId: string) {\n return useQuery(patientsByHintIdRef as FunctionReference<\"query\">, {\n hintId,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Appointment hooks\n// ---------------------------------------------------------------------------\n\ninterface UseAppointmentListOptions {\n patientId?: string;\n status?: string;\n startDate?: string;\n endDate?: string;\n limit?: number;\n}\n\n/**\n * Subscribe to a list of appointments with optional filtering.\n */\nfunction useAppointments(options?: UseAppointmentListOptions) {\n return useQuery(\n appointmentsListRef as FunctionReference<\"query\">,\n options ?? {},\n );\n}\n\n/**\n * Subscribe to a single appointment by Convex document ID.\n */\nfunction useAppointment(id: string) {\n return useQuery(appointmentsGetRef as FunctionReference<\"query\">, { id });\n}\n\n/**\n * Subscribe to an appointment by its Elation ID.\n */\nfunction useAppointmentByElationId(elationId: string) {\n return useQuery(appointmentsByElationIdRef as FunctionReference<\"query\">, {\n elationId,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Physician hooks\n// ---------------------------------------------------------------------------\n\ninterface Physician {\n _id: string;\n elationId: number;\n firstName?: string;\n lastName?: string;\n npi?: string;\n credentials?: string;\n specialties?: string[];\n practice?: number;\n email?: string;\n phone?: string;\n lastSyncedAt: string;\n}\n\nconst physiciansGetByElationIdsRef = makeFunctionReference<\n \"query\",\n { ids: number[] },\n Physician[]\n>(\"physicians:getByElationIds\");\n\nconst physiciansGetByElationIdRef = makeFunctionReference<\n \"query\",\n { id: number },\n Physician | null\n>(\"physicians:getByElationId\");\n\n/**\n * Resolve a batch of physicians by their Elation IDs. Returns an array\n * of physicians that exist in Convex; missing ids are silently dropped.\n *\n * Use this to resolve medication `prescribing_physician` / appointment\n * physician ids to display names, avoiding the per-physician HTTP\n * round-trip through Truth's Elation proxy.\n *\n * Pass `undefined` (or an empty array) to skip the query — the hook\n * returns `undefined` until you pass a populated list.\n */\nfunction usePhysiciansByElationIds(ids: number[] | undefined) {\n return useQuery(\n physiciansGetByElationIdsRef as FunctionReference<\"query\">,\n ids && ids.length > 0 ? { ids } : \"skip\",\n );\n}\n\n/**\n * Subscribe to a single physician by their Elation ID.\n */\nfunction usePhysicianByElationId(id: number | undefined) {\n return useQuery(\n physiciansGetByElationIdRef as FunctionReference<\"query\">,\n id !== undefined ? { id } : \"skip\",\n );\n}\n\n// ---------------------------------------------------------------------------\n// Patient medical hooks (medications, problems, allergies, appointments)\n// ---------------------------------------------------------------------------\n\nconst medicationsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getMedicationsByElationPatient\");\n\nconst problemsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getProblemsByElationPatient\");\n\nconst allergiesByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getAllergiesByElationPatient\");\n\nconst appointmentsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getAppointmentsByElationPatient\");\n\ninterface UsePatientMedicalOptions {\n /**\n * Base URL of the Truth API. Required to trigger the background refresh\n * (e.g. `https://app.truth.communication-hub.com`). If omitted, the hook\n * only reads from Convex and does not refresh.\n */\n apiBaseUrl?: string;\n /** API key used for the refresh call. */\n apiKey?: string;\n /**\n * If true, suppress the background refresh. Useful when you know the\n * data was just refreshed by another component on the page.\n */\n skipRefresh?: boolean;\n}\n\n/**\n * Composite hook that returns a patient's medical records — medications,\n * problems, allergies, appointments — from the Convex cache.\n *\n * On mount (and when `elationId` changes) fires a background refresh\n * against Truth's `/api/patients/medical/refresh` endpoint so stale data\n * is pulled in without blocking render. Returns cached data immediately;\n * Convex subscription updates the UI when refresh completes.\n */\nfunction usePatientMedical(\n elationId: number | undefined,\n options?: UsePatientMedicalOptions,\n) {\n const medications = useQuery(\n medicationsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const problems = useQuery(\n problemsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const allergies = useQuery(\n allergiesByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const appointments = useQuery(\n appointmentsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n useEffect(() => {\n if (elationId === undefined || options?.skipRefresh) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/medical/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({ elationId }),\n signal: controller.signal,\n }).catch(() => {\n /* background refresh — failures are non-fatal */\n });\n\n return () => controller.abort();\n }, [elationId, options?.apiBaseUrl, options?.apiKey, options?.skipRefresh]);\n\n return { medications, problems, allergies, appointments };\n}\n\n// ---------------------------------------------------------------------------\n// Patient basic hooks (Elation demographics + Hint memberships/account)\n// ---------------------------------------------------------------------------\n//\n// Replaces the CommHub `getPatientBasicDetails` Hasura Action. Reads\n// from Convex-cached rows (populated by the patients-backfill cron +\n// `/api/patients/basic/refresh` on-demand), falling back to an empty\n// state until the background refresh completes.\n\nconst elationPatientByIdRef = makeFunctionReference<\n \"query\",\n { elationId: number },\n unknown | null\n>(\"elationPatients:getByElationId\");\n\nconst hintPatientByIdRef = makeFunctionReference<\n \"query\",\n { hintId: string },\n unknown | null\n>(\"hintPatients:getByHintId\");\n\nconst pharmacyByNcpdpRef = makeFunctionReference<\n \"query\",\n { ncpdpId: string },\n unknown | null\n>(\"elationPharmacies:getByNcpdpId\");\n\nconst patientPhotoByIdRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown | null\n>(\"elationPatientPhotos:getByElationPatientId\");\n\ninterface UsePatientBasicOptions {\n /** Truth API base URL used for the background refresh. */\n apiBaseUrl?: string;\n /** API key for the refresh call. */\n apiKey?: string;\n /** Suppress the background refresh. */\n skipRefresh?: boolean;\n}\n\ninterface UsePatientBasicResult {\n /**\n * Raw Elation patient payload (matches the shape Elation's\n * `/patients/{id}` returns — `first_name`, `last_name`, `phones`, etc).\n * Returns `undefined` while the cache miss is loading, `null` if the\n * patient isn't in Convex yet (first-open-after-backfill).\n */\n elationPatient: Record<string, unknown> | null | undefined;\n /**\n * Raw Hint patient payload (matches the shape Hint's\n * `/provider/patients/{id}` returns — `memberships`, `account`,\n * `phones`, etc).\n */\n hintPatient: Record<string, unknown> | null | undefined;\n /**\n * The full Convex row for Elation (includes `elationId`, `lastSyncedAt`,\n * `photoS3Key`, `preferredPharmacyNcpdpId`, etc). Use this when you\n * need the structured/typed fields rather than the raw Elation payload.\n */\n elationRow: Record<string, unknown> | null | undefined;\n /**\n * The full Convex row for Hint. Similar relationship to `hintPatient`.\n */\n hintRow: Record<string, unknown> | null | undefined;\n /** True while either cache miss is still pending. */\n loading: boolean;\n}\n\n/**\n * Composite hook returning a patient's basic details — Hint demographics\n * + memberships + account, Elation demographics + clinical metadata —\n * from the Convex cache.\n *\n * On mount (and when inputs change) fires a background refresh against\n * `/api/patients/basic/refresh` so stale rows get pulled fresh without\n * blocking render. Returns cached data immediately; Convex subscription\n * updates the UI when refresh completes.\n */\nfunction usePatientBasic(\n input: { hintId?: string; elationId?: number },\n options?: UsePatientBasicOptions,\n): UsePatientBasicResult {\n const elationRow = useQuery(\n elationPatientByIdRef as FunctionReference<\"query\">,\n input.elationId !== undefined ? { elationId: input.elationId } : \"skip\",\n ) as Record<string, unknown> | null | undefined;\n\n const hintRow = useQuery(\n hintPatientByIdRef as FunctionReference<\"query\">,\n input.hintId !== undefined ? { hintId: input.hintId } : \"skip\",\n ) as Record<string, unknown> | null | undefined;\n\n useEffect(() => {\n if (options?.skipRefresh) {\n return;\n }\n if (!input.hintId && input.elationId === undefined) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/basic/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({\n hintId: input.hintId,\n elationId: input.elationId,\n }),\n signal: controller.signal,\n }).catch(() => {\n /* background refresh — failures are non-fatal */\n });\n\n return () => controller.abort();\n }, [\n input.hintId,\n input.elationId,\n options?.apiBaseUrl,\n options?.apiKey,\n options?.skipRefresh,\n ]);\n\n // Surface the raw payload under elationPatient/hintPatient so consumers\n // can keep using the exact Elation/Hint field names (first_name,\n // memberships, account.past_due_in_cents) with zero mapping.\n const elationPatient =\n elationRow === undefined\n ? undefined\n : elationRow === null\n ? null\n : ((elationRow.raw as Record<string, unknown>) ?? null);\n const hintPatient =\n hintRow === undefined\n ? undefined\n : hintRow === null\n ? null\n : ((hintRow.raw as Record<string, unknown>) ?? null);\n\n const elationLoading =\n input.elationId !== undefined && elationRow === undefined;\n const hintLoading = input.hintId !== undefined && hintRow === undefined;\n\n return {\n elationPatient,\n hintPatient,\n elationRow,\n hintRow,\n loading: elationLoading || hintLoading,\n };\n}\n\n/**\n * Read a shared pharmacy row by NCPDP id. Backs the pharmacy card in\n * the patient panel (from Elation's preferred_pharmacy reference).\n */\nfunction usePharmacyByNcpdpId(ncpdpId: string | undefined) {\n return useQuery(\n pharmacyByNcpdpRef as FunctionReference<\"query\">,\n ncpdpId ? { ncpdpId } : \"skip\",\n );\n}\n\ninterface UsePatientPhotoOptions {\n apiBaseUrl?: string;\n apiKey?: string;\n skipRefresh?: boolean;\n}\n\n/**\n * Subscribe to a patient's profile photo (s3Key + metadata). The\n * consumer constructs a download URL via the Truth attachments resource\n * (signed S3 URL). Fires a background refresh to pull the latest photo\n * binary from Elation + upload to S3.\n */\nfunction usePatientPhoto(\n elationId: number | undefined,\n options?: UsePatientPhotoOptions,\n) {\n const photo = useQuery(\n patientPhotoByIdRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n );\n\n useEffect(() => {\n if (options?.skipRefresh) {\n return;\n }\n if (elationId === undefined) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/photo/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({ elationId }),\n signal: controller.signal,\n }).catch(() => {\n /* non-fatal */\n });\n\n return () => controller.abort();\n }, [elationId, options?.apiBaseUrl, options?.apiKey, options?.skipRefresh]);\n\n return photo;\n}\n\n// ---------------------------------------------------------------------------\n// Conversation messages (Dialpad calls + SMS merged)\n// ---------------------------------------------------------------------------\n\ninterface ConversationMessage {\n kind: \"call\" | \"sms\";\n id: string;\n providerId: string;\n state: string | null;\n direction: string | null;\n fromNumber: string | null;\n toNumber: string | null;\n voicemailLink: string | null;\n duration: number | null;\n text: string | null;\n mms: boolean;\n mmsUrl: string | null;\n messageStatus: string | null;\n occurredAt: string;\n conversationId: string | null;\n patientId: string | null;\n}\n\nconst messagesByPhonesRef = makeFunctionReference<\n \"query\",\n { phoneA: string; phoneB: string; limit?: number },\n ConversationMessage[]\n>(\"conversationMessages:getByPhones\");\n\nconst messagesByConversationIdRef = makeFunctionReference<\n \"query\",\n { conversationId: string; limit?: number },\n ConversationMessage[]\n>(\"conversationMessages:getByConversationId\");\n\ninterface UseConversationMessagesOptions {\n /** Max items to return (default 200). */\n limit?: number;\n}\n\n/**\n * Subscribe to a conversation's calls + SMS merged chronologically.\n * Pass the patient phone + the provider phone — Truth computes a\n * normalized pair key server-side so formatting differences don't\n * matter.\n *\n * Returns `undefined` while loading, then `ConversationMessage[]`\n * sorted newest-first. Updates live as new webhook events land in\n * Convex via the kinesis consumer.\n */\nfunction useConversationMessages(\n input: { phoneA?: string; phoneB?: string; conversationId?: string },\n options?: UseConversationMessagesOptions,\n): ConversationMessage[] | undefined {\n const hasPair = !!input.phoneA && !!input.phoneB;\n const byPair = useQuery(\n messagesByPhonesRef as FunctionReference<\"query\">,\n hasPair\n ? {\n phoneA: input.phoneA as string,\n phoneB: input.phoneB as string,\n limit: options?.limit,\n }\n : \"skip\",\n ) as ConversationMessage[] | undefined;\n\n const byConvo = useQuery(\n messagesByConversationIdRef as FunctionReference<\"query\">,\n !hasPair && input.conversationId\n ? { conversationId: input.conversationId, limit: options?.limit }\n : \"skip\",\n ) as ConversationMessage[] | undefined;\n\n return hasPair ? byPair : byConvo;\n}\n\nexport {\n usePatients,\n usePatient,\n usePatientByElationId,\n usePatientByHintId,\n useAppointments,\n useAppointment,\n useAppointmentByElationId,\n usePhysiciansByElationIds,\n usePhysicianByElationId,\n usePatientMedical,\n usePatientBasic,\n usePharmacyByNcpdpId,\n usePatientPhoto,\n useConversationMessages,\n};\nexport type { ConversationMessage, UseConversationMessagesOptions };\nexport type {\n UsePatientListOptions,\n UseAppointmentListOptions,\n UsePatientMedicalOptions,\n UsePatientBasicOptions,\n UsePatientBasicResult,\n UsePatientPhotoOptions,\n Physician,\n};\n","/**\n * useNotifications — Truth SDK React hook for push notifications.\n *\n * Shape-compatible with `expo-notifications` where practical so\n * consumers port with minimal change. The hook dynamically imports\n * `expo-notifications` so the SDK doesn't hard-depend on Expo —\n * consumers running on a non-Expo runtime (e.g. a server test harness)\n * can still import the SDK without Metro blowing up.\n *\n * Exposes:\n * - permissionStatus — \"granted\" / \"denied\" / \"undetermined\"\n * - devicePushToken — native APNs/FCM token (undefined until register)\n * - register() — request permission, fetch token, register with Truth\n * - unregister() — revoke the device\n * - addReceivedListener / addResponseListener — re-exports of expo's\n * - getBadgeCount / setBadgeCount — re-exports of expo's\n *\n * Web push (VAPID subscription) lands in Phase 3.\n */\n\n\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n isWebPushSupported,\n registerServiceWorker,\n subscribeToPush,\n subscriptionToJSON,\n} from \"../web-push\";\n\n// `expo-notifications` is an optional peer dep. We lazy-import it via\n// a string-indirection so TypeScript + tsup don't try to resolve its\n// types at build time (the SDK ships without a hard dep so non-Expo\n// consumers — web, Node services — can still import from @hipnation-truth/sdk/react).\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ExpoNotificationsModule = any;\n\nasync function loadExpo(): Promise<ExpoNotificationsModule | null> {\n try {\n // Literal specifier so Hermes can parse the dynamic import (Hermes\n // rejects `import(variable)`). Magic comments tell vite/webpack not\n // to resolve at build time on consumers that don't ship Expo.\n return await import(\n /* @vite-ignore */ /* webpackIgnore: true */\n // @ts-ignore — expo-notifications is an optional peer dep\n \"expo-notifications\"\n );\n } catch {\n return null;\n }\n}\n\nexport type PermissionStatus =\n | \"granted\"\n | \"denied\"\n | \"undetermined\"\n | \"unknown\";\n\nexport interface UseNotificationsOptions {\n /** Truth API base URL, e.g. https://app.truth.communication-hub.com */\n apiBaseUrl: string;\n /** `hn_live_*` API key for the caller's application. */\n apiKey: string;\n /** Current user id — used when registering the device. */\n userId: string | null | undefined;\n /** Optional app version string stored on the device row. */\n appVersion?: string;\n /**\n * Auto-register on mount when permission is already granted.\n * Default: true. Set false if you want to control the registration\n * lifecycle yourself.\n */\n autoRegister?: boolean;\n /** VAPID public key for web push. Fetched automatically if omitted. */\n vapidPublicKey?: string;\n /** Path to the service worker file. Default: \"/truth-sw.js\" */\n serviceWorkerPath?: string;\n}\n\nexport interface UseNotificationsResult {\n permissionStatus: PermissionStatus;\n devicePushToken: string | null;\n register: () => Promise<{ ok: boolean; reason?: string }>;\n unregister: () => Promise<void>;\n addReceivedListener: (listener: (n: unknown) => void) => () => void;\n addResponseListener: (listener: (r: unknown) => void) => () => void;\n getBadgeCount: () => Promise<number>;\n setBadgeCount: (count: number) => Promise<void>;\n}\n\nexport function useNotifications(\n options: UseNotificationsOptions,\n): UseNotificationsResult {\n const [permissionStatus, setPermissionStatus] =\n useState<PermissionStatus>(\"unknown\");\n const [devicePushToken, setDevicePushToken] = useState<string | null>(null);\n const expoRef = useRef<ExpoNotificationsModule | null>(null);\n const isWebRef = useRef(false);\n const vapidKeyRef = useRef<string | null>(options.vapidPublicKey ?? null);\n\n useEffect(() => {\n let mounted = true;\n void (async () => {\n const expo = await loadExpo();\n if (!mounted) {\n return;\n }\n expoRef.current = expo;\n\n if (expo) {\n try {\n const perm = await expo.getPermissionsAsync();\n if (!mounted) {\n return;\n }\n setPermissionStatus(mapStatus(perm?.status));\n } catch {\n setPermissionStatus(\"unknown\");\n }\n return;\n }\n\n if (isWebPushSupported()) {\n isWebRef.current = true;\n if (!vapidKeyRef.current) {\n try {\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/vapid-key`,\n {\n headers: {\n Accept: \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n },\n );\n if (res.ok) {\n const data = await res.json();\n vapidKeyRef.current = data.vapidPublicKey ?? null;\n }\n } catch {\n // best-effort\n }\n }\n if (!mounted) {\n return;\n }\n const webPerm =\n typeof Notification !== \"undefined\"\n ? Notification.permission\n : \"default\";\n setPermissionStatus(\n webPerm === \"granted\"\n ? \"granted\"\n : webPerm === \"denied\"\n ? \"denied\"\n : \"undetermined\",\n );\n } else {\n setPermissionStatus(\"unknown\");\n }\n })();\n return () => {\n mounted = false;\n };\n }, [options.apiBaseUrl, options.apiKey]);\n\n const register = useCallback(async () => {\n if (!options.userId) {\n return { ok: false, reason: \"missing_userId\" };\n }\n\n // Web push path\n if (isWebRef.current) {\n const vapidKey = vapidKeyRef.current;\n if (!vapidKey) {\n return { ok: false, reason: \"no_vapid_key\" };\n }\n\n const webPerm = await Notification.requestPermission();\n setPermissionStatus(\n webPerm === \"granted\"\n ? \"granted\"\n : webPerm === \"denied\"\n ? \"denied\"\n : \"undetermined\",\n );\n if (webPerm !== \"granted\") {\n return { ok: false, reason: \"permission_denied\" };\n }\n\n try {\n const swPath = options.serviceWorkerPath ?? \"/truth-sw.js\";\n const registration = await registerServiceWorker(swPath);\n await navigator.serviceWorker.ready;\n const subscription = await subscribeToPush(registration, vapidKey);\n const subJSON = subscriptionToJSON(subscription);\n setDevicePushToken(subscription.endpoint);\n\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/devices/register`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({\n userId: options.userId,\n platform: \"web\",\n webPushSubscription: subJSON,\n appVersion: options.appVersion,\n locale: navigator.language,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n }),\n },\n );\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n return {\n ok: false,\n reason: `register_failed_${res.status}: ${text.slice(0, 120)}`,\n };\n }\n return { ok: true };\n } catch (err) {\n return {\n ok: false,\n reason: `web_push_error: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n }\n\n // Native (expo-notifications) path\n const expo = expoRef.current ?? (await loadExpo());\n expoRef.current = expo;\n if (!expo) {\n return { ok: false, reason: \"expo_notifications_missing\" };\n }\n\n let perm = await expo.getPermissionsAsync();\n if (perm?.status !== \"granted\") {\n perm = await expo.requestPermissionsAsync();\n }\n setPermissionStatus(mapStatus(perm?.status));\n if (perm?.status !== \"granted\") {\n return { ok: false, reason: \"permission_denied\" };\n }\n\n const tokenResp = await expo.getDevicePushTokenAsync();\n const nativeToken = tokenResp?.data;\n const platform = detectPlatform(tokenResp?.type);\n if (!nativeToken || (platform !== \"ios\" && platform !== \"android\")) {\n return { ok: false, reason: \"no_native_token\" };\n }\n\n setDevicePushToken(nativeToken);\n\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/devices/register`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({\n userId: options.userId,\n platform,\n nativeToken,\n appVersion: options.appVersion,\n locale:\n typeof navigator !== \"undefined\" ? navigator.language : undefined,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n }),\n },\n );\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n return {\n ok: false,\n reason: `register_failed_${res.status}: ${text.slice(0, 120)}`,\n };\n }\n return { ok: true };\n }, [\n options.apiBaseUrl,\n options.apiKey,\n options.userId,\n options.appVersion,\n options.serviceWorkerPath,\n ]);\n\n const unregister = useCallback(async () => {\n if (!devicePushToken) {\n return;\n }\n await fetch(`${options.apiBaseUrl}/api/notifications/devices/unregister`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({ nativeToken: devicePushToken }),\n }).catch(() => {\n /* non-fatal */\n });\n setDevicePushToken(null);\n }, [options.apiBaseUrl, options.apiKey, devicePushToken]);\n\n const addReceivedListener = useCallback(\n (listener: (n: unknown) => void): (() => void) => {\n if (isWebRef.current) {\n if (\n typeof navigator === \"undefined\" ||\n !(\"serviceWorker\" in navigator)\n ) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === \"TRUTH_PUSH_RECEIVED\") {\n listener(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () =>\n navigator.serviceWorker.removeEventListener(\"message\", handler);\n }\n const expo = expoRef.current;\n if (!expo?.addNotificationReceivedListener) {\n return () => {};\n }\n const sub = expo.addNotificationReceivedListener(listener);\n return () => sub.remove?.();\n },\n [],\n );\n\n const addResponseListener = useCallback(\n (listener: (r: unknown) => void): (() => void) => {\n if (isWebRef.current) {\n if (\n typeof navigator === \"undefined\" ||\n !(\"serviceWorker\" in navigator)\n ) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === \"TRUTH_PUSH_TAPPED\") {\n listener(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () =>\n navigator.serviceWorker.removeEventListener(\"message\", handler);\n }\n const expo = expoRef.current;\n if (!expo?.addNotificationResponseReceivedListener) {\n return () => {};\n }\n const sub = expo.addNotificationResponseReceivedListener(listener);\n return () => sub.remove?.();\n },\n [],\n );\n\n const getBadgeCount = useCallback(async (): Promise<number> => {\n const expo = expoRef.current;\n if (!expo?.getBadgeCountAsync) {\n return 0;\n }\n return (await expo.getBadgeCountAsync()) ?? 0;\n }, []);\n\n const setBadgeCount = useCallback(async (count: number): Promise<void> => {\n const expo = expoRef.current;\n if (!expo?.setBadgeCountAsync) {\n return;\n }\n await expo.setBadgeCountAsync(count);\n }, []);\n\n // Auto-register once on mount when conditions are met\n const autoRegister = options.autoRegister !== false;\n useEffect(() => {\n if (!autoRegister) {\n return;\n }\n if (permissionStatus !== \"granted\") {\n return;\n }\n if (devicePushToken) {\n return;\n }\n if (!options.userId) {\n return;\n }\n void register();\n }, [\n autoRegister,\n permissionStatus,\n devicePushToken,\n options.userId,\n register,\n ]);\n\n return {\n permissionStatus,\n devicePushToken,\n register,\n unregister,\n addReceivedListener,\n addResponseListener,\n getBadgeCount,\n setBadgeCount,\n };\n}\n\nfunction mapStatus(status: string | undefined): PermissionStatus {\n if (status === \"granted\") {\n return \"granted\";\n }\n if (status === \"denied\") {\n return \"denied\";\n }\n if (status === \"undetermined\") {\n return \"undetermined\";\n }\n return \"unknown\";\n}\n\nfunction detectPlatform(\n tokenType: string | undefined,\n): \"ios\" | \"android\" | \"web\" | \"unknown\" {\n if (tokenType === \"apns\") {\n return \"ios\";\n }\n if (tokenType === \"fcm\") {\n return \"android\";\n }\n if (tokenType === \"web\") {\n return \"web\";\n }\n return \"unknown\";\n}\n","/**\n * Web Push helpers for browser environments.\n *\n * Handles service worker registration, VAPID push subscription, and\n * message forwarding from the service worker to the main thread.\n */\n\nexport interface WebPushConfig {\n vapidPublicKey: string;\n serviceWorkerPath?: string;\n}\n\nexport function isWebPushSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n \"PushManager\" in window\n );\n}\n\nexport async function registerServiceWorker(\n path = \"/truth-sw.js\",\n): Promise<ServiceWorkerRegistration> {\n return navigator.serviceWorker.register(path);\n}\n\nexport async function subscribeToPush(\n registration: ServiceWorkerRegistration,\n vapidPublicKey: string,\n): Promise<PushSubscription> {\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n return existing;\n }\n\n return registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(\n vapidPublicKey,\n ) as unknown as ArrayBuffer,\n });\n}\n\nexport function subscriptionToJSON(sub: PushSubscription): {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n} {\n const json = sub.toJSON();\n return {\n endpoint: sub.endpoint,\n keys: {\n p256dh: json.keys?.p256dh ?? \"\",\n auth: json.keys?.auth ?? \"\",\n },\n };\n}\n\nfunction urlBase64ToUint8Array(base64String: string): Uint8Array {\n const padding = \"=\".repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const rawData = atob(base64);\n const outputArray = new Uint8Array(rawData.length);\n for (let i = 0; i < rawData.length; ++i) {\n outputArray[i] = rawData.charCodeAt(i);\n }\n return outputArray;\n}\n\nexport function onServiceWorkerMessage(\n type: string,\n callback: (payload: unknown) => void,\n): () => void {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === type) {\n callback(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () => navigator.serviceWorker.removeEventListener(\"message\", handler);\n}\n","/**\n * TruthProvider — Convex React provider pre-configured for Truth.\n *\n * Wraps children in a ConvexProvider connected to the correct\n * Truth Convex deployment for the given environment.\n *\n * @example\n * ```tsx\n * import { TruthProvider } from '@hipnation-truth/sdk/react';\n *\n * function App() {\n * return (\n * <TruthProvider environment=\"sandbox\">\n * <MyApp />\n * </TruthProvider>\n * );\n * }\n * ```\n */\n\n\"use client\";\n\nimport { ConvexProvider, ConvexReactClient } from \"convex/react\";\nimport type { ReactNode } from \"react\";\nimport { createElement, useMemo } from \"react\";\n\n// Mirrors CONVEX_URLS in client.ts. UAT shares prod resources.\n//\n// Exported so `resolveConvexUrl` can be unit-tested without booting\n// React — a regression in this map shipped SDK 0.4.1 that pointed\n// UAT at sandbox Convex. The test in provider.test.ts locks every\n// stage → URL pair down.\nexport const CONVEX_URLS: Record<string, string> = {\n local: \"https://courteous-duck-623.convex.cloud\",\n staging: \"https://courteous-duck-623.convex.cloud\",\n stg: \"https://courteous-duck-623.convex.cloud\",\n sandbox: \"https://courteous-duck-623.convex.cloud\",\n uat: \"https://gallant-gecko-217.convex.cloud\",\n production: \"https://gallant-gecko-217.convex.cloud\",\n};\n\n/**\n * Resolve the Convex URL for a given environment. Honors an explicit\n * override and falls back to sandbox for unknown environments.\n */\nexport function resolveConvexUrl(\n environment: string | undefined,\n override?: string,\n): string {\n if (override) {\n return override;\n }\n const env = environment ?? \"sandbox\";\n return CONVEX_URLS[env] ?? CONVEX_URLS.sandbox;\n}\n\ninterface TruthProviderProps {\n /** Truth environment — determines which Convex deployment to connect to */\n environment?: string;\n /** Override the Convex URL directly */\n convexUrl?: string;\n children: ReactNode;\n}\n\nfunction TruthProvider({\n environment = \"sandbox\",\n convexUrl,\n children,\n}: TruthProviderProps) {\n const url = resolveConvexUrl(environment, convexUrl);\n\n const client = useMemo(() => new ConvexReactClient(url), [url]);\n\n return createElement(ConvexProvider, { client }, children);\n}\n\nexport { TruthProvider };\nexport type { TruthProviderProps };\n","/**\n * React tracking hook — fire-and-forget event tracking from client components.\n *\n * Creates a lightweight Tracker instance scoped to the browser session\n * and exposes a useTrack() hook for emitting events to Truth's Kinesis stream.\n *\n * @example\n * ```tsx\n * import { useTruth } from '@hipnation-truth/sdk/react';\n *\n * function ConversationView({ conversationId }) {\n * const truth = useTruth();\n *\n * useEffect(() => {\n * truth.track('conversation.marked_read.v1', {\n * read_by_actor_id: userId,\n * unread_count_before: 5,\n * unread_count_after: 0,\n * }, { subject: { conversation_id: conversationId } });\n * }, [conversationId]);\n * }\n * ```\n */\n\n\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { createContext, createElement, useContext, useMemo } from \"react\";\nimport type {\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"../tracking/events\";\nimport { Tracker } from \"../tracking/tracker\";\nimport type { ActorContext } from \"../types/config\";\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\ninterface TruthTrackingContextValue {\n track: <T extends EventType>(\n eventType: T,\n payload: EventPayloadMap[T],\n options?: TrackOptions,\n ) => void;\n identify: (actorId: string, actorType: ActorContext[\"actorType\"]) => void;\n}\n\nconst TruthTrackingContext = createContext<TruthTrackingContextValue | null>(\n null,\n);\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\ninterface TruthTrackingProviderProps {\n /** Truth environment — determines API URL for event delivery */\n environment?: string;\n /** Event source identifier */\n source?: string;\n /** Source version (git SHA) */\n sourceVersion?: string;\n /** Default tenant ID */\n tenantId?: string;\n /** API key for authentication */\n apiKey?: string;\n children: ReactNode;\n}\n\nfunction TruthTrackingProvider({\n environment = \"sandbox\",\n source = \"communication-hub.frontend\",\n sourceVersion = \"unknown\",\n tenantId = \"hipnation\",\n apiKey = \"\",\n children,\n}: TruthTrackingProviderProps) {\n const value = useMemo<TruthTrackingContextValue>(() => {\n const tracker = new Tracker({\n apiKey,\n environment,\n source,\n sourceVersion,\n tenantId,\n batchSize: 10,\n flushIntervalMs: 5000,\n });\n\n return {\n track: (eventType, payload, options) => {\n tracker.track(eventType, payload, options);\n },\n identify: (actorId, actorType) => {\n tracker.setActor({ actorId, actorType });\n },\n };\n }, [apiKey, environment, source, sourceVersion, tenantId]);\n\n return createElement(TruthTrackingContext.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Access the Truth tracking context. Must be within a TruthTrackingProvider.\n * Returns `{ track, identify }` for emitting events to Kinesis.\n */\nfunction useTruth(): TruthTrackingContextValue {\n const ctx = useContext(TruthTrackingContext);\n if (!ctx) {\n throw new Error(\"useTruth must be used within a TruthTrackingProvider\");\n }\n return ctx;\n}\n\nexport { TruthTrackingProvider, useTruth };\nexport type { TruthTrackingProviderProps, TruthTrackingContextValue };\n","/**\n * Event tracker with batching, retry, and flush support.\n *\n * Buffers events in an internal queue and flushes them to the Truth API\n * endpoint. Flushes occur when the buffer reaches `batchSize` or every\n * `flushIntervalMs` milliseconds, whichever comes first.\n */\n\nimport type { ActorContext } from \"../types/config\";\nimport type {\n EventEnvelope,\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"./events\";\n\n// ---------------------------------------------------------------------------\n// UUID v7 helper (no external dependency)\n// ---------------------------------------------------------------------------\n\n/**\n * Generates a UUID v7 string. Uses crypto.randomUUID where available,\n * falling back to a timestamp + random bytes implementation.\n *\n * UUID v7 layout (RFC 9562):\n * 48 bits - unix timestamp (ms)\n * 4 bits - version (0b0111)\n * 12 bits - random\n * 2 bits - variant (0b10)\n * 62 bits - random\n */\nfunction generateUuidV7(): string {\n const now = Date.now();\n\n // 6 bytes of timestamp\n const timeBytes = new Uint8Array(6);\n let ts = now;\n for (let i = 5; i >= 0; i--) {\n timeBytes[i] = ts & 0xff;\n ts = Math.floor(ts / 256);\n }\n\n // 10 bytes of random\n const randomBytes = new Uint8Array(10);\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n globalThis.crypto.getRandomValues\n ) {\n globalThis.crypto.getRandomValues(randomBytes);\n } else {\n for (let i = 0; i < 10; i++) {\n randomBytes[i] = Math.floor(Math.random() * 256);\n }\n }\n\n // Assemble 16 bytes\n const bytes = new Uint8Array(16);\n bytes.set(timeBytes, 0);\n bytes.set(randomBytes, 6);\n\n // Set version (bits 48-51 to 0b0111)\n bytes[6] = (bytes[6] & 0x0f) | 0x70;\n\n // Set variant (bits 64-65 to 0b10)\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n\n // Format as hex string with dashes\n const hex = Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return [\n hex.slice(0, 8),\n hex.slice(8, 12),\n hex.slice(12, 16),\n hex.slice(16, 20),\n hex.slice(20, 32),\n ].join(\"-\");\n}\n\n// ---------------------------------------------------------------------------\n// Environment-based API URL resolution\n// ---------------------------------------------------------------------------\n\n// Environment → Truth API base URL.\n//\n// Topology:\n// local / staging / stg / sandbox → sandbox Truth (app.sandbox.*)\n// uat / production → production Truth (app.truth.*)\n//\n// UAT shares production resources (same Fly Postgres + Hasura + Truth +\n// Convex + EHR tokens as prod). Staging is the isolated sandbox env.\nconst API_URLS: Record<string, string> = {\n local: \"http://localhost:3000\",\n staging: \"https://app.sandbox.communication-hub.com\",\n stg: \"https://app.sandbox.communication-hub.com\",\n sandbox: \"https://app.sandbox.communication-hub.com\",\n uat: \"https://app.truth.communication-hub.com\",\n production: \"https://app.truth.communication-hub.com\",\n};\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_BATCH_SIZE = 25;\nconst DEFAULT_FLUSH_INTERVAL_MS = 5_000;\nconst MAX_RETRIES = 3;\nconst BASE_RETRY_DELAY_MS = 500;\n\n// ---------------------------------------------------------------------------\n// Tracker configuration\n// ---------------------------------------------------------------------------\n\ninterface TrackerConfig {\n apiKey: string;\n environment: string;\n source: string;\n sourceVersion: string;\n tenantId: string;\n batchSize: number;\n flushIntervalMs: number;\n apiBaseUrl?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Tracker class\n// ---------------------------------------------------------------------------\n\nclass Tracker {\n private readonly config: TrackerConfig;\n readonly apiUrl: string;\n private queue: EventEnvelope[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private defaultActor: ActorContext | undefined;\n private isFlushing = false;\n private isShutdown = false;\n\n constructor(config: TrackerConfig) {\n this.config = config;\n this.apiUrl =\n config.apiBaseUrl ?? API_URLS[config.environment] ?? API_URLS.local;\n\n this.startFlushInterval();\n this.registerShutdownHooks();\n }\n\n /**\n * Set the default actor context for subsequent events.\n */\n setActor(actor: ActorContext): void {\n this.defaultActor = actor;\n }\n\n /**\n * Enqueue a typed event for delivery. This is fire-and-forget from\n * the caller's perspective -- events are buffered and flushed in batches.\n */\n track<T extends EventType>(\n eventType: T,\n payload: EventPayloadMap[T],\n options?: TrackOptions,\n ): void {\n if (this.isShutdown) {\n return;\n }\n\n const now = new Date().toISOString();\n\n const envelope: EventEnvelope = {\n event_id: generateUuidV7(),\n event_type: eventType,\n schema_version: 1,\n occurred_at: options?.occurredAt ?? now,\n received_at: now,\n source: this.config.source,\n source_version: this.config.sourceVersion,\n tenant_id: options?.tenantId ?? this.config.tenantId,\n actor:\n options?.actor ??\n (this.defaultActor\n ? {\n actor_id: this.defaultActor.actorId,\n actor_type: this.defaultActor.actorType,\n }\n : undefined),\n subject: options?.subject,\n compliance: options?.compliance,\n payload: payload as unknown as Record<string, unknown>,\n };\n\n this.queue.push(envelope);\n\n if (this.queue.length >= this.config.batchSize) {\n void this.flush();\n }\n }\n\n /**\n * Force an immediate flush of all buffered events.\n * Returns a promise that resolves when the flush completes.\n */\n async flush(): Promise<void> {\n if (this.queue.length === 0 || this.isFlushing) {\n return;\n }\n\n this.isFlushing = true;\n const batch = this.queue.splice(0, this.config.batchSize);\n\n try {\n await this.sendBatch(batch);\n } catch {\n // Re-queue events that failed to send (prepend to maintain ordering)\n this.queue.unshift(...batch);\n } finally {\n this.isFlushing = false;\n }\n\n // If there are still events in the queue, flush again\n if (this.queue.length >= this.config.batchSize) {\n await this.flush();\n }\n }\n\n /**\n * Gracefully shut down the tracker. Flushes remaining events and\n * clears the flush interval.\n */\n async shutdown(): Promise<void> {\n this.isShutdown = true;\n this.stopFlushInterval();\n await this.flush();\n }\n\n /**\n * Send a batch of events to the Truth API with exponential backoff retry.\n */\n private async sendBatch(batch: EventEnvelope[]): Promise<void> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(`${this.apiUrl}/api/events/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": this.config.apiKey,\n },\n body: JSON.stringify({ events: batch }),\n });\n\n if (response.ok) {\n return;\n }\n\n // Don't retry 4xx client errors (except 429)\n if (\n response.status >= 400 &&\n response.status < 500 &&\n response.status !== 429\n ) {\n return;\n }\n\n lastError = new Error(\n `HTTP ${response.status}: ${response.statusText}`,\n );\n } catch (error) {\n lastError = error;\n }\n\n // Exponential backoff with jitter\n if (attempt < MAX_RETRIES) {\n const delay = BASE_RETRY_DELAY_MS * 2 ** attempt;\n const jitter = Math.random() * delay * 0.5;\n await sleep(delay + jitter);\n }\n }\n\n throw lastError;\n }\n\n private startFlushInterval(): void {\n if (this.config.flushIntervalMs > 0) {\n this.flushTimer = setInterval(() => {\n void this.flush();\n }, this.config.flushIntervalMs);\n\n // Unref the timer so it doesn't prevent Node.js from exiting\n if (typeof this.flushTimer === \"object\" && \"unref\" in this.flushTimer) {\n this.flushTimer.unref();\n }\n }\n }\n\n private stopFlushInterval(): void {\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n }\n\n private registerShutdownHooks(): void {\n // Only register in Node.js environments\n if (typeof globalThis.process !== \"undefined\" && globalThis.process.on) {\n const shutdownHandler = () => {\n void this.shutdown();\n };\n\n globalThis.process.on(\"beforeExit\", shutdownHandler);\n globalThis.process.on(\"SIGTERM\", shutdownHandler);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport {\n Tracker,\n generateUuidV7,\n DEFAULT_BATCH_SIZE,\n DEFAULT_FLUSH_INTERVAL_MS,\n};\nexport type { TrackerConfig };\n"],"mappings":"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","names":["import_react","import_server","import_react","_a","res","import_react","import_react"]}
|
|
1
|
+
{"version":3,"sources":["../src/react.ts","../src/react/conversations.ts","../src/react/hooks.ts","../src/react/notifications.ts","../src/web-push.ts","../src/react/provider.ts","../src/react/tracking.ts","../src/tracking/tracker.ts"],"sourcesContent":["/**\n * @hipnation-truth/sdk/react\n *\n * React hooks and provider for Truth SDK — real-time Convex-backed data.\n *\n * @example\n * ```tsx\n * import { TruthProvider, usePatients, useAppointments } from '@hipnation-truth/sdk/react';\n *\n * function App() {\n * return (\n * <TruthProvider environment=\"sandbox\">\n * <PatientList />\n * </TruthProvider>\n * );\n * }\n *\n * function PatientList() {\n * const patients = usePatients({ limit: 20 });\n * if (!patients) return <div>Loading...</div>;\n * return patients.map(p => <div key={p._id}>{p.firstName}</div>);\n * }\n * ```\n */\n\n// Reactive conversation / message hooks (Convex-backed, PR #110 schema)\nexport type {\n ConversationListItem,\n ConversationMessageRow,\n ConversationRow,\n UseConversationsFilters,\n UseMessagesOptions,\n UseQueryResult,\n} from \"./react/conversations\";\nexport {\n useConversationByPhonePair,\n useConversations,\n useMessages,\n useUnreadCount,\n} from \"./react/conversations\";\n// Re-export types consumers commonly need\nexport type {\n ConversationMessage,\n Physician,\n UseAppointmentListOptions,\n UseConversationMessagesOptions,\n UsePatientBasicOptions,\n UsePatientBasicResult,\n UsePatientListOptions,\n UsePatientMedicalOptions,\n UsePatientPhotoOptions,\n} from \"./react/hooks\";\n// Patient / Appointment / Physician / Medical / Messages hooks\nexport {\n useAppointment,\n useAppointmentByElationId,\n useAppointments,\n useConversationMessages,\n usePatient,\n usePatientBasic,\n usePatientByElationId,\n usePatientByHintId,\n usePatientMedical,\n usePatientPhoto,\n usePatients,\n usePharmacyByNcpdpId,\n usePhysicianByElationId,\n usePhysiciansByElationIds,\n} from \"./react/hooks\";\n// Notifications\nexport type {\n PermissionStatus,\n UseNotificationsOptions,\n UseNotificationsResult,\n} from \"./react/notifications\";\nexport { useNotifications } from \"./react/notifications\";\nexport type { TruthProviderProps } from \"./react/provider\";\n// Provider\nexport { TruthProvider } from \"./react/provider\";\nexport type {\n TruthTrackingContextValue,\n TruthTrackingProviderProps,\n} from \"./react/tracking\";\n// Tracking\nexport { TruthTrackingProvider, useTruth } from \"./react/tracking\";\n// Re-export event types for track() calls\nexport type {\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"./tracking/events\";\nexport type { Appointment, AppointmentListOptions } from \"./types/appointment\";\nexport type { Patient, PatientListOptions } from \"./types/patient\";\n","/**\n * React hooks for Truth SDK — reactive conversation + message data.\n *\n * Wraps Convex queries owned by the Truth backend so consumers (CommHub\n * Expo frontend) get live-updating conversation lists, message threads,\n * and unread totals without managing subscriptions themselves.\n *\n * **Hook contract:** every hook in this file returns\n * `{ data, loading, error }`. `data` is `undefined` while loading and\n * also when the underlying query is `\"skip\"`'d (caller didn't pass the\n * required arg yet). `loading` is `true` only while a real subscription\n * is in-flight; once `data` resolves (even to `null`) `loading` flips\n * to `false`. `error` is reserved for SDK-side validation — Convex\n * itself throws on subscribe rather than returning an error value, so\n * unhandled query errors propagate as React errors and should be caught\n * with an error boundary.\n *\n * **Backed by PR #110 schema (commit `41dbb59` on\n * `feat/migrate-dialpad-webhook-and-messages-to-truth`):**\n * - `conversations:listForUser` → `useConversations`\n * - `conversations:getUnreadTotalForUser` → `useUnreadCount`\n * - `conversations:getByPhonePair` → `useConversationByPhonePair`\n * - `conversationMessages:getByConversationId` → `useMessages`\n *\n * **Deliberately NOT shipped here (no backend query yet — flagged in\n * PR #111 body for follow-up):**\n * - Single-conversation-by-id lookup. Convex `conversations` has no\n * `get(id)` query — only `getByPhonePair`. CommHub uses phonePair as\n * the primary handle, so the byPair flavor is what consumers need;\n * if id-based lookup is needed later we can add a `conversations:get`\n * in Truth.\n * - Participants / \"seen by\" hook. The migrated schema has no\n * `conversationParticipants` table — read state is per-user via\n * `conversationReads` and there's no public list query for it yet.\n * - `markRead` mutation. PR #110 declares `markRead` as\n * `internalMutation`, so the frontend can't invoke it directly — it\n * has to go through a Truth HTTP endpoint, or webhook-migrator needs\n * to flip it to a public `mutation`. Flagged for a follow-up.\n *\n * Must be used within `<TruthProvider />` (see `./provider`).\n *\n * @example\n * ```tsx\n * import {\n * useConversations,\n * useConversationByPhonePair,\n * useMessages,\n * useUnreadCount,\n * } from '@hipnation-truth/sdk/react';\n *\n * function Inbox({ userId }: { userId: string }) {\n * const { data: convos, loading } = useConversations({ userId });\n * const { data: unread } = useUnreadCount(userId);\n * if (loading) return <Spinner />;\n * return (\n * <div>\n * <Badge count={unread ?? 0} />\n * {convos?.map((c) => <ConvoRow key={c.id} convo={c} />)}\n * </div>\n * );\n * }\n * ```\n */\n\n\"use client\";\n\nimport { useQuery } from \"convex/react\";\nimport type { FunctionReference } from \"convex/server\";\nimport { makeFunctionReference } from \"convex/server\";\n\n// ---------------------------------------------------------------------------\n// Shared types\n// ---------------------------------------------------------------------------\n//\n// Mirror the shapes returned by the Convex queries on PR #110. Once\n// the consuming app regenerates `convex/_generated/api.d.ts` against\n// the merged schema these can be replaced with inferred types — for\n// today they give consumers useful autocomplete.\n\n/**\n * Row shape returned by `conversations:listForUser` — joins the base\n * `conversations` row with the caller's per-user `conversationReads`\n * entry so unread state lives on each item.\n */\nexport interface ConversationListItem {\n /** Convex id of the conversation row. */\n id: string;\n patientPhone: string;\n providerPhone: string;\n /** Sorted-digit-pair key (commutative — same regardless of direction). */\n phonePair: string;\n /** Truth-side patient id, when the kinesis pipeline could resolve it. */\n patientId: string | null;\n /** ISO timestamp of the most recent message in the conversation. */\n lastMessageAt: string;\n /** Caller's unread count for this conversation. */\n unreadCount: number;\n /** ISO timestamp of when the caller last marked the conversation read. */\n lastReadAt: string | null;\n}\n\n/**\n * Raw `conversations` table row — what `getByPhonePair` returns. No\n * unread / read joining; for that, prefer `useConversations`.\n */\nexport interface ConversationRow {\n _id: string;\n _creationTime: number;\n patientPhone: string;\n providerPhone: string;\n phonePair: string;\n patientId: string | null;\n lastEventId: string | null;\n lastMessageAt: string;\n createdAt: string;\n}\n\n/**\n * Message row returned by `conversationMessages:getByConversationId` —\n * calls + SMS merged chronologically. Mirrors `ConversationMessage`\n * already exported from the SDK; re-exported here under a clearer name\n * for the new hook surface.\n */\nexport interface ConversationMessageRow {\n kind: \"call\" | \"sms\";\n id: string;\n providerId: string;\n state: string | null;\n direction: string | null;\n fromNumber: string | null;\n toNumber: string | null;\n voicemailLink: string | null;\n duration: number | null;\n text: string | null;\n mms: boolean;\n mmsUrl: string | null;\n messageStatus: string | null;\n occurredAt: string;\n conversationId: string | null;\n patientId: string | null;\n}\n\nexport interface UseQueryResult<T> {\n /**\n * Query data. `undefined` while loading or while the query is\n * intentionally skipped (e.g. caller hasn't supplied `userId` yet).\n */\n data: T | undefined;\n /** True only while a real subscription is in-flight. */\n loading: boolean;\n /**\n * Reserved for client-side validation errors surfaced by the SDK\n * itself. Convex query errors propagate as React errors and should\n * be caught with an error boundary.\n */\n error: Error | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Convex function references — string-keyed, decoupled from the\n// consuming app's generated `convex/_generated/api`. Names below match\n// the queries on PR #110 (commit 41dbb59) verbatim.\n// ---------------------------------------------------------------------------\n\nconst conversationsListForUserRef = makeFunctionReference<\n \"query\",\n { userId: string; limit?: number },\n ConversationListItem[]\n>(\"conversations:listForUser\");\n\nconst conversationsGetUnreadTotalForUserRef = makeFunctionReference<\n \"query\",\n { userId: string },\n number\n>(\"conversations:getUnreadTotalForUser\");\n\nconst conversationsGetByPhonePairRef = makeFunctionReference<\n \"query\",\n { phonePair: string },\n ConversationRow | null\n>(\"conversations:getByPhonePair\");\n\n// Lives on the existing `conversationMessages` module (pre-#110). PR\n// #110 added `conversationId` indexing on `messageSms` / `messageCalls`,\n// so this query is now reliable for the new conversations table.\nconst conversationMessagesGetByConversationIdRef = makeFunctionReference<\n \"query\",\n { conversationId: string; limit?: number },\n ConversationMessageRow[]\n>(\"conversationMessages:getByConversationId\");\n\n// ---------------------------------------------------------------------------\n// Skipped-query sentinel + result shaping\n// ---------------------------------------------------------------------------\n\nconst SKIP = \"skip\" as const;\n\nfunction toResult<T>(\n value: T | undefined,\n skipped: boolean,\n): UseQueryResult<T> {\n if (skipped) {\n return { data: undefined, loading: false, error: undefined };\n }\n return {\n data: value,\n loading: value === undefined,\n error: undefined,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Filters + options\n// ---------------------------------------------------------------------------\n\nexport interface UseConversationsFilters {\n /**\n * Truth user id (the Better Auth subject). Pass `undefined` to skip\n * the query — useful when the auth session is still loading.\n */\n userId: string | null | undefined;\n /** Page size. Server caps at 100 by default. */\n limit?: number;\n}\n\nexport interface UseMessagesOptions {\n /** Page size. Server caps at 200 by default. */\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to a user's conversations — joined with their per-conversation\n * unread count, sorted by most recent message. Updates live as new\n * SMS / calls land in Convex and the webhook bumps `unreadCount`.\n *\n * Returns `{ data: undefined, loading: false }` while the auth session\n * resolves (`userId === undefined`); not an error, just a skip.\n */\nfunction useConversations(\n filters: UseConversationsFilters,\n): UseQueryResult<ConversationListItem[]> {\n const skipped = !filters.userId;\n const result = useQuery(\n conversationsListForUserRef as FunctionReference<\"query\">,\n skipped\n ? SKIP\n : {\n userId: filters.userId as string,\n limit: filters.limit,\n },\n ) as ConversationListItem[] | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Look up a single conversation by its normalized phonePair (sorted\n * digit-pair, e.g. `\"5125550123|6505551234\"`). Returns `null` if no\n * conversation exists yet for that pair.\n *\n * Use the `phonePairKey(patientPhone, providerPhone)` helper exposed by\n * the Truth Convex module to derive the key from raw phone strings.\n */\nfunction useConversationByPhonePair(\n phonePair: string | null | undefined,\n): UseQueryResult<ConversationRow | null> {\n const skipped = !phonePair;\n const result = useQuery(\n conversationsGetByPhonePairRef as FunctionReference<\"query\">,\n skipped ? SKIP : { phonePair: phonePair as string },\n ) as ConversationRow | null | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Subscribe to a paginated message stream for a single conversation.\n * Calls + SMS merged chronologically, newest-first. Live as the\n * kinesis consumer writes to `messageCalls` / `messageSms`.\n *\n * Pass the Convex `_id` of the conversation row (from\n * `useConversations` / `useConversationByPhonePair`) as\n * `conversationId`.\n */\nfunction useMessages(\n conversationId: string | null | undefined,\n options?: UseMessagesOptions,\n): UseQueryResult<ConversationMessageRow[]> {\n const skipped = !conversationId;\n const result = useQuery(\n conversationMessagesGetByConversationIdRef as FunctionReference<\"query\">,\n skipped\n ? SKIP\n : {\n conversationId: conversationId as string,\n limit: options?.limit,\n },\n ) as ConversationMessageRow[] | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Subscribe to the user's total unread message count across all\n * conversations. Backs the inbox tab badge in CommHub.\n *\n * Returns `0` when the underlying query resolves with no unread\n * messages, `undefined` while loading or when `userId` is missing.\n */\nfunction useUnreadCount(\n userId: string | null | undefined,\n): UseQueryResult<number> {\n const skipped = !userId;\n const result = useQuery(\n conversationsGetUnreadTotalForUserRef as FunctionReference<\"query\">,\n skipped ? SKIP : { userId: userId as string },\n ) as number | undefined;\n\n return toResult(result, skipped);\n}\n\nexport {\n useConversationByPhonePair,\n useConversations,\n useMessages,\n useUnreadCount,\n};\n","/**\n * React hooks for Truth SDK — real-time Convex-backed data access.\n *\n * These hooks use Convex React subscriptions for live-updating data.\n * Must be used within a ConvexProvider (see TruthProvider).\n *\n * @example\n * ```tsx\n * import { usePatients, useAppointments } from '@hipnation-truth/sdk/react';\n *\n * function PatientList() {\n * const patients = usePatients({ limit: 20 });\n * return patients?.map(p => <div key={p._id}>{p.firstName} {p.lastName}</div>);\n * }\n * ```\n */\n\n\"use client\";\n\nimport { useQuery } from \"convex/react\";\nimport { useEffect } from \"react\";\n\n// ---------------------------------------------------------------------------\n// Convex function references\n// ---------------------------------------------------------------------------\n// We use string-based references to avoid importing the generated API\n// (which lives in the consuming app, not the SDK). Convex supports this\n// via the `api` module pattern, but for a published SDK we use\n// makeFunctionReference to stay decoupled from the app's codegen.\n\nimport type { FunctionReference } from \"convex/server\";\nimport { makeFunctionReference } from \"convex/server\";\n\n// Patient query references\nconst patientsListRef = makeFunctionReference<\n \"query\",\n {\n search?: string;\n lastName?: string;\n cursor?: string;\n limit?: number;\n },\n unknown[]\n>(\"patients:list\");\n\nconst patientsGetRef = makeFunctionReference<\n \"query\",\n { id: string },\n unknown | null\n>(\"patients:get\");\n\nconst patientsByElationIdRef = makeFunctionReference<\n \"query\",\n { elationId: string },\n unknown | null\n>(\"patients:getByElationId\");\n\nconst patientsByHintIdRef = makeFunctionReference<\n \"query\",\n { hintId: string },\n unknown | null\n>(\"patients:getByHintId\");\n\n// Appointment query references\nconst appointmentsListRef = makeFunctionReference<\n \"query\",\n {\n patientId?: string;\n status?: string;\n startDate?: string;\n endDate?: string;\n limit?: number;\n },\n unknown[]\n>(\"appointments:list\");\n\nconst appointmentsGetRef = makeFunctionReference<\n \"query\",\n { id: string },\n unknown | null\n>(\"appointments:get\");\n\nconst appointmentsByElationIdRef = makeFunctionReference<\n \"query\",\n { elationId: string },\n unknown | null\n>(\"appointments:getByElationId\");\n\n// ---------------------------------------------------------------------------\n// Patient hooks\n// ---------------------------------------------------------------------------\n\ninterface UsePatientListOptions {\n search?: string;\n lastName?: string;\n limit?: number;\n}\n\n/**\n * Subscribe to a list of patients with optional search and filtering.\n * Returns undefined while loading, then an array of patients.\n */\nfunction usePatients(options?: UsePatientListOptions) {\n return useQuery(patientsListRef as FunctionReference<\"query\">, options ?? {});\n}\n\n/**\n * Subscribe to a single patient by Convex document ID.\n */\nfunction usePatient(id: string) {\n return useQuery(patientsGetRef as FunctionReference<\"query\">, { id });\n}\n\n/**\n * Subscribe to a patient by their Elation ID.\n */\nfunction usePatientByElationId(elationId: string) {\n return useQuery(patientsByElationIdRef as FunctionReference<\"query\">, {\n elationId,\n });\n}\n\n/**\n * Subscribe to a patient by their Hint ID.\n */\nfunction usePatientByHintId(hintId: string) {\n return useQuery(patientsByHintIdRef as FunctionReference<\"query\">, {\n hintId,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Appointment hooks\n// ---------------------------------------------------------------------------\n\ninterface UseAppointmentListOptions {\n patientId?: string;\n status?: string;\n startDate?: string;\n endDate?: string;\n limit?: number;\n}\n\n/**\n * Subscribe to a list of appointments with optional filtering.\n */\nfunction useAppointments(options?: UseAppointmentListOptions) {\n return useQuery(\n appointmentsListRef as FunctionReference<\"query\">,\n options ?? {},\n );\n}\n\n/**\n * Subscribe to a single appointment by Convex document ID.\n */\nfunction useAppointment(id: string) {\n return useQuery(appointmentsGetRef as FunctionReference<\"query\">, { id });\n}\n\n/**\n * Subscribe to an appointment by its Elation ID.\n */\nfunction useAppointmentByElationId(elationId: string) {\n return useQuery(appointmentsByElationIdRef as FunctionReference<\"query\">, {\n elationId,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Physician hooks\n// ---------------------------------------------------------------------------\n\ninterface Physician {\n _id: string;\n elationId: number;\n firstName?: string;\n lastName?: string;\n npi?: string;\n credentials?: string;\n specialties?: string[];\n practice?: number;\n email?: string;\n phone?: string;\n lastSyncedAt: string;\n}\n\nconst physiciansGetByElationIdsRef = makeFunctionReference<\n \"query\",\n { ids: number[] },\n Physician[]\n>(\"physicians:getByElationIds\");\n\nconst physiciansGetByElationIdRef = makeFunctionReference<\n \"query\",\n { id: number },\n Physician | null\n>(\"physicians:getByElationId\");\n\n/**\n * Resolve a batch of physicians by their Elation IDs. Returns an array\n * of physicians that exist in Convex; missing ids are silently dropped.\n *\n * Use this to resolve medication `prescribing_physician` / appointment\n * physician ids to display names, avoiding the per-physician HTTP\n * round-trip through Truth's Elation proxy.\n *\n * Pass `undefined` (or an empty array) to skip the query — the hook\n * returns `undefined` until you pass a populated list.\n */\nfunction usePhysiciansByElationIds(ids: number[] | undefined) {\n return useQuery(\n physiciansGetByElationIdsRef as FunctionReference<\"query\">,\n ids && ids.length > 0 ? { ids } : \"skip\",\n );\n}\n\n/**\n * Subscribe to a single physician by their Elation ID.\n */\nfunction usePhysicianByElationId(id: number | undefined) {\n return useQuery(\n physiciansGetByElationIdRef as FunctionReference<\"query\">,\n id !== undefined ? { id } : \"skip\",\n );\n}\n\n// ---------------------------------------------------------------------------\n// Patient medical hooks (medications, problems, allergies, appointments)\n// ---------------------------------------------------------------------------\n\nconst medicationsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getMedicationsByElationPatient\");\n\nconst problemsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getProblemsByElationPatient\");\n\nconst allergiesByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getAllergiesByElationPatient\");\n\nconst appointmentsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getAppointmentsByElationPatient\");\n\ninterface UsePatientMedicalOptions {\n /**\n * Base URL of the Truth API. Required to trigger the background refresh\n * (e.g. `https://app.truth.communication-hub.com`). If omitted, the hook\n * only reads from Convex and does not refresh.\n */\n apiBaseUrl?: string;\n /** API key used for the refresh call. */\n apiKey?: string;\n /**\n * If true, suppress the background refresh. Useful when you know the\n * data was just refreshed by another component on the page.\n */\n skipRefresh?: boolean;\n}\n\n/**\n * Composite hook that returns a patient's medical records — medications,\n * problems, allergies, appointments — from the Convex cache.\n *\n * On mount (and when `elationId` changes) fires a background refresh\n * against Truth's `/api/patients/medical/refresh` endpoint so stale data\n * is pulled in without blocking render. Returns cached data immediately;\n * Convex subscription updates the UI when refresh completes.\n */\nfunction usePatientMedical(\n elationId: number | undefined,\n options?: UsePatientMedicalOptions,\n) {\n const medications = useQuery(\n medicationsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const problems = useQuery(\n problemsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const allergies = useQuery(\n allergiesByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const appointments = useQuery(\n appointmentsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n useEffect(() => {\n if (elationId === undefined || options?.skipRefresh) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/medical/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({ elationId }),\n signal: controller.signal,\n }).catch(() => {\n /* background refresh — failures are non-fatal */\n });\n\n return () => controller.abort();\n }, [elationId, options?.apiBaseUrl, options?.apiKey, options?.skipRefresh]);\n\n return { medications, problems, allergies, appointments };\n}\n\n// ---------------------------------------------------------------------------\n// Patient basic hooks (Elation demographics + Hint memberships/account)\n// ---------------------------------------------------------------------------\n//\n// Replaces the CommHub `getPatientBasicDetails` Hasura Action. Reads\n// from Convex-cached rows (populated by the patients-backfill cron +\n// `/api/patients/basic/refresh` on-demand), falling back to an empty\n// state until the background refresh completes.\n\nconst elationPatientByIdRef = makeFunctionReference<\n \"query\",\n { elationId: number },\n unknown | null\n>(\"elationPatients:getByElationId\");\n\nconst hintPatientByIdRef = makeFunctionReference<\n \"query\",\n { hintId: string },\n unknown | null\n>(\"hintPatients:getByHintId\");\n\nconst pharmacyByNcpdpRef = makeFunctionReference<\n \"query\",\n { ncpdpId: string },\n unknown | null\n>(\"elationPharmacies:getByNcpdpId\");\n\nconst patientPhotoByIdRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown | null\n>(\"elationPatientPhotos:getByElationPatientId\");\n\ninterface UsePatientBasicOptions {\n /** Truth API base URL used for the background refresh. */\n apiBaseUrl?: string;\n /** API key for the refresh call. */\n apiKey?: string;\n /** Suppress the background refresh. */\n skipRefresh?: boolean;\n}\n\ninterface UsePatientBasicResult {\n /**\n * Raw Elation patient payload (matches the shape Elation's\n * `/patients/{id}` returns — `first_name`, `last_name`, `phones`, etc).\n * Returns `undefined` while the cache miss is loading, `null` if the\n * patient isn't in Convex yet (first-open-after-backfill).\n */\n elationPatient: Record<string, unknown> | null | undefined;\n /**\n * Raw Hint patient payload (matches the shape Hint's\n * `/provider/patients/{id}` returns — `memberships`, `account`,\n * `phones`, etc).\n */\n hintPatient: Record<string, unknown> | null | undefined;\n /**\n * The full Convex row for Elation (includes `elationId`, `lastSyncedAt`,\n * `photoS3Key`, `preferredPharmacyNcpdpId`, etc). Use this when you\n * need the structured/typed fields rather than the raw Elation payload.\n */\n elationRow: Record<string, unknown> | null | undefined;\n /**\n * The full Convex row for Hint. Similar relationship to `hintPatient`.\n */\n hintRow: Record<string, unknown> | null | undefined;\n /** True while either cache miss is still pending. */\n loading: boolean;\n}\n\n/**\n * Composite hook returning a patient's basic details — Hint demographics\n * + memberships + account, Elation demographics + clinical metadata —\n * from the Convex cache.\n *\n * On mount (and when inputs change) fires a background refresh against\n * `/api/patients/basic/refresh` so stale rows get pulled fresh without\n * blocking render. Returns cached data immediately; Convex subscription\n * updates the UI when refresh completes.\n */\nfunction usePatientBasic(\n input: { hintId?: string; elationId?: number },\n options?: UsePatientBasicOptions,\n): UsePatientBasicResult {\n const elationRow = useQuery(\n elationPatientByIdRef as FunctionReference<\"query\">,\n input.elationId !== undefined ? { elationId: input.elationId } : \"skip\",\n ) as Record<string, unknown> | null | undefined;\n\n const hintRow = useQuery(\n hintPatientByIdRef as FunctionReference<\"query\">,\n input.hintId !== undefined ? { hintId: input.hintId } : \"skip\",\n ) as Record<string, unknown> | null | undefined;\n\n useEffect(() => {\n if (options?.skipRefresh) {\n return;\n }\n if (!input.hintId && input.elationId === undefined) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/basic/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({\n hintId: input.hintId,\n elationId: input.elationId,\n }),\n signal: controller.signal,\n }).catch(() => {\n /* background refresh — failures are non-fatal */\n });\n\n return () => controller.abort();\n }, [\n input.hintId,\n input.elationId,\n options?.apiBaseUrl,\n options?.apiKey,\n options?.skipRefresh,\n ]);\n\n // Surface the raw payload under elationPatient/hintPatient so consumers\n // can keep using the exact Elation/Hint field names (first_name,\n // memberships, account.past_due_in_cents) with zero mapping.\n const elationPatient =\n elationRow === undefined\n ? undefined\n : elationRow === null\n ? null\n : ((elationRow.raw as Record<string, unknown>) ?? null);\n const hintPatient =\n hintRow === undefined\n ? undefined\n : hintRow === null\n ? null\n : ((hintRow.raw as Record<string, unknown>) ?? null);\n\n const elationLoading =\n input.elationId !== undefined && elationRow === undefined;\n const hintLoading = input.hintId !== undefined && hintRow === undefined;\n\n return {\n elationPatient,\n hintPatient,\n elationRow,\n hintRow,\n loading: elationLoading || hintLoading,\n };\n}\n\n/**\n * Read a shared pharmacy row by NCPDP id. Backs the pharmacy card in\n * the patient panel (from Elation's preferred_pharmacy reference).\n */\nfunction usePharmacyByNcpdpId(ncpdpId: string | undefined) {\n return useQuery(\n pharmacyByNcpdpRef as FunctionReference<\"query\">,\n ncpdpId ? { ncpdpId } : \"skip\",\n );\n}\n\ninterface UsePatientPhotoOptions {\n apiBaseUrl?: string;\n apiKey?: string;\n skipRefresh?: boolean;\n}\n\n/**\n * Subscribe to a patient's profile photo (s3Key + metadata). The\n * consumer constructs a download URL via the Truth attachments resource\n * (signed S3 URL). Fires a background refresh to pull the latest photo\n * binary from Elation + upload to S3.\n */\nfunction usePatientPhoto(\n elationId: number | undefined,\n options?: UsePatientPhotoOptions,\n) {\n const photo = useQuery(\n patientPhotoByIdRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n );\n\n useEffect(() => {\n if (options?.skipRefresh) {\n return;\n }\n if (elationId === undefined) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/photo/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({ elationId }),\n signal: controller.signal,\n }).catch(() => {\n /* non-fatal */\n });\n\n return () => controller.abort();\n }, [elationId, options?.apiBaseUrl, options?.apiKey, options?.skipRefresh]);\n\n return photo;\n}\n\n// ---------------------------------------------------------------------------\n// Conversation messages (Dialpad calls + SMS merged)\n// ---------------------------------------------------------------------------\n\ninterface ConversationMessage {\n kind: \"call\" | \"sms\";\n id: string;\n providerId: string;\n state: string | null;\n direction: string | null;\n fromNumber: string | null;\n toNumber: string | null;\n voicemailLink: string | null;\n duration: number | null;\n text: string | null;\n mms: boolean;\n mmsUrl: string | null;\n messageStatus: string | null;\n occurredAt: string;\n conversationId: string | null;\n patientId: string | null;\n}\n\nconst messagesByPhonesRef = makeFunctionReference<\n \"query\",\n { phoneA: string; phoneB: string; limit?: number },\n ConversationMessage[]\n>(\"conversationMessages:getByPhones\");\n\nconst messagesByConversationIdRef = makeFunctionReference<\n \"query\",\n { conversationId: string; limit?: number },\n ConversationMessage[]\n>(\"conversationMessages:getByConversationId\");\n\ninterface UseConversationMessagesOptions {\n /** Max items to return (default 200). */\n limit?: number;\n}\n\n/**\n * Subscribe to a conversation's calls + SMS merged chronologically.\n * Pass the patient phone + the provider phone — Truth computes a\n * normalized pair key server-side so formatting differences don't\n * matter.\n *\n * Returns `undefined` while loading, then `ConversationMessage[]`\n * sorted newest-first. Updates live as new webhook events land in\n * Convex via the kinesis consumer.\n */\nfunction useConversationMessages(\n input: { phoneA?: string; phoneB?: string; conversationId?: string },\n options?: UseConversationMessagesOptions,\n): ConversationMessage[] | undefined {\n const hasPair = !!input.phoneA && !!input.phoneB;\n const byPair = useQuery(\n messagesByPhonesRef as FunctionReference<\"query\">,\n hasPair\n ? {\n phoneA: input.phoneA as string,\n phoneB: input.phoneB as string,\n limit: options?.limit,\n }\n : \"skip\",\n ) as ConversationMessage[] | undefined;\n\n const byConvo = useQuery(\n messagesByConversationIdRef as FunctionReference<\"query\">,\n !hasPair && input.conversationId\n ? { conversationId: input.conversationId, limit: options?.limit }\n : \"skip\",\n ) as ConversationMessage[] | undefined;\n\n return hasPair ? byPair : byConvo;\n}\n\nexport {\n usePatients,\n usePatient,\n usePatientByElationId,\n usePatientByHintId,\n useAppointments,\n useAppointment,\n useAppointmentByElationId,\n usePhysiciansByElationIds,\n usePhysicianByElationId,\n usePatientMedical,\n usePatientBasic,\n usePharmacyByNcpdpId,\n usePatientPhoto,\n useConversationMessages,\n};\nexport type { ConversationMessage, UseConversationMessagesOptions };\nexport type {\n UsePatientListOptions,\n UseAppointmentListOptions,\n UsePatientMedicalOptions,\n UsePatientBasicOptions,\n UsePatientBasicResult,\n UsePatientPhotoOptions,\n Physician,\n};\n","/**\n * useNotifications — Truth SDK React hook for push notifications.\n *\n * Shape-compatible with `expo-notifications` where practical so\n * consumers port with minimal change. The hook dynamically imports\n * `expo-notifications` so the SDK doesn't hard-depend on Expo —\n * consumers running on a non-Expo runtime (e.g. a server test harness)\n * can still import the SDK without Metro blowing up.\n *\n * Exposes:\n * - permissionStatus — \"granted\" / \"denied\" / \"undetermined\"\n * - devicePushToken — native APNs/FCM token (undefined until register)\n * - register() — request permission, fetch token, register with Truth\n * - unregister() — revoke the device\n * - addReceivedListener / addResponseListener — re-exports of expo's\n * - getBadgeCount / setBadgeCount — re-exports of expo's\n *\n * Web push (VAPID subscription) lands in Phase 3.\n */\n\n\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n isWebPushSupported,\n registerServiceWorker,\n subscribeToPush,\n subscriptionToJSON,\n} from \"../web-push\";\n\n// `expo-notifications` is an optional peer dep. We lazy-import it via\n// a string-indirection so TypeScript + tsup don't try to resolve its\n// types at build time (the SDK ships without a hard dep so non-Expo\n// consumers — web, Node services — can still import from @hipnation-truth/sdk/react).\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ExpoNotificationsModule = any;\n\nasync function loadExpo(): Promise<ExpoNotificationsModule | null> {\n // Hermes rejects dynamic `import()`, so use Metro's `require()` which\n // is provided in every React Native module's scope. `expo-notifications`\n // is an optional peer dep — non-Expo consumers (web, Node) hit the\n // catch and we return null.\n try {\n // @ts-ignore — require is available at runtime in Metro/Node;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n return require(\"expo-notifications\") as ExpoNotificationsModule;\n } catch {\n return null;\n }\n}\n\nexport type PermissionStatus =\n | \"granted\"\n | \"denied\"\n | \"undetermined\"\n | \"unknown\";\n\nexport interface UseNotificationsOptions {\n /** Truth API base URL, e.g. https://app.truth.communication-hub.com */\n apiBaseUrl: string;\n /** `hn_live_*` API key for the caller's application. */\n apiKey: string;\n /** Current user id — used when registering the device. */\n userId: string | null | undefined;\n /** Optional app version string stored on the device row. */\n appVersion?: string;\n /**\n * Auto-register on mount when permission is already granted.\n * Default: true. Set false if you want to control the registration\n * lifecycle yourself.\n */\n autoRegister?: boolean;\n /** VAPID public key for web push. Fetched automatically if omitted. */\n vapidPublicKey?: string;\n /** Path to the service worker file. Default: \"/truth-sw.js\" */\n serviceWorkerPath?: string;\n}\n\nexport interface UseNotificationsResult {\n permissionStatus: PermissionStatus;\n devicePushToken: string | null;\n register: () => Promise<{ ok: boolean; reason?: string }>;\n unregister: () => Promise<void>;\n addReceivedListener: (listener: (n: unknown) => void) => () => void;\n addResponseListener: (listener: (r: unknown) => void) => () => void;\n getBadgeCount: () => Promise<number>;\n setBadgeCount: (count: number) => Promise<void>;\n}\n\nexport function useNotifications(\n options: UseNotificationsOptions,\n): UseNotificationsResult {\n const [permissionStatus, setPermissionStatus] =\n useState<PermissionStatus>(\"unknown\");\n const [devicePushToken, setDevicePushToken] = useState<string | null>(null);\n const expoRef = useRef<ExpoNotificationsModule | null>(null);\n const isWebRef = useRef(false);\n const vapidKeyRef = useRef<string | null>(options.vapidPublicKey ?? null);\n\n useEffect(() => {\n let mounted = true;\n void (async () => {\n const expo = await loadExpo();\n if (!mounted) {\n return;\n }\n expoRef.current = expo;\n\n if (expo) {\n try {\n const perm = await expo.getPermissionsAsync();\n if (!mounted) {\n return;\n }\n setPermissionStatus(mapStatus(perm?.status));\n } catch {\n setPermissionStatus(\"unknown\");\n }\n return;\n }\n\n if (isWebPushSupported()) {\n isWebRef.current = true;\n if (!vapidKeyRef.current) {\n try {\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/vapid-key`,\n {\n headers: {\n Accept: \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n },\n );\n if (res.ok) {\n const data = await res.json();\n vapidKeyRef.current = data.vapidPublicKey ?? null;\n }\n } catch {\n // best-effort\n }\n }\n if (!mounted) {\n return;\n }\n const webPerm =\n typeof Notification !== \"undefined\"\n ? Notification.permission\n : \"default\";\n setPermissionStatus(\n webPerm === \"granted\"\n ? \"granted\"\n : webPerm === \"denied\"\n ? \"denied\"\n : \"undetermined\",\n );\n } else {\n setPermissionStatus(\"unknown\");\n }\n })();\n return () => {\n mounted = false;\n };\n }, [options.apiBaseUrl, options.apiKey]);\n\n const register = useCallback(async () => {\n if (!options.userId) {\n return { ok: false, reason: \"missing_userId\" };\n }\n\n // Web push path\n if (isWebRef.current) {\n const vapidKey = vapidKeyRef.current;\n if (!vapidKey) {\n return { ok: false, reason: \"no_vapid_key\" };\n }\n\n const webPerm = await Notification.requestPermission();\n setPermissionStatus(\n webPerm === \"granted\"\n ? \"granted\"\n : webPerm === \"denied\"\n ? \"denied\"\n : \"undetermined\",\n );\n if (webPerm !== \"granted\") {\n return { ok: false, reason: \"permission_denied\" };\n }\n\n try {\n const swPath = options.serviceWorkerPath ?? \"/truth-sw.js\";\n const registration = await registerServiceWorker(swPath);\n await navigator.serviceWorker.ready;\n const subscription = await subscribeToPush(registration, vapidKey);\n const subJSON = subscriptionToJSON(subscription);\n setDevicePushToken(subscription.endpoint);\n\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/devices/register`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({\n userId: options.userId,\n platform: \"web\",\n webPushSubscription: subJSON,\n appVersion: options.appVersion,\n locale: navigator.language,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n }),\n },\n );\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n return {\n ok: false,\n reason: `register_failed_${res.status}: ${text.slice(0, 120)}`,\n };\n }\n return { ok: true };\n } catch (err) {\n return {\n ok: false,\n reason: `web_push_error: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n }\n\n // Native (expo-notifications) path\n const expo = expoRef.current ?? (await loadExpo());\n expoRef.current = expo;\n if (!expo) {\n return { ok: false, reason: \"expo_notifications_missing\" };\n }\n\n let perm = await expo.getPermissionsAsync();\n if (perm?.status !== \"granted\") {\n perm = await expo.requestPermissionsAsync();\n }\n setPermissionStatus(mapStatus(perm?.status));\n if (perm?.status !== \"granted\") {\n return { ok: false, reason: \"permission_denied\" };\n }\n\n const tokenResp = await expo.getDevicePushTokenAsync();\n const nativeToken = tokenResp?.data;\n const platform = detectPlatform(tokenResp?.type);\n if (!nativeToken || (platform !== \"ios\" && platform !== \"android\")) {\n return { ok: false, reason: \"no_native_token\" };\n }\n\n setDevicePushToken(nativeToken);\n\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/devices/register`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({\n userId: options.userId,\n platform,\n nativeToken,\n appVersion: options.appVersion,\n locale:\n typeof navigator !== \"undefined\" ? navigator.language : undefined,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n }),\n },\n );\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n return {\n ok: false,\n reason: `register_failed_${res.status}: ${text.slice(0, 120)}`,\n };\n }\n return { ok: true };\n }, [\n options.apiBaseUrl,\n options.apiKey,\n options.userId,\n options.appVersion,\n options.serviceWorkerPath,\n ]);\n\n const unregister = useCallback(async () => {\n if (!devicePushToken) {\n return;\n }\n await fetch(`${options.apiBaseUrl}/api/notifications/devices/unregister`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({ nativeToken: devicePushToken }),\n }).catch(() => {\n /* non-fatal */\n });\n setDevicePushToken(null);\n }, [options.apiBaseUrl, options.apiKey, devicePushToken]);\n\n const addReceivedListener = useCallback(\n (listener: (n: unknown) => void): (() => void) => {\n if (isWebRef.current) {\n if (\n typeof navigator === \"undefined\" ||\n !(\"serviceWorker\" in navigator)\n ) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === \"TRUTH_PUSH_RECEIVED\") {\n listener(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () =>\n navigator.serviceWorker.removeEventListener(\"message\", handler);\n }\n const expo = expoRef.current;\n if (!expo?.addNotificationReceivedListener) {\n return () => {};\n }\n const sub = expo.addNotificationReceivedListener(listener);\n return () => sub.remove?.();\n },\n [],\n );\n\n const addResponseListener = useCallback(\n (listener: (r: unknown) => void): (() => void) => {\n if (isWebRef.current) {\n if (\n typeof navigator === \"undefined\" ||\n !(\"serviceWorker\" in navigator)\n ) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === \"TRUTH_PUSH_TAPPED\") {\n listener(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () =>\n navigator.serviceWorker.removeEventListener(\"message\", handler);\n }\n const expo = expoRef.current;\n if (!expo?.addNotificationResponseReceivedListener) {\n return () => {};\n }\n const sub = expo.addNotificationResponseReceivedListener(listener);\n return () => sub.remove?.();\n },\n [],\n );\n\n const getBadgeCount = useCallback(async (): Promise<number> => {\n const expo = expoRef.current;\n if (!expo?.getBadgeCountAsync) {\n return 0;\n }\n return (await expo.getBadgeCountAsync()) ?? 0;\n }, []);\n\n const setBadgeCount = useCallback(async (count: number): Promise<void> => {\n const expo = expoRef.current;\n if (!expo?.setBadgeCountAsync) {\n return;\n }\n await expo.setBadgeCountAsync(count);\n }, []);\n\n // Auto-register once on mount when conditions are met\n const autoRegister = options.autoRegister !== false;\n useEffect(() => {\n if (!autoRegister) {\n return;\n }\n if (permissionStatus !== \"granted\") {\n return;\n }\n if (devicePushToken) {\n return;\n }\n if (!options.userId) {\n return;\n }\n void register();\n }, [\n autoRegister,\n permissionStatus,\n devicePushToken,\n options.userId,\n register,\n ]);\n\n return {\n permissionStatus,\n devicePushToken,\n register,\n unregister,\n addReceivedListener,\n addResponseListener,\n getBadgeCount,\n setBadgeCount,\n };\n}\n\nfunction mapStatus(status: string | undefined): PermissionStatus {\n if (status === \"granted\") {\n return \"granted\";\n }\n if (status === \"denied\") {\n return \"denied\";\n }\n if (status === \"undetermined\") {\n return \"undetermined\";\n }\n return \"unknown\";\n}\n\nfunction detectPlatform(\n tokenType: string | undefined,\n): \"ios\" | \"android\" | \"web\" | \"unknown\" {\n if (tokenType === \"apns\") {\n return \"ios\";\n }\n if (tokenType === \"fcm\") {\n return \"android\";\n }\n if (tokenType === \"web\") {\n return \"web\";\n }\n return \"unknown\";\n}\n","/**\n * Web Push helpers for browser environments.\n *\n * Handles service worker registration, VAPID push subscription, and\n * message forwarding from the service worker to the main thread.\n */\n\nexport interface WebPushConfig {\n vapidPublicKey: string;\n serviceWorkerPath?: string;\n}\n\nexport function isWebPushSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n \"PushManager\" in window\n );\n}\n\nexport async function registerServiceWorker(\n path = \"/truth-sw.js\",\n): Promise<ServiceWorkerRegistration> {\n return navigator.serviceWorker.register(path);\n}\n\nexport async function subscribeToPush(\n registration: ServiceWorkerRegistration,\n vapidPublicKey: string,\n): Promise<PushSubscription> {\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n return existing;\n }\n\n return registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(\n vapidPublicKey,\n ) as unknown as ArrayBuffer,\n });\n}\n\nexport function subscriptionToJSON(sub: PushSubscription): {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n} {\n const json = sub.toJSON();\n return {\n endpoint: sub.endpoint,\n keys: {\n p256dh: json.keys?.p256dh ?? \"\",\n auth: json.keys?.auth ?? \"\",\n },\n };\n}\n\nfunction urlBase64ToUint8Array(base64String: string): Uint8Array {\n const padding = \"=\".repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const rawData = atob(base64);\n const outputArray = new Uint8Array(rawData.length);\n for (let i = 0; i < rawData.length; ++i) {\n outputArray[i] = rawData.charCodeAt(i);\n }\n return outputArray;\n}\n\nexport function onServiceWorkerMessage(\n type: string,\n callback: (payload: unknown) => void,\n): () => void {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === type) {\n callback(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () => navigator.serviceWorker.removeEventListener(\"message\", handler);\n}\n","/**\n * TruthProvider — Convex React provider pre-configured for Truth.\n *\n * Wraps children in a ConvexProvider connected to the correct\n * Truth Convex deployment for the given environment.\n *\n * @example\n * ```tsx\n * import { TruthProvider } from '@hipnation-truth/sdk/react';\n *\n * function App() {\n * return (\n * <TruthProvider environment=\"sandbox\">\n * <MyApp />\n * </TruthProvider>\n * );\n * }\n * ```\n */\n\n\"use client\";\n\nimport { ConvexProvider, ConvexReactClient } from \"convex/react\";\nimport type { ReactNode } from \"react\";\nimport { createElement, useMemo } from \"react\";\n\n// Mirrors CONVEX_URLS in client.ts. UAT shares prod resources.\n//\n// Exported so `resolveConvexUrl` can be unit-tested without booting\n// React — a regression in this map shipped SDK 0.4.1 that pointed\n// UAT at sandbox Convex. The test in provider.test.ts locks every\n// stage → URL pair down.\nexport const CONVEX_URLS: Record<string, string> = {\n local: \"https://courteous-duck-623.convex.cloud\",\n staging: \"https://courteous-duck-623.convex.cloud\",\n stg: \"https://courteous-duck-623.convex.cloud\",\n sandbox: \"https://courteous-duck-623.convex.cloud\",\n uat: \"https://gallant-gecko-217.convex.cloud\",\n production: \"https://gallant-gecko-217.convex.cloud\",\n};\n\n/**\n * Resolve the Convex URL for a given environment. Honors an explicit\n * override and falls back to sandbox for unknown environments.\n */\nexport function resolveConvexUrl(\n environment: string | undefined,\n override?: string,\n): string {\n if (override) {\n return override;\n }\n const env = environment ?? \"sandbox\";\n return CONVEX_URLS[env] ?? CONVEX_URLS.sandbox;\n}\n\ninterface TruthProviderProps {\n /** Truth environment — determines which Convex deployment to connect to */\n environment?: string;\n /** Override the Convex URL directly */\n convexUrl?: string;\n children: ReactNode;\n}\n\nfunction TruthProvider({\n environment = \"sandbox\",\n convexUrl,\n children,\n}: TruthProviderProps) {\n const url = resolveConvexUrl(environment, convexUrl);\n\n const client = useMemo(() => new ConvexReactClient(url), [url]);\n\n return createElement(ConvexProvider, { client }, children);\n}\n\nexport { TruthProvider };\nexport type { TruthProviderProps };\n","/**\n * React tracking hook — fire-and-forget event tracking from client components.\n *\n * Creates a lightweight Tracker instance scoped to the browser session\n * and exposes a useTrack() hook for emitting events to Truth's Kinesis stream.\n *\n * @example\n * ```tsx\n * import { useTruth } from '@hipnation-truth/sdk/react';\n *\n * function ConversationView({ conversationId }) {\n * const truth = useTruth();\n *\n * useEffect(() => {\n * truth.track('conversation.marked_read.v1', {\n * read_by_actor_id: userId,\n * unread_count_before: 5,\n * unread_count_after: 0,\n * }, { subject: { conversation_id: conversationId } });\n * }, [conversationId]);\n * }\n * ```\n */\n\n\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { createContext, createElement, useContext, useMemo } from \"react\";\nimport type {\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"../tracking/events\";\nimport { Tracker } from \"../tracking/tracker\";\nimport type { ActorContext } from \"../types/config\";\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\ninterface TruthTrackingContextValue {\n track: <T extends EventType>(\n eventType: T,\n payload: EventPayloadMap[T],\n options?: TrackOptions,\n ) => void;\n identify: (actorId: string, actorType: ActorContext[\"actorType\"]) => void;\n}\n\nconst TruthTrackingContext = createContext<TruthTrackingContextValue | null>(\n null,\n);\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\ninterface TruthTrackingProviderProps {\n /** Truth environment — determines API URL for event delivery */\n environment?: string;\n /** Event source identifier */\n source?: string;\n /** Source version (git SHA) */\n sourceVersion?: string;\n /** Default tenant ID */\n tenantId?: string;\n /** API key for authentication */\n apiKey?: string;\n children: ReactNode;\n}\n\nfunction TruthTrackingProvider({\n environment = \"sandbox\",\n source = \"communication-hub.frontend\",\n sourceVersion = \"unknown\",\n tenantId = \"hipnation\",\n apiKey = \"\",\n children,\n}: TruthTrackingProviderProps) {\n const value = useMemo<TruthTrackingContextValue>(() => {\n const tracker = new Tracker({\n apiKey,\n environment,\n source,\n sourceVersion,\n tenantId,\n batchSize: 10,\n flushIntervalMs: 5000,\n });\n\n return {\n track: (eventType, payload, options) => {\n tracker.track(eventType, payload, options);\n },\n identify: (actorId, actorType) => {\n tracker.setActor({ actorId, actorType });\n },\n };\n }, [apiKey, environment, source, sourceVersion, tenantId]);\n\n return createElement(TruthTrackingContext.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Access the Truth tracking context. Must be within a TruthTrackingProvider.\n * Returns `{ track, identify }` for emitting events to Kinesis.\n */\nfunction useTruth(): TruthTrackingContextValue {\n const ctx = useContext(TruthTrackingContext);\n if (!ctx) {\n throw new Error(\"useTruth must be used within a TruthTrackingProvider\");\n }\n return ctx;\n}\n\nexport { TruthTrackingProvider, useTruth };\nexport type { TruthTrackingProviderProps, TruthTrackingContextValue };\n","/**\n * Event tracker with batching, retry, and flush support.\n *\n * Buffers events in an internal queue and flushes them to the Truth API\n * endpoint. Flushes occur when the buffer reaches `batchSize` or every\n * `flushIntervalMs` milliseconds, whichever comes first.\n */\n\nimport type { ActorContext } from \"../types/config\";\nimport type {\n EventEnvelope,\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"./events\";\n\n// ---------------------------------------------------------------------------\n// UUID v7 helper (no external dependency)\n// ---------------------------------------------------------------------------\n\n/**\n * Generates a UUID v7 string. Uses crypto.randomUUID where available,\n * falling back to a timestamp + random bytes implementation.\n *\n * UUID v7 layout (RFC 9562):\n * 48 bits - unix timestamp (ms)\n * 4 bits - version (0b0111)\n * 12 bits - random\n * 2 bits - variant (0b10)\n * 62 bits - random\n */\nfunction generateUuidV7(): string {\n const now = Date.now();\n\n // 6 bytes of timestamp\n const timeBytes = new Uint8Array(6);\n let ts = now;\n for (let i = 5; i >= 0; i--) {\n timeBytes[i] = ts & 0xff;\n ts = Math.floor(ts / 256);\n }\n\n // 10 bytes of random\n const randomBytes = new Uint8Array(10);\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n globalThis.crypto.getRandomValues\n ) {\n globalThis.crypto.getRandomValues(randomBytes);\n } else {\n for (let i = 0; i < 10; i++) {\n randomBytes[i] = Math.floor(Math.random() * 256);\n }\n }\n\n // Assemble 16 bytes\n const bytes = new Uint8Array(16);\n bytes.set(timeBytes, 0);\n bytes.set(randomBytes, 6);\n\n // Set version (bits 48-51 to 0b0111)\n bytes[6] = (bytes[6] & 0x0f) | 0x70;\n\n // Set variant (bits 64-65 to 0b10)\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n\n // Format as hex string with dashes\n const hex = Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return [\n hex.slice(0, 8),\n hex.slice(8, 12),\n hex.slice(12, 16),\n hex.slice(16, 20),\n hex.slice(20, 32),\n ].join(\"-\");\n}\n\n// ---------------------------------------------------------------------------\n// Environment-based API URL resolution\n// ---------------------------------------------------------------------------\n\n// Environment → Truth API base URL.\n//\n// Topology:\n// local / staging / stg / sandbox → sandbox Truth (app.sandbox.*)\n// uat / production → production Truth (app.truth.*)\n//\n// UAT shares production resources (same Fly Postgres + Hasura + Truth +\n// Convex + EHR tokens as prod). Staging is the isolated sandbox env.\nconst API_URLS: Record<string, string> = {\n local: \"http://localhost:3000\",\n staging: \"https://app.sandbox.communication-hub.com\",\n stg: \"https://app.sandbox.communication-hub.com\",\n sandbox: \"https://app.sandbox.communication-hub.com\",\n uat: \"https://app.truth.communication-hub.com\",\n production: \"https://app.truth.communication-hub.com\",\n};\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_BATCH_SIZE = 25;\nconst DEFAULT_FLUSH_INTERVAL_MS = 5_000;\nconst MAX_RETRIES = 3;\nconst BASE_RETRY_DELAY_MS = 500;\n\n// ---------------------------------------------------------------------------\n// Tracker configuration\n// ---------------------------------------------------------------------------\n\ninterface TrackerConfig {\n apiKey: string;\n environment: string;\n source: string;\n sourceVersion: string;\n tenantId: string;\n batchSize: number;\n flushIntervalMs: number;\n apiBaseUrl?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Tracker class\n// ---------------------------------------------------------------------------\n\nclass Tracker {\n private readonly config: TrackerConfig;\n readonly apiUrl: string;\n private queue: EventEnvelope[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private defaultActor: ActorContext | undefined;\n private isFlushing = false;\n private isShutdown = false;\n\n constructor(config: TrackerConfig) {\n this.config = config;\n this.apiUrl =\n config.apiBaseUrl ?? API_URLS[config.environment] ?? API_URLS.local;\n\n this.startFlushInterval();\n this.registerShutdownHooks();\n }\n\n /**\n * Set the default actor context for subsequent events.\n */\n setActor(actor: ActorContext): void {\n this.defaultActor = actor;\n }\n\n /**\n * Enqueue a typed event for delivery. This is fire-and-forget from\n * the caller's perspective -- events are buffered and flushed in batches.\n */\n track<T extends EventType>(\n eventType: T,\n payload: EventPayloadMap[T],\n options?: TrackOptions,\n ): void {\n if (this.isShutdown) {\n return;\n }\n\n const now = new Date().toISOString();\n\n const envelope: EventEnvelope = {\n event_id: generateUuidV7(),\n event_type: eventType,\n schema_version: 1,\n occurred_at: options?.occurredAt ?? now,\n received_at: now,\n source: this.config.source,\n source_version: this.config.sourceVersion,\n tenant_id: options?.tenantId ?? this.config.tenantId,\n actor:\n options?.actor ??\n (this.defaultActor\n ? {\n actor_id: this.defaultActor.actorId,\n actor_type: this.defaultActor.actorType,\n }\n : undefined),\n subject: options?.subject,\n compliance: options?.compliance,\n payload: payload as unknown as Record<string, unknown>,\n };\n\n this.queue.push(envelope);\n\n if (this.queue.length >= this.config.batchSize) {\n void this.flush();\n }\n }\n\n /**\n * Force an immediate flush of all buffered events.\n * Returns a promise that resolves when the flush completes.\n */\n async flush(): Promise<void> {\n if (this.queue.length === 0 || this.isFlushing) {\n return;\n }\n\n this.isFlushing = true;\n const batch = this.queue.splice(0, this.config.batchSize);\n\n try {\n await this.sendBatch(batch);\n } catch {\n // Re-queue events that failed to send (prepend to maintain ordering)\n this.queue.unshift(...batch);\n } finally {\n this.isFlushing = false;\n }\n\n // If there are still events in the queue, flush again\n if (this.queue.length >= this.config.batchSize) {\n await this.flush();\n }\n }\n\n /**\n * Gracefully shut down the tracker. Flushes remaining events and\n * clears the flush interval.\n */\n async shutdown(): Promise<void> {\n this.isShutdown = true;\n this.stopFlushInterval();\n await this.flush();\n }\n\n /**\n * Send a batch of events to the Truth API with exponential backoff retry.\n */\n private async sendBatch(batch: EventEnvelope[]): Promise<void> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(`${this.apiUrl}/api/events/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": this.config.apiKey,\n },\n body: JSON.stringify({ events: batch }),\n });\n\n if (response.ok) {\n return;\n }\n\n // Don't retry 4xx client errors (except 429)\n if (\n response.status >= 400 &&\n response.status < 500 &&\n response.status !== 429\n ) {\n return;\n }\n\n lastError = new Error(\n `HTTP ${response.status}: ${response.statusText}`,\n );\n } catch (error) {\n lastError = error;\n }\n\n // Exponential backoff with jitter\n if (attempt < MAX_RETRIES) {\n const delay = BASE_RETRY_DELAY_MS * 2 ** attempt;\n const jitter = Math.random() * delay * 0.5;\n await sleep(delay + jitter);\n }\n }\n\n throw lastError;\n }\n\n private startFlushInterval(): void {\n if (this.config.flushIntervalMs > 0) {\n this.flushTimer = setInterval(() => {\n void this.flush();\n }, this.config.flushIntervalMs);\n\n // Unref the timer so it doesn't prevent Node.js from exiting\n if (typeof this.flushTimer === \"object\" && \"unref\" in this.flushTimer) {\n this.flushTimer.unref();\n }\n }\n }\n\n private stopFlushInterval(): void {\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n }\n\n private registerShutdownHooks(): void {\n // Only register in Node.js environments\n if (typeof globalThis.process !== \"undefined\" && globalThis.process.on) {\n const shutdownHandler = () => {\n void this.shutdown();\n };\n\n globalThis.process.on(\"beforeExit\", shutdownHandler);\n globalThis.process.on(\"SIGTERM\", shutdownHandler);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport {\n Tracker,\n generateUuidV7,\n DEFAULT_BATCH_SIZE,\n DEFAULT_FLUSH_INTERVAL_MS,\n};\nexport type { TrackerConfig };\n"],"mappings":"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","names":["import_react","import_server","import_react","_a","res","import_react","import_react"]}
|
package/dist/react.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
var __pow = Math.pow;
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
3
9
|
var __async = (__this, __arguments, generator) => {
|
|
4
10
|
return new Promise((resolve, reject) => {
|
|
5
11
|
var fulfilled = (value) => {
|
|
@@ -347,12 +353,7 @@ function urlBase64ToUint8Array(base64String) {
|
|
|
347
353
|
function loadExpo() {
|
|
348
354
|
return __async(this, null, function* () {
|
|
349
355
|
try {
|
|
350
|
-
return
|
|
351
|
-
/* @vite-ignore */
|
|
352
|
-
/* webpackIgnore: true */
|
|
353
|
-
// @ts-ignore — expo-notifications is an optional peer dep
|
|
354
|
-
"expo-notifications"
|
|
355
|
-
);
|
|
356
|
+
return __require("expo-notifications");
|
|
356
357
|
} catch (e) {
|
|
357
358
|
return null;
|
|
358
359
|
}
|
package/dist/react.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react/conversations.ts","../src/react/hooks.ts","../src/react/notifications.ts","../src/web-push.ts","../src/react/provider.ts","../src/react/tracking.ts","../src/tracking/tracker.ts"],"sourcesContent":["/**\n * React hooks for Truth SDK — reactive conversation + message data.\n *\n * Wraps Convex queries owned by the Truth backend so consumers (CommHub\n * Expo frontend) get live-updating conversation lists, message threads,\n * and unread totals without managing subscriptions themselves.\n *\n * **Hook contract:** every hook in this file returns\n * `{ data, loading, error }`. `data` is `undefined` while loading and\n * also when the underlying query is `\"skip\"`'d (caller didn't pass the\n * required arg yet). `loading` is `true` only while a real subscription\n * is in-flight; once `data` resolves (even to `null`) `loading` flips\n * to `false`. `error` is reserved for SDK-side validation — Convex\n * itself throws on subscribe rather than returning an error value, so\n * unhandled query errors propagate as React errors and should be caught\n * with an error boundary.\n *\n * **Backed by PR #110 schema (commit `41dbb59` on\n * `feat/migrate-dialpad-webhook-and-messages-to-truth`):**\n * - `conversations:listForUser` → `useConversations`\n * - `conversations:getUnreadTotalForUser` → `useUnreadCount`\n * - `conversations:getByPhonePair` → `useConversationByPhonePair`\n * - `conversationMessages:getByConversationId` → `useMessages`\n *\n * **Deliberately NOT shipped here (no backend query yet — flagged in\n * PR #111 body for follow-up):**\n * - Single-conversation-by-id lookup. Convex `conversations` has no\n * `get(id)` query — only `getByPhonePair`. CommHub uses phonePair as\n * the primary handle, so the byPair flavor is what consumers need;\n * if id-based lookup is needed later we can add a `conversations:get`\n * in Truth.\n * - Participants / \"seen by\" hook. The migrated schema has no\n * `conversationParticipants` table — read state is per-user via\n * `conversationReads` and there's no public list query for it yet.\n * - `markRead` mutation. PR #110 declares `markRead` as\n * `internalMutation`, so the frontend can't invoke it directly — it\n * has to go through a Truth HTTP endpoint, or webhook-migrator needs\n * to flip it to a public `mutation`. Flagged for a follow-up.\n *\n * Must be used within `<TruthProvider />` (see `./provider`).\n *\n * @example\n * ```tsx\n * import {\n * useConversations,\n * useConversationByPhonePair,\n * useMessages,\n * useUnreadCount,\n * } from '@hipnation-truth/sdk/react';\n *\n * function Inbox({ userId }: { userId: string }) {\n * const { data: convos, loading } = useConversations({ userId });\n * const { data: unread } = useUnreadCount(userId);\n * if (loading) return <Spinner />;\n * return (\n * <div>\n * <Badge count={unread ?? 0} />\n * {convos?.map((c) => <ConvoRow key={c.id} convo={c} />)}\n * </div>\n * );\n * }\n * ```\n */\n\n\"use client\";\n\nimport { useQuery } from \"convex/react\";\nimport type { FunctionReference } from \"convex/server\";\nimport { makeFunctionReference } from \"convex/server\";\n\n// ---------------------------------------------------------------------------\n// Shared types\n// ---------------------------------------------------------------------------\n//\n// Mirror the shapes returned by the Convex queries on PR #110. Once\n// the consuming app regenerates `convex/_generated/api.d.ts` against\n// the merged schema these can be replaced with inferred types — for\n// today they give consumers useful autocomplete.\n\n/**\n * Row shape returned by `conversations:listForUser` — joins the base\n * `conversations` row with the caller's per-user `conversationReads`\n * entry so unread state lives on each item.\n */\nexport interface ConversationListItem {\n /** Convex id of the conversation row. */\n id: string;\n patientPhone: string;\n providerPhone: string;\n /** Sorted-digit-pair key (commutative — same regardless of direction). */\n phonePair: string;\n /** Truth-side patient id, when the kinesis pipeline could resolve it. */\n patientId: string | null;\n /** ISO timestamp of the most recent message in the conversation. */\n lastMessageAt: string;\n /** Caller's unread count for this conversation. */\n unreadCount: number;\n /** ISO timestamp of when the caller last marked the conversation read. */\n lastReadAt: string | null;\n}\n\n/**\n * Raw `conversations` table row — what `getByPhonePair` returns. No\n * unread / read joining; for that, prefer `useConversations`.\n */\nexport interface ConversationRow {\n _id: string;\n _creationTime: number;\n patientPhone: string;\n providerPhone: string;\n phonePair: string;\n patientId: string | null;\n lastEventId: string | null;\n lastMessageAt: string;\n createdAt: string;\n}\n\n/**\n * Message row returned by `conversationMessages:getByConversationId` —\n * calls + SMS merged chronologically. Mirrors `ConversationMessage`\n * already exported from the SDK; re-exported here under a clearer name\n * for the new hook surface.\n */\nexport interface ConversationMessageRow {\n kind: \"call\" | \"sms\";\n id: string;\n providerId: string;\n state: string | null;\n direction: string | null;\n fromNumber: string | null;\n toNumber: string | null;\n voicemailLink: string | null;\n duration: number | null;\n text: string | null;\n mms: boolean;\n mmsUrl: string | null;\n messageStatus: string | null;\n occurredAt: string;\n conversationId: string | null;\n patientId: string | null;\n}\n\nexport interface UseQueryResult<T> {\n /**\n * Query data. `undefined` while loading or while the query is\n * intentionally skipped (e.g. caller hasn't supplied `userId` yet).\n */\n data: T | undefined;\n /** True only while a real subscription is in-flight. */\n loading: boolean;\n /**\n * Reserved for client-side validation errors surfaced by the SDK\n * itself. Convex query errors propagate as React errors and should\n * be caught with an error boundary.\n */\n error: Error | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Convex function references — string-keyed, decoupled from the\n// consuming app's generated `convex/_generated/api`. Names below match\n// the queries on PR #110 (commit 41dbb59) verbatim.\n// ---------------------------------------------------------------------------\n\nconst conversationsListForUserRef = makeFunctionReference<\n \"query\",\n { userId: string; limit?: number },\n ConversationListItem[]\n>(\"conversations:listForUser\");\n\nconst conversationsGetUnreadTotalForUserRef = makeFunctionReference<\n \"query\",\n { userId: string },\n number\n>(\"conversations:getUnreadTotalForUser\");\n\nconst conversationsGetByPhonePairRef = makeFunctionReference<\n \"query\",\n { phonePair: string },\n ConversationRow | null\n>(\"conversations:getByPhonePair\");\n\n// Lives on the existing `conversationMessages` module (pre-#110). PR\n// #110 added `conversationId` indexing on `messageSms` / `messageCalls`,\n// so this query is now reliable for the new conversations table.\nconst conversationMessagesGetByConversationIdRef = makeFunctionReference<\n \"query\",\n { conversationId: string; limit?: number },\n ConversationMessageRow[]\n>(\"conversationMessages:getByConversationId\");\n\n// ---------------------------------------------------------------------------\n// Skipped-query sentinel + result shaping\n// ---------------------------------------------------------------------------\n\nconst SKIP = \"skip\" as const;\n\nfunction toResult<T>(\n value: T | undefined,\n skipped: boolean,\n): UseQueryResult<T> {\n if (skipped) {\n return { data: undefined, loading: false, error: undefined };\n }\n return {\n data: value,\n loading: value === undefined,\n error: undefined,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Filters + options\n// ---------------------------------------------------------------------------\n\nexport interface UseConversationsFilters {\n /**\n * Truth user id (the Better Auth subject). Pass `undefined` to skip\n * the query — useful when the auth session is still loading.\n */\n userId: string | null | undefined;\n /** Page size. Server caps at 100 by default. */\n limit?: number;\n}\n\nexport interface UseMessagesOptions {\n /** Page size. Server caps at 200 by default. */\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to a user's conversations — joined with their per-conversation\n * unread count, sorted by most recent message. Updates live as new\n * SMS / calls land in Convex and the webhook bumps `unreadCount`.\n *\n * Returns `{ data: undefined, loading: false }` while the auth session\n * resolves (`userId === undefined`); not an error, just a skip.\n */\nfunction useConversations(\n filters: UseConversationsFilters,\n): UseQueryResult<ConversationListItem[]> {\n const skipped = !filters.userId;\n const result = useQuery(\n conversationsListForUserRef as FunctionReference<\"query\">,\n skipped\n ? SKIP\n : {\n userId: filters.userId as string,\n limit: filters.limit,\n },\n ) as ConversationListItem[] | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Look up a single conversation by its normalized phonePair (sorted\n * digit-pair, e.g. `\"5125550123|6505551234\"`). Returns `null` if no\n * conversation exists yet for that pair.\n *\n * Use the `phonePairKey(patientPhone, providerPhone)` helper exposed by\n * the Truth Convex module to derive the key from raw phone strings.\n */\nfunction useConversationByPhonePair(\n phonePair: string | null | undefined,\n): UseQueryResult<ConversationRow | null> {\n const skipped = !phonePair;\n const result = useQuery(\n conversationsGetByPhonePairRef as FunctionReference<\"query\">,\n skipped ? SKIP : { phonePair: phonePair as string },\n ) as ConversationRow | null | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Subscribe to a paginated message stream for a single conversation.\n * Calls + SMS merged chronologically, newest-first. Live as the\n * kinesis consumer writes to `messageCalls` / `messageSms`.\n *\n * Pass the Convex `_id` of the conversation row (from\n * `useConversations` / `useConversationByPhonePair`) as\n * `conversationId`.\n */\nfunction useMessages(\n conversationId: string | null | undefined,\n options?: UseMessagesOptions,\n): UseQueryResult<ConversationMessageRow[]> {\n const skipped = !conversationId;\n const result = useQuery(\n conversationMessagesGetByConversationIdRef as FunctionReference<\"query\">,\n skipped\n ? SKIP\n : {\n conversationId: conversationId as string,\n limit: options?.limit,\n },\n ) as ConversationMessageRow[] | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Subscribe to the user's total unread message count across all\n * conversations. Backs the inbox tab badge in CommHub.\n *\n * Returns `0` when the underlying query resolves with no unread\n * messages, `undefined` while loading or when `userId` is missing.\n */\nfunction useUnreadCount(\n userId: string | null | undefined,\n): UseQueryResult<number> {\n const skipped = !userId;\n const result = useQuery(\n conversationsGetUnreadTotalForUserRef as FunctionReference<\"query\">,\n skipped ? SKIP : { userId: userId as string },\n ) as number | undefined;\n\n return toResult(result, skipped);\n}\n\nexport {\n useConversationByPhonePair,\n useConversations,\n useMessages,\n useUnreadCount,\n};\n","/**\n * React hooks for Truth SDK — real-time Convex-backed data access.\n *\n * These hooks use Convex React subscriptions for live-updating data.\n * Must be used within a ConvexProvider (see TruthProvider).\n *\n * @example\n * ```tsx\n * import { usePatients, useAppointments } from '@hipnation-truth/sdk/react';\n *\n * function PatientList() {\n * const patients = usePatients({ limit: 20 });\n * return patients?.map(p => <div key={p._id}>{p.firstName} {p.lastName}</div>);\n * }\n * ```\n */\n\n\"use client\";\n\nimport { useQuery } from \"convex/react\";\nimport { useEffect } from \"react\";\n\n// ---------------------------------------------------------------------------\n// Convex function references\n// ---------------------------------------------------------------------------\n// We use string-based references to avoid importing the generated API\n// (which lives in the consuming app, not the SDK). Convex supports this\n// via the `api` module pattern, but for a published SDK we use\n// makeFunctionReference to stay decoupled from the app's codegen.\n\nimport type { FunctionReference } from \"convex/server\";\nimport { makeFunctionReference } from \"convex/server\";\n\n// Patient query references\nconst patientsListRef = makeFunctionReference<\n \"query\",\n {\n search?: string;\n lastName?: string;\n cursor?: string;\n limit?: number;\n },\n unknown[]\n>(\"patients:list\");\n\nconst patientsGetRef = makeFunctionReference<\n \"query\",\n { id: string },\n unknown | null\n>(\"patients:get\");\n\nconst patientsByElationIdRef = makeFunctionReference<\n \"query\",\n { elationId: string },\n unknown | null\n>(\"patients:getByElationId\");\n\nconst patientsByHintIdRef = makeFunctionReference<\n \"query\",\n { hintId: string },\n unknown | null\n>(\"patients:getByHintId\");\n\n// Appointment query references\nconst appointmentsListRef = makeFunctionReference<\n \"query\",\n {\n patientId?: string;\n status?: string;\n startDate?: string;\n endDate?: string;\n limit?: number;\n },\n unknown[]\n>(\"appointments:list\");\n\nconst appointmentsGetRef = makeFunctionReference<\n \"query\",\n { id: string },\n unknown | null\n>(\"appointments:get\");\n\nconst appointmentsByElationIdRef = makeFunctionReference<\n \"query\",\n { elationId: string },\n unknown | null\n>(\"appointments:getByElationId\");\n\n// ---------------------------------------------------------------------------\n// Patient hooks\n// ---------------------------------------------------------------------------\n\ninterface UsePatientListOptions {\n search?: string;\n lastName?: string;\n limit?: number;\n}\n\n/**\n * Subscribe to a list of patients with optional search and filtering.\n * Returns undefined while loading, then an array of patients.\n */\nfunction usePatients(options?: UsePatientListOptions) {\n return useQuery(patientsListRef as FunctionReference<\"query\">, options ?? {});\n}\n\n/**\n * Subscribe to a single patient by Convex document ID.\n */\nfunction usePatient(id: string) {\n return useQuery(patientsGetRef as FunctionReference<\"query\">, { id });\n}\n\n/**\n * Subscribe to a patient by their Elation ID.\n */\nfunction usePatientByElationId(elationId: string) {\n return useQuery(patientsByElationIdRef as FunctionReference<\"query\">, {\n elationId,\n });\n}\n\n/**\n * Subscribe to a patient by their Hint ID.\n */\nfunction usePatientByHintId(hintId: string) {\n return useQuery(patientsByHintIdRef as FunctionReference<\"query\">, {\n hintId,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Appointment hooks\n// ---------------------------------------------------------------------------\n\ninterface UseAppointmentListOptions {\n patientId?: string;\n status?: string;\n startDate?: string;\n endDate?: string;\n limit?: number;\n}\n\n/**\n * Subscribe to a list of appointments with optional filtering.\n */\nfunction useAppointments(options?: UseAppointmentListOptions) {\n return useQuery(\n appointmentsListRef as FunctionReference<\"query\">,\n options ?? {},\n );\n}\n\n/**\n * Subscribe to a single appointment by Convex document ID.\n */\nfunction useAppointment(id: string) {\n return useQuery(appointmentsGetRef as FunctionReference<\"query\">, { id });\n}\n\n/**\n * Subscribe to an appointment by its Elation ID.\n */\nfunction useAppointmentByElationId(elationId: string) {\n return useQuery(appointmentsByElationIdRef as FunctionReference<\"query\">, {\n elationId,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Physician hooks\n// ---------------------------------------------------------------------------\n\ninterface Physician {\n _id: string;\n elationId: number;\n firstName?: string;\n lastName?: string;\n npi?: string;\n credentials?: string;\n specialties?: string[];\n practice?: number;\n email?: string;\n phone?: string;\n lastSyncedAt: string;\n}\n\nconst physiciansGetByElationIdsRef = makeFunctionReference<\n \"query\",\n { ids: number[] },\n Physician[]\n>(\"physicians:getByElationIds\");\n\nconst physiciansGetByElationIdRef = makeFunctionReference<\n \"query\",\n { id: number },\n Physician | null\n>(\"physicians:getByElationId\");\n\n/**\n * Resolve a batch of physicians by their Elation IDs. Returns an array\n * of physicians that exist in Convex; missing ids are silently dropped.\n *\n * Use this to resolve medication `prescribing_physician` / appointment\n * physician ids to display names, avoiding the per-physician HTTP\n * round-trip through Truth's Elation proxy.\n *\n * Pass `undefined` (or an empty array) to skip the query — the hook\n * returns `undefined` until you pass a populated list.\n */\nfunction usePhysiciansByElationIds(ids: number[] | undefined) {\n return useQuery(\n physiciansGetByElationIdsRef as FunctionReference<\"query\">,\n ids && ids.length > 0 ? { ids } : \"skip\",\n );\n}\n\n/**\n * Subscribe to a single physician by their Elation ID.\n */\nfunction usePhysicianByElationId(id: number | undefined) {\n return useQuery(\n physiciansGetByElationIdRef as FunctionReference<\"query\">,\n id !== undefined ? { id } : \"skip\",\n );\n}\n\n// ---------------------------------------------------------------------------\n// Patient medical hooks (medications, problems, allergies, appointments)\n// ---------------------------------------------------------------------------\n\nconst medicationsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getMedicationsByElationPatient\");\n\nconst problemsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getProblemsByElationPatient\");\n\nconst allergiesByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getAllergiesByElationPatient\");\n\nconst appointmentsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getAppointmentsByElationPatient\");\n\ninterface UsePatientMedicalOptions {\n /**\n * Base URL of the Truth API. Required to trigger the background refresh\n * (e.g. `https://app.truth.communication-hub.com`). If omitted, the hook\n * only reads from Convex and does not refresh.\n */\n apiBaseUrl?: string;\n /** API key used for the refresh call. */\n apiKey?: string;\n /**\n * If true, suppress the background refresh. Useful when you know the\n * data was just refreshed by another component on the page.\n */\n skipRefresh?: boolean;\n}\n\n/**\n * Composite hook that returns a patient's medical records — medications,\n * problems, allergies, appointments — from the Convex cache.\n *\n * On mount (and when `elationId` changes) fires a background refresh\n * against Truth's `/api/patients/medical/refresh` endpoint so stale data\n * is pulled in without blocking render. Returns cached data immediately;\n * Convex subscription updates the UI when refresh completes.\n */\nfunction usePatientMedical(\n elationId: number | undefined,\n options?: UsePatientMedicalOptions,\n) {\n const medications = useQuery(\n medicationsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const problems = useQuery(\n problemsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const allergies = useQuery(\n allergiesByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const appointments = useQuery(\n appointmentsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n useEffect(() => {\n if (elationId === undefined || options?.skipRefresh) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/medical/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({ elationId }),\n signal: controller.signal,\n }).catch(() => {\n /* background refresh — failures are non-fatal */\n });\n\n return () => controller.abort();\n }, [elationId, options?.apiBaseUrl, options?.apiKey, options?.skipRefresh]);\n\n return { medications, problems, allergies, appointments };\n}\n\n// ---------------------------------------------------------------------------\n// Patient basic hooks (Elation demographics + Hint memberships/account)\n// ---------------------------------------------------------------------------\n//\n// Replaces the CommHub `getPatientBasicDetails` Hasura Action. Reads\n// from Convex-cached rows (populated by the patients-backfill cron +\n// `/api/patients/basic/refresh` on-demand), falling back to an empty\n// state until the background refresh completes.\n\nconst elationPatientByIdRef = makeFunctionReference<\n \"query\",\n { elationId: number },\n unknown | null\n>(\"elationPatients:getByElationId\");\n\nconst hintPatientByIdRef = makeFunctionReference<\n \"query\",\n { hintId: string },\n unknown | null\n>(\"hintPatients:getByHintId\");\n\nconst pharmacyByNcpdpRef = makeFunctionReference<\n \"query\",\n { ncpdpId: string },\n unknown | null\n>(\"elationPharmacies:getByNcpdpId\");\n\nconst patientPhotoByIdRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown | null\n>(\"elationPatientPhotos:getByElationPatientId\");\n\ninterface UsePatientBasicOptions {\n /** Truth API base URL used for the background refresh. */\n apiBaseUrl?: string;\n /** API key for the refresh call. */\n apiKey?: string;\n /** Suppress the background refresh. */\n skipRefresh?: boolean;\n}\n\ninterface UsePatientBasicResult {\n /**\n * Raw Elation patient payload (matches the shape Elation's\n * `/patients/{id}` returns — `first_name`, `last_name`, `phones`, etc).\n * Returns `undefined` while the cache miss is loading, `null` if the\n * patient isn't in Convex yet (first-open-after-backfill).\n */\n elationPatient: Record<string, unknown> | null | undefined;\n /**\n * Raw Hint patient payload (matches the shape Hint's\n * `/provider/patients/{id}` returns — `memberships`, `account`,\n * `phones`, etc).\n */\n hintPatient: Record<string, unknown> | null | undefined;\n /**\n * The full Convex row for Elation (includes `elationId`, `lastSyncedAt`,\n * `photoS3Key`, `preferredPharmacyNcpdpId`, etc). Use this when you\n * need the structured/typed fields rather than the raw Elation payload.\n */\n elationRow: Record<string, unknown> | null | undefined;\n /**\n * The full Convex row for Hint. Similar relationship to `hintPatient`.\n */\n hintRow: Record<string, unknown> | null | undefined;\n /** True while either cache miss is still pending. */\n loading: boolean;\n}\n\n/**\n * Composite hook returning a patient's basic details — Hint demographics\n * + memberships + account, Elation demographics + clinical metadata —\n * from the Convex cache.\n *\n * On mount (and when inputs change) fires a background refresh against\n * `/api/patients/basic/refresh` so stale rows get pulled fresh without\n * blocking render. Returns cached data immediately; Convex subscription\n * updates the UI when refresh completes.\n */\nfunction usePatientBasic(\n input: { hintId?: string; elationId?: number },\n options?: UsePatientBasicOptions,\n): UsePatientBasicResult {\n const elationRow = useQuery(\n elationPatientByIdRef as FunctionReference<\"query\">,\n input.elationId !== undefined ? { elationId: input.elationId } : \"skip\",\n ) as Record<string, unknown> | null | undefined;\n\n const hintRow = useQuery(\n hintPatientByIdRef as FunctionReference<\"query\">,\n input.hintId !== undefined ? { hintId: input.hintId } : \"skip\",\n ) as Record<string, unknown> | null | undefined;\n\n useEffect(() => {\n if (options?.skipRefresh) {\n return;\n }\n if (!input.hintId && input.elationId === undefined) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/basic/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({\n hintId: input.hintId,\n elationId: input.elationId,\n }),\n signal: controller.signal,\n }).catch(() => {\n /* background refresh — failures are non-fatal */\n });\n\n return () => controller.abort();\n }, [\n input.hintId,\n input.elationId,\n options?.apiBaseUrl,\n options?.apiKey,\n options?.skipRefresh,\n ]);\n\n // Surface the raw payload under elationPatient/hintPatient so consumers\n // can keep using the exact Elation/Hint field names (first_name,\n // memberships, account.past_due_in_cents) with zero mapping.\n const elationPatient =\n elationRow === undefined\n ? undefined\n : elationRow === null\n ? null\n : ((elationRow.raw as Record<string, unknown>) ?? null);\n const hintPatient =\n hintRow === undefined\n ? undefined\n : hintRow === null\n ? null\n : ((hintRow.raw as Record<string, unknown>) ?? null);\n\n const elationLoading =\n input.elationId !== undefined && elationRow === undefined;\n const hintLoading = input.hintId !== undefined && hintRow === undefined;\n\n return {\n elationPatient,\n hintPatient,\n elationRow,\n hintRow,\n loading: elationLoading || hintLoading,\n };\n}\n\n/**\n * Read a shared pharmacy row by NCPDP id. Backs the pharmacy card in\n * the patient panel (from Elation's preferred_pharmacy reference).\n */\nfunction usePharmacyByNcpdpId(ncpdpId: string | undefined) {\n return useQuery(\n pharmacyByNcpdpRef as FunctionReference<\"query\">,\n ncpdpId ? { ncpdpId } : \"skip\",\n );\n}\n\ninterface UsePatientPhotoOptions {\n apiBaseUrl?: string;\n apiKey?: string;\n skipRefresh?: boolean;\n}\n\n/**\n * Subscribe to a patient's profile photo (s3Key + metadata). The\n * consumer constructs a download URL via the Truth attachments resource\n * (signed S3 URL). Fires a background refresh to pull the latest photo\n * binary from Elation + upload to S3.\n */\nfunction usePatientPhoto(\n elationId: number | undefined,\n options?: UsePatientPhotoOptions,\n) {\n const photo = useQuery(\n patientPhotoByIdRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n );\n\n useEffect(() => {\n if (options?.skipRefresh) {\n return;\n }\n if (elationId === undefined) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/photo/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({ elationId }),\n signal: controller.signal,\n }).catch(() => {\n /* non-fatal */\n });\n\n return () => controller.abort();\n }, [elationId, options?.apiBaseUrl, options?.apiKey, options?.skipRefresh]);\n\n return photo;\n}\n\n// ---------------------------------------------------------------------------\n// Conversation messages (Dialpad calls + SMS merged)\n// ---------------------------------------------------------------------------\n\ninterface ConversationMessage {\n kind: \"call\" | \"sms\";\n id: string;\n providerId: string;\n state: string | null;\n direction: string | null;\n fromNumber: string | null;\n toNumber: string | null;\n voicemailLink: string | null;\n duration: number | null;\n text: string | null;\n mms: boolean;\n mmsUrl: string | null;\n messageStatus: string | null;\n occurredAt: string;\n conversationId: string | null;\n patientId: string | null;\n}\n\nconst messagesByPhonesRef = makeFunctionReference<\n \"query\",\n { phoneA: string; phoneB: string; limit?: number },\n ConversationMessage[]\n>(\"conversationMessages:getByPhones\");\n\nconst messagesByConversationIdRef = makeFunctionReference<\n \"query\",\n { conversationId: string; limit?: number },\n ConversationMessage[]\n>(\"conversationMessages:getByConversationId\");\n\ninterface UseConversationMessagesOptions {\n /** Max items to return (default 200). */\n limit?: number;\n}\n\n/**\n * Subscribe to a conversation's calls + SMS merged chronologically.\n * Pass the patient phone + the provider phone — Truth computes a\n * normalized pair key server-side so formatting differences don't\n * matter.\n *\n * Returns `undefined` while loading, then `ConversationMessage[]`\n * sorted newest-first. Updates live as new webhook events land in\n * Convex via the kinesis consumer.\n */\nfunction useConversationMessages(\n input: { phoneA?: string; phoneB?: string; conversationId?: string },\n options?: UseConversationMessagesOptions,\n): ConversationMessage[] | undefined {\n const hasPair = !!input.phoneA && !!input.phoneB;\n const byPair = useQuery(\n messagesByPhonesRef as FunctionReference<\"query\">,\n hasPair\n ? {\n phoneA: input.phoneA as string,\n phoneB: input.phoneB as string,\n limit: options?.limit,\n }\n : \"skip\",\n ) as ConversationMessage[] | undefined;\n\n const byConvo = useQuery(\n messagesByConversationIdRef as FunctionReference<\"query\">,\n !hasPair && input.conversationId\n ? { conversationId: input.conversationId, limit: options?.limit }\n : \"skip\",\n ) as ConversationMessage[] | undefined;\n\n return hasPair ? byPair : byConvo;\n}\n\nexport {\n usePatients,\n usePatient,\n usePatientByElationId,\n usePatientByHintId,\n useAppointments,\n useAppointment,\n useAppointmentByElationId,\n usePhysiciansByElationIds,\n usePhysicianByElationId,\n usePatientMedical,\n usePatientBasic,\n usePharmacyByNcpdpId,\n usePatientPhoto,\n useConversationMessages,\n};\nexport type { ConversationMessage, UseConversationMessagesOptions };\nexport type {\n UsePatientListOptions,\n UseAppointmentListOptions,\n UsePatientMedicalOptions,\n UsePatientBasicOptions,\n UsePatientBasicResult,\n UsePatientPhotoOptions,\n Physician,\n};\n","/**\n * useNotifications — Truth SDK React hook for push notifications.\n *\n * Shape-compatible with `expo-notifications` where practical so\n * consumers port with minimal change. The hook dynamically imports\n * `expo-notifications` so the SDK doesn't hard-depend on Expo —\n * consumers running on a non-Expo runtime (e.g. a server test harness)\n * can still import the SDK without Metro blowing up.\n *\n * Exposes:\n * - permissionStatus — \"granted\" / \"denied\" / \"undetermined\"\n * - devicePushToken — native APNs/FCM token (undefined until register)\n * - register() — request permission, fetch token, register with Truth\n * - unregister() — revoke the device\n * - addReceivedListener / addResponseListener — re-exports of expo's\n * - getBadgeCount / setBadgeCount — re-exports of expo's\n *\n * Web push (VAPID subscription) lands in Phase 3.\n */\n\n\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n isWebPushSupported,\n registerServiceWorker,\n subscribeToPush,\n subscriptionToJSON,\n} from \"../web-push\";\n\n// `expo-notifications` is an optional peer dep. We lazy-import it via\n// a string-indirection so TypeScript + tsup don't try to resolve its\n// types at build time (the SDK ships without a hard dep so non-Expo\n// consumers — web, Node services — can still import from @hipnation-truth/sdk/react).\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ExpoNotificationsModule = any;\n\nasync function loadExpo(): Promise<ExpoNotificationsModule | null> {\n try {\n // Literal specifier so Hermes can parse the dynamic import (Hermes\n // rejects `import(variable)`). Magic comments tell vite/webpack not\n // to resolve at build time on consumers that don't ship Expo.\n return await import(\n /* @vite-ignore */ /* webpackIgnore: true */\n // @ts-ignore — expo-notifications is an optional peer dep\n \"expo-notifications\"\n );\n } catch {\n return null;\n }\n}\n\nexport type PermissionStatus =\n | \"granted\"\n | \"denied\"\n | \"undetermined\"\n | \"unknown\";\n\nexport interface UseNotificationsOptions {\n /** Truth API base URL, e.g. https://app.truth.communication-hub.com */\n apiBaseUrl: string;\n /** `hn_live_*` API key for the caller's application. */\n apiKey: string;\n /** Current user id — used when registering the device. */\n userId: string | null | undefined;\n /** Optional app version string stored on the device row. */\n appVersion?: string;\n /**\n * Auto-register on mount when permission is already granted.\n * Default: true. Set false if you want to control the registration\n * lifecycle yourself.\n */\n autoRegister?: boolean;\n /** VAPID public key for web push. Fetched automatically if omitted. */\n vapidPublicKey?: string;\n /** Path to the service worker file. Default: \"/truth-sw.js\" */\n serviceWorkerPath?: string;\n}\n\nexport interface UseNotificationsResult {\n permissionStatus: PermissionStatus;\n devicePushToken: string | null;\n register: () => Promise<{ ok: boolean; reason?: string }>;\n unregister: () => Promise<void>;\n addReceivedListener: (listener: (n: unknown) => void) => () => void;\n addResponseListener: (listener: (r: unknown) => void) => () => void;\n getBadgeCount: () => Promise<number>;\n setBadgeCount: (count: number) => Promise<void>;\n}\n\nexport function useNotifications(\n options: UseNotificationsOptions,\n): UseNotificationsResult {\n const [permissionStatus, setPermissionStatus] =\n useState<PermissionStatus>(\"unknown\");\n const [devicePushToken, setDevicePushToken] = useState<string | null>(null);\n const expoRef = useRef<ExpoNotificationsModule | null>(null);\n const isWebRef = useRef(false);\n const vapidKeyRef = useRef<string | null>(options.vapidPublicKey ?? null);\n\n useEffect(() => {\n let mounted = true;\n void (async () => {\n const expo = await loadExpo();\n if (!mounted) {\n return;\n }\n expoRef.current = expo;\n\n if (expo) {\n try {\n const perm = await expo.getPermissionsAsync();\n if (!mounted) {\n return;\n }\n setPermissionStatus(mapStatus(perm?.status));\n } catch {\n setPermissionStatus(\"unknown\");\n }\n return;\n }\n\n if (isWebPushSupported()) {\n isWebRef.current = true;\n if (!vapidKeyRef.current) {\n try {\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/vapid-key`,\n {\n headers: {\n Accept: \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n },\n );\n if (res.ok) {\n const data = await res.json();\n vapidKeyRef.current = data.vapidPublicKey ?? null;\n }\n } catch {\n // best-effort\n }\n }\n if (!mounted) {\n return;\n }\n const webPerm =\n typeof Notification !== \"undefined\"\n ? Notification.permission\n : \"default\";\n setPermissionStatus(\n webPerm === \"granted\"\n ? \"granted\"\n : webPerm === \"denied\"\n ? \"denied\"\n : \"undetermined\",\n );\n } else {\n setPermissionStatus(\"unknown\");\n }\n })();\n return () => {\n mounted = false;\n };\n }, [options.apiBaseUrl, options.apiKey]);\n\n const register = useCallback(async () => {\n if (!options.userId) {\n return { ok: false, reason: \"missing_userId\" };\n }\n\n // Web push path\n if (isWebRef.current) {\n const vapidKey = vapidKeyRef.current;\n if (!vapidKey) {\n return { ok: false, reason: \"no_vapid_key\" };\n }\n\n const webPerm = await Notification.requestPermission();\n setPermissionStatus(\n webPerm === \"granted\"\n ? \"granted\"\n : webPerm === \"denied\"\n ? \"denied\"\n : \"undetermined\",\n );\n if (webPerm !== \"granted\") {\n return { ok: false, reason: \"permission_denied\" };\n }\n\n try {\n const swPath = options.serviceWorkerPath ?? \"/truth-sw.js\";\n const registration = await registerServiceWorker(swPath);\n await navigator.serviceWorker.ready;\n const subscription = await subscribeToPush(registration, vapidKey);\n const subJSON = subscriptionToJSON(subscription);\n setDevicePushToken(subscription.endpoint);\n\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/devices/register`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({\n userId: options.userId,\n platform: \"web\",\n webPushSubscription: subJSON,\n appVersion: options.appVersion,\n locale: navigator.language,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n }),\n },\n );\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n return {\n ok: false,\n reason: `register_failed_${res.status}: ${text.slice(0, 120)}`,\n };\n }\n return { ok: true };\n } catch (err) {\n return {\n ok: false,\n reason: `web_push_error: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n }\n\n // Native (expo-notifications) path\n const expo = expoRef.current ?? (await loadExpo());\n expoRef.current = expo;\n if (!expo) {\n return { ok: false, reason: \"expo_notifications_missing\" };\n }\n\n let perm = await expo.getPermissionsAsync();\n if (perm?.status !== \"granted\") {\n perm = await expo.requestPermissionsAsync();\n }\n setPermissionStatus(mapStatus(perm?.status));\n if (perm?.status !== \"granted\") {\n return { ok: false, reason: \"permission_denied\" };\n }\n\n const tokenResp = await expo.getDevicePushTokenAsync();\n const nativeToken = tokenResp?.data;\n const platform = detectPlatform(tokenResp?.type);\n if (!nativeToken || (platform !== \"ios\" && platform !== \"android\")) {\n return { ok: false, reason: \"no_native_token\" };\n }\n\n setDevicePushToken(nativeToken);\n\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/devices/register`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({\n userId: options.userId,\n platform,\n nativeToken,\n appVersion: options.appVersion,\n locale:\n typeof navigator !== \"undefined\" ? navigator.language : undefined,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n }),\n },\n );\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n return {\n ok: false,\n reason: `register_failed_${res.status}: ${text.slice(0, 120)}`,\n };\n }\n return { ok: true };\n }, [\n options.apiBaseUrl,\n options.apiKey,\n options.userId,\n options.appVersion,\n options.serviceWorkerPath,\n ]);\n\n const unregister = useCallback(async () => {\n if (!devicePushToken) {\n return;\n }\n await fetch(`${options.apiBaseUrl}/api/notifications/devices/unregister`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({ nativeToken: devicePushToken }),\n }).catch(() => {\n /* non-fatal */\n });\n setDevicePushToken(null);\n }, [options.apiBaseUrl, options.apiKey, devicePushToken]);\n\n const addReceivedListener = useCallback(\n (listener: (n: unknown) => void): (() => void) => {\n if (isWebRef.current) {\n if (\n typeof navigator === \"undefined\" ||\n !(\"serviceWorker\" in navigator)\n ) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === \"TRUTH_PUSH_RECEIVED\") {\n listener(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () =>\n navigator.serviceWorker.removeEventListener(\"message\", handler);\n }\n const expo = expoRef.current;\n if (!expo?.addNotificationReceivedListener) {\n return () => {};\n }\n const sub = expo.addNotificationReceivedListener(listener);\n return () => sub.remove?.();\n },\n [],\n );\n\n const addResponseListener = useCallback(\n (listener: (r: unknown) => void): (() => void) => {\n if (isWebRef.current) {\n if (\n typeof navigator === \"undefined\" ||\n !(\"serviceWorker\" in navigator)\n ) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === \"TRUTH_PUSH_TAPPED\") {\n listener(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () =>\n navigator.serviceWorker.removeEventListener(\"message\", handler);\n }\n const expo = expoRef.current;\n if (!expo?.addNotificationResponseReceivedListener) {\n return () => {};\n }\n const sub = expo.addNotificationResponseReceivedListener(listener);\n return () => sub.remove?.();\n },\n [],\n );\n\n const getBadgeCount = useCallback(async (): Promise<number> => {\n const expo = expoRef.current;\n if (!expo?.getBadgeCountAsync) {\n return 0;\n }\n return (await expo.getBadgeCountAsync()) ?? 0;\n }, []);\n\n const setBadgeCount = useCallback(async (count: number): Promise<void> => {\n const expo = expoRef.current;\n if (!expo?.setBadgeCountAsync) {\n return;\n }\n await expo.setBadgeCountAsync(count);\n }, []);\n\n // Auto-register once on mount when conditions are met\n const autoRegister = options.autoRegister !== false;\n useEffect(() => {\n if (!autoRegister) {\n return;\n }\n if (permissionStatus !== \"granted\") {\n return;\n }\n if (devicePushToken) {\n return;\n }\n if (!options.userId) {\n return;\n }\n void register();\n }, [\n autoRegister,\n permissionStatus,\n devicePushToken,\n options.userId,\n register,\n ]);\n\n return {\n permissionStatus,\n devicePushToken,\n register,\n unregister,\n addReceivedListener,\n addResponseListener,\n getBadgeCount,\n setBadgeCount,\n };\n}\n\nfunction mapStatus(status: string | undefined): PermissionStatus {\n if (status === \"granted\") {\n return \"granted\";\n }\n if (status === \"denied\") {\n return \"denied\";\n }\n if (status === \"undetermined\") {\n return \"undetermined\";\n }\n return \"unknown\";\n}\n\nfunction detectPlatform(\n tokenType: string | undefined,\n): \"ios\" | \"android\" | \"web\" | \"unknown\" {\n if (tokenType === \"apns\") {\n return \"ios\";\n }\n if (tokenType === \"fcm\") {\n return \"android\";\n }\n if (tokenType === \"web\") {\n return \"web\";\n }\n return \"unknown\";\n}\n","/**\n * Web Push helpers for browser environments.\n *\n * Handles service worker registration, VAPID push subscription, and\n * message forwarding from the service worker to the main thread.\n */\n\nexport interface WebPushConfig {\n vapidPublicKey: string;\n serviceWorkerPath?: string;\n}\n\nexport function isWebPushSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n \"PushManager\" in window\n );\n}\n\nexport async function registerServiceWorker(\n path = \"/truth-sw.js\",\n): Promise<ServiceWorkerRegistration> {\n return navigator.serviceWorker.register(path);\n}\n\nexport async function subscribeToPush(\n registration: ServiceWorkerRegistration,\n vapidPublicKey: string,\n): Promise<PushSubscription> {\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n return existing;\n }\n\n return registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(\n vapidPublicKey,\n ) as unknown as ArrayBuffer,\n });\n}\n\nexport function subscriptionToJSON(sub: PushSubscription): {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n} {\n const json = sub.toJSON();\n return {\n endpoint: sub.endpoint,\n keys: {\n p256dh: json.keys?.p256dh ?? \"\",\n auth: json.keys?.auth ?? \"\",\n },\n };\n}\n\nfunction urlBase64ToUint8Array(base64String: string): Uint8Array {\n const padding = \"=\".repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const rawData = atob(base64);\n const outputArray = new Uint8Array(rawData.length);\n for (let i = 0; i < rawData.length; ++i) {\n outputArray[i] = rawData.charCodeAt(i);\n }\n return outputArray;\n}\n\nexport function onServiceWorkerMessage(\n type: string,\n callback: (payload: unknown) => void,\n): () => void {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === type) {\n callback(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () => navigator.serviceWorker.removeEventListener(\"message\", handler);\n}\n","/**\n * TruthProvider — Convex React provider pre-configured for Truth.\n *\n * Wraps children in a ConvexProvider connected to the correct\n * Truth Convex deployment for the given environment.\n *\n * @example\n * ```tsx\n * import { TruthProvider } from '@hipnation-truth/sdk/react';\n *\n * function App() {\n * return (\n * <TruthProvider environment=\"sandbox\">\n * <MyApp />\n * </TruthProvider>\n * );\n * }\n * ```\n */\n\n\"use client\";\n\nimport { ConvexProvider, ConvexReactClient } from \"convex/react\";\nimport type { ReactNode } from \"react\";\nimport { createElement, useMemo } from \"react\";\n\n// Mirrors CONVEX_URLS in client.ts. UAT shares prod resources.\n//\n// Exported so `resolveConvexUrl` can be unit-tested without booting\n// React — a regression in this map shipped SDK 0.4.1 that pointed\n// UAT at sandbox Convex. The test in provider.test.ts locks every\n// stage → URL pair down.\nexport const CONVEX_URLS: Record<string, string> = {\n local: \"https://courteous-duck-623.convex.cloud\",\n staging: \"https://courteous-duck-623.convex.cloud\",\n stg: \"https://courteous-duck-623.convex.cloud\",\n sandbox: \"https://courteous-duck-623.convex.cloud\",\n uat: \"https://gallant-gecko-217.convex.cloud\",\n production: \"https://gallant-gecko-217.convex.cloud\",\n};\n\n/**\n * Resolve the Convex URL for a given environment. Honors an explicit\n * override and falls back to sandbox for unknown environments.\n */\nexport function resolveConvexUrl(\n environment: string | undefined,\n override?: string,\n): string {\n if (override) {\n return override;\n }\n const env = environment ?? \"sandbox\";\n return CONVEX_URLS[env] ?? CONVEX_URLS.sandbox;\n}\n\ninterface TruthProviderProps {\n /** Truth environment — determines which Convex deployment to connect to */\n environment?: string;\n /** Override the Convex URL directly */\n convexUrl?: string;\n children: ReactNode;\n}\n\nfunction TruthProvider({\n environment = \"sandbox\",\n convexUrl,\n children,\n}: TruthProviderProps) {\n const url = resolveConvexUrl(environment, convexUrl);\n\n const client = useMemo(() => new ConvexReactClient(url), [url]);\n\n return createElement(ConvexProvider, { client }, children);\n}\n\nexport { TruthProvider };\nexport type { TruthProviderProps };\n","/**\n * React tracking hook — fire-and-forget event tracking from client components.\n *\n * Creates a lightweight Tracker instance scoped to the browser session\n * and exposes a useTrack() hook for emitting events to Truth's Kinesis stream.\n *\n * @example\n * ```tsx\n * import { useTruth } from '@hipnation-truth/sdk/react';\n *\n * function ConversationView({ conversationId }) {\n * const truth = useTruth();\n *\n * useEffect(() => {\n * truth.track('conversation.marked_read.v1', {\n * read_by_actor_id: userId,\n * unread_count_before: 5,\n * unread_count_after: 0,\n * }, { subject: { conversation_id: conversationId } });\n * }, [conversationId]);\n * }\n * ```\n */\n\n\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { createContext, createElement, useContext, useMemo } from \"react\";\nimport type {\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"../tracking/events\";\nimport { Tracker } from \"../tracking/tracker\";\nimport type { ActorContext } from \"../types/config\";\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\ninterface TruthTrackingContextValue {\n track: <T extends EventType>(\n eventType: T,\n payload: EventPayloadMap[T],\n options?: TrackOptions,\n ) => void;\n identify: (actorId: string, actorType: ActorContext[\"actorType\"]) => void;\n}\n\nconst TruthTrackingContext = createContext<TruthTrackingContextValue | null>(\n null,\n);\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\ninterface TruthTrackingProviderProps {\n /** Truth environment — determines API URL for event delivery */\n environment?: string;\n /** Event source identifier */\n source?: string;\n /** Source version (git SHA) */\n sourceVersion?: string;\n /** Default tenant ID */\n tenantId?: string;\n /** API key for authentication */\n apiKey?: string;\n children: ReactNode;\n}\n\nfunction TruthTrackingProvider({\n environment = \"sandbox\",\n source = \"communication-hub.frontend\",\n sourceVersion = \"unknown\",\n tenantId = \"hipnation\",\n apiKey = \"\",\n children,\n}: TruthTrackingProviderProps) {\n const value = useMemo<TruthTrackingContextValue>(() => {\n const tracker = new Tracker({\n apiKey,\n environment,\n source,\n sourceVersion,\n tenantId,\n batchSize: 10,\n flushIntervalMs: 5000,\n });\n\n return {\n track: (eventType, payload, options) => {\n tracker.track(eventType, payload, options);\n },\n identify: (actorId, actorType) => {\n tracker.setActor({ actorId, actorType });\n },\n };\n }, [apiKey, environment, source, sourceVersion, tenantId]);\n\n return createElement(TruthTrackingContext.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Access the Truth tracking context. Must be within a TruthTrackingProvider.\n * Returns `{ track, identify }` for emitting events to Kinesis.\n */\nfunction useTruth(): TruthTrackingContextValue {\n const ctx = useContext(TruthTrackingContext);\n if (!ctx) {\n throw new Error(\"useTruth must be used within a TruthTrackingProvider\");\n }\n return ctx;\n}\n\nexport { TruthTrackingProvider, useTruth };\nexport type { TruthTrackingProviderProps, TruthTrackingContextValue };\n","/**\n * Event tracker with batching, retry, and flush support.\n *\n * Buffers events in an internal queue and flushes them to the Truth API\n * endpoint. Flushes occur when the buffer reaches `batchSize` or every\n * `flushIntervalMs` milliseconds, whichever comes first.\n */\n\nimport type { ActorContext } from \"../types/config\";\nimport type {\n EventEnvelope,\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"./events\";\n\n// ---------------------------------------------------------------------------\n// UUID v7 helper (no external dependency)\n// ---------------------------------------------------------------------------\n\n/**\n * Generates a UUID v7 string. Uses crypto.randomUUID where available,\n * falling back to a timestamp + random bytes implementation.\n *\n * UUID v7 layout (RFC 9562):\n * 48 bits - unix timestamp (ms)\n * 4 bits - version (0b0111)\n * 12 bits - random\n * 2 bits - variant (0b10)\n * 62 bits - random\n */\nfunction generateUuidV7(): string {\n const now = Date.now();\n\n // 6 bytes of timestamp\n const timeBytes = new Uint8Array(6);\n let ts = now;\n for (let i = 5; i >= 0; i--) {\n timeBytes[i] = ts & 0xff;\n ts = Math.floor(ts / 256);\n }\n\n // 10 bytes of random\n const randomBytes = new Uint8Array(10);\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n globalThis.crypto.getRandomValues\n ) {\n globalThis.crypto.getRandomValues(randomBytes);\n } else {\n for (let i = 0; i < 10; i++) {\n randomBytes[i] = Math.floor(Math.random() * 256);\n }\n }\n\n // Assemble 16 bytes\n const bytes = new Uint8Array(16);\n bytes.set(timeBytes, 0);\n bytes.set(randomBytes, 6);\n\n // Set version (bits 48-51 to 0b0111)\n bytes[6] = (bytes[6] & 0x0f) | 0x70;\n\n // Set variant (bits 64-65 to 0b10)\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n\n // Format as hex string with dashes\n const hex = Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return [\n hex.slice(0, 8),\n hex.slice(8, 12),\n hex.slice(12, 16),\n hex.slice(16, 20),\n hex.slice(20, 32),\n ].join(\"-\");\n}\n\n// ---------------------------------------------------------------------------\n// Environment-based API URL resolution\n// ---------------------------------------------------------------------------\n\n// Environment → Truth API base URL.\n//\n// Topology:\n// local / staging / stg / sandbox → sandbox Truth (app.sandbox.*)\n// uat / production → production Truth (app.truth.*)\n//\n// UAT shares production resources (same Fly Postgres + Hasura + Truth +\n// Convex + EHR tokens as prod). Staging is the isolated sandbox env.\nconst API_URLS: Record<string, string> = {\n local: \"http://localhost:3000\",\n staging: \"https://app.sandbox.communication-hub.com\",\n stg: \"https://app.sandbox.communication-hub.com\",\n sandbox: \"https://app.sandbox.communication-hub.com\",\n uat: \"https://app.truth.communication-hub.com\",\n production: \"https://app.truth.communication-hub.com\",\n};\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_BATCH_SIZE = 25;\nconst DEFAULT_FLUSH_INTERVAL_MS = 5_000;\nconst MAX_RETRIES = 3;\nconst BASE_RETRY_DELAY_MS = 500;\n\n// ---------------------------------------------------------------------------\n// Tracker configuration\n// ---------------------------------------------------------------------------\n\ninterface TrackerConfig {\n apiKey: string;\n environment: string;\n source: string;\n sourceVersion: string;\n tenantId: string;\n batchSize: number;\n flushIntervalMs: number;\n apiBaseUrl?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Tracker class\n// ---------------------------------------------------------------------------\n\nclass Tracker {\n private readonly config: TrackerConfig;\n readonly apiUrl: string;\n private queue: EventEnvelope[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private defaultActor: ActorContext | undefined;\n private isFlushing = false;\n private isShutdown = false;\n\n constructor(config: TrackerConfig) {\n this.config = config;\n this.apiUrl =\n config.apiBaseUrl ?? API_URLS[config.environment] ?? API_URLS.local;\n\n this.startFlushInterval();\n this.registerShutdownHooks();\n }\n\n /**\n * Set the default actor context for subsequent events.\n */\n setActor(actor: ActorContext): void {\n this.defaultActor = actor;\n }\n\n /**\n * Enqueue a typed event for delivery. This is fire-and-forget from\n * the caller's perspective -- events are buffered and flushed in batches.\n */\n track<T extends EventType>(\n eventType: T,\n payload: EventPayloadMap[T],\n options?: TrackOptions,\n ): void {\n if (this.isShutdown) {\n return;\n }\n\n const now = new Date().toISOString();\n\n const envelope: EventEnvelope = {\n event_id: generateUuidV7(),\n event_type: eventType,\n schema_version: 1,\n occurred_at: options?.occurredAt ?? now,\n received_at: now,\n source: this.config.source,\n source_version: this.config.sourceVersion,\n tenant_id: options?.tenantId ?? this.config.tenantId,\n actor:\n options?.actor ??\n (this.defaultActor\n ? {\n actor_id: this.defaultActor.actorId,\n actor_type: this.defaultActor.actorType,\n }\n : undefined),\n subject: options?.subject,\n compliance: options?.compliance,\n payload: payload as unknown as Record<string, unknown>,\n };\n\n this.queue.push(envelope);\n\n if (this.queue.length >= this.config.batchSize) {\n void this.flush();\n }\n }\n\n /**\n * Force an immediate flush of all buffered events.\n * Returns a promise that resolves when the flush completes.\n */\n async flush(): Promise<void> {\n if (this.queue.length === 0 || this.isFlushing) {\n return;\n }\n\n this.isFlushing = true;\n const batch = this.queue.splice(0, this.config.batchSize);\n\n try {\n await this.sendBatch(batch);\n } catch {\n // Re-queue events that failed to send (prepend to maintain ordering)\n this.queue.unshift(...batch);\n } finally {\n this.isFlushing = false;\n }\n\n // If there are still events in the queue, flush again\n if (this.queue.length >= this.config.batchSize) {\n await this.flush();\n }\n }\n\n /**\n * Gracefully shut down the tracker. Flushes remaining events and\n * clears the flush interval.\n */\n async shutdown(): Promise<void> {\n this.isShutdown = true;\n this.stopFlushInterval();\n await this.flush();\n }\n\n /**\n * Send a batch of events to the Truth API with exponential backoff retry.\n */\n private async sendBatch(batch: EventEnvelope[]): Promise<void> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(`${this.apiUrl}/api/events/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": this.config.apiKey,\n },\n body: JSON.stringify({ events: batch }),\n });\n\n if (response.ok) {\n return;\n }\n\n // Don't retry 4xx client errors (except 429)\n if (\n response.status >= 400 &&\n response.status < 500 &&\n response.status !== 429\n ) {\n return;\n }\n\n lastError = new Error(\n `HTTP ${response.status}: ${response.statusText}`,\n );\n } catch (error) {\n lastError = error;\n }\n\n // Exponential backoff with jitter\n if (attempt < MAX_RETRIES) {\n const delay = BASE_RETRY_DELAY_MS * 2 ** attempt;\n const jitter = Math.random() * delay * 0.5;\n await sleep(delay + jitter);\n }\n }\n\n throw lastError;\n }\n\n private startFlushInterval(): void {\n if (this.config.flushIntervalMs > 0) {\n this.flushTimer = setInterval(() => {\n void this.flush();\n }, this.config.flushIntervalMs);\n\n // Unref the timer so it doesn't prevent Node.js from exiting\n if (typeof this.flushTimer === \"object\" && \"unref\" in this.flushTimer) {\n this.flushTimer.unref();\n }\n }\n }\n\n private stopFlushInterval(): void {\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n }\n\n private registerShutdownHooks(): void {\n // Only register in Node.js environments\n if (typeof globalThis.process !== \"undefined\" && globalThis.process.on) {\n const shutdownHandler = () => {\n void this.shutdown();\n };\n\n globalThis.process.on(\"beforeExit\", shutdownHandler);\n globalThis.process.on(\"SIGTERM\", shutdownHandler);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport {\n Tracker,\n generateUuidV7,\n DEFAULT_BATCH_SIZE,\n DEFAULT_FLUSH_INTERVAL_MS,\n};\nexport type { TrackerConfig };\n"],"mappings":"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","names":["useQuery","makeFunctionReference","useEffect","useEffect","_a","res","createElement","useMemo","useMemo","createElement"]}
|
|
1
|
+
{"version":3,"sources":["../src/react/conversations.ts","../src/react/hooks.ts","../src/react/notifications.ts","../src/web-push.ts","../src/react/provider.ts","../src/react/tracking.ts","../src/tracking/tracker.ts"],"sourcesContent":["/**\n * React hooks for Truth SDK — reactive conversation + message data.\n *\n * Wraps Convex queries owned by the Truth backend so consumers (CommHub\n * Expo frontend) get live-updating conversation lists, message threads,\n * and unread totals without managing subscriptions themselves.\n *\n * **Hook contract:** every hook in this file returns\n * `{ data, loading, error }`. `data` is `undefined` while loading and\n * also when the underlying query is `\"skip\"`'d (caller didn't pass the\n * required arg yet). `loading` is `true` only while a real subscription\n * is in-flight; once `data` resolves (even to `null`) `loading` flips\n * to `false`. `error` is reserved for SDK-side validation — Convex\n * itself throws on subscribe rather than returning an error value, so\n * unhandled query errors propagate as React errors and should be caught\n * with an error boundary.\n *\n * **Backed by PR #110 schema (commit `41dbb59` on\n * `feat/migrate-dialpad-webhook-and-messages-to-truth`):**\n * - `conversations:listForUser` → `useConversations`\n * - `conversations:getUnreadTotalForUser` → `useUnreadCount`\n * - `conversations:getByPhonePair` → `useConversationByPhonePair`\n * - `conversationMessages:getByConversationId` → `useMessages`\n *\n * **Deliberately NOT shipped here (no backend query yet — flagged in\n * PR #111 body for follow-up):**\n * - Single-conversation-by-id lookup. Convex `conversations` has no\n * `get(id)` query — only `getByPhonePair`. CommHub uses phonePair as\n * the primary handle, so the byPair flavor is what consumers need;\n * if id-based lookup is needed later we can add a `conversations:get`\n * in Truth.\n * - Participants / \"seen by\" hook. The migrated schema has no\n * `conversationParticipants` table — read state is per-user via\n * `conversationReads` and there's no public list query for it yet.\n * - `markRead` mutation. PR #110 declares `markRead` as\n * `internalMutation`, so the frontend can't invoke it directly — it\n * has to go through a Truth HTTP endpoint, or webhook-migrator needs\n * to flip it to a public `mutation`. Flagged for a follow-up.\n *\n * Must be used within `<TruthProvider />` (see `./provider`).\n *\n * @example\n * ```tsx\n * import {\n * useConversations,\n * useConversationByPhonePair,\n * useMessages,\n * useUnreadCount,\n * } from '@hipnation-truth/sdk/react';\n *\n * function Inbox({ userId }: { userId: string }) {\n * const { data: convos, loading } = useConversations({ userId });\n * const { data: unread } = useUnreadCount(userId);\n * if (loading) return <Spinner />;\n * return (\n * <div>\n * <Badge count={unread ?? 0} />\n * {convos?.map((c) => <ConvoRow key={c.id} convo={c} />)}\n * </div>\n * );\n * }\n * ```\n */\n\n\"use client\";\n\nimport { useQuery } from \"convex/react\";\nimport type { FunctionReference } from \"convex/server\";\nimport { makeFunctionReference } from \"convex/server\";\n\n// ---------------------------------------------------------------------------\n// Shared types\n// ---------------------------------------------------------------------------\n//\n// Mirror the shapes returned by the Convex queries on PR #110. Once\n// the consuming app regenerates `convex/_generated/api.d.ts` against\n// the merged schema these can be replaced with inferred types — for\n// today they give consumers useful autocomplete.\n\n/**\n * Row shape returned by `conversations:listForUser` — joins the base\n * `conversations` row with the caller's per-user `conversationReads`\n * entry so unread state lives on each item.\n */\nexport interface ConversationListItem {\n /** Convex id of the conversation row. */\n id: string;\n patientPhone: string;\n providerPhone: string;\n /** Sorted-digit-pair key (commutative — same regardless of direction). */\n phonePair: string;\n /** Truth-side patient id, when the kinesis pipeline could resolve it. */\n patientId: string | null;\n /** ISO timestamp of the most recent message in the conversation. */\n lastMessageAt: string;\n /** Caller's unread count for this conversation. */\n unreadCount: number;\n /** ISO timestamp of when the caller last marked the conversation read. */\n lastReadAt: string | null;\n}\n\n/**\n * Raw `conversations` table row — what `getByPhonePair` returns. No\n * unread / read joining; for that, prefer `useConversations`.\n */\nexport interface ConversationRow {\n _id: string;\n _creationTime: number;\n patientPhone: string;\n providerPhone: string;\n phonePair: string;\n patientId: string | null;\n lastEventId: string | null;\n lastMessageAt: string;\n createdAt: string;\n}\n\n/**\n * Message row returned by `conversationMessages:getByConversationId` —\n * calls + SMS merged chronologically. Mirrors `ConversationMessage`\n * already exported from the SDK; re-exported here under a clearer name\n * for the new hook surface.\n */\nexport interface ConversationMessageRow {\n kind: \"call\" | \"sms\";\n id: string;\n providerId: string;\n state: string | null;\n direction: string | null;\n fromNumber: string | null;\n toNumber: string | null;\n voicemailLink: string | null;\n duration: number | null;\n text: string | null;\n mms: boolean;\n mmsUrl: string | null;\n messageStatus: string | null;\n occurredAt: string;\n conversationId: string | null;\n patientId: string | null;\n}\n\nexport interface UseQueryResult<T> {\n /**\n * Query data. `undefined` while loading or while the query is\n * intentionally skipped (e.g. caller hasn't supplied `userId` yet).\n */\n data: T | undefined;\n /** True only while a real subscription is in-flight. */\n loading: boolean;\n /**\n * Reserved for client-side validation errors surfaced by the SDK\n * itself. Convex query errors propagate as React errors and should\n * be caught with an error boundary.\n */\n error: Error | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Convex function references — string-keyed, decoupled from the\n// consuming app's generated `convex/_generated/api`. Names below match\n// the queries on PR #110 (commit 41dbb59) verbatim.\n// ---------------------------------------------------------------------------\n\nconst conversationsListForUserRef = makeFunctionReference<\n \"query\",\n { userId: string; limit?: number },\n ConversationListItem[]\n>(\"conversations:listForUser\");\n\nconst conversationsGetUnreadTotalForUserRef = makeFunctionReference<\n \"query\",\n { userId: string },\n number\n>(\"conversations:getUnreadTotalForUser\");\n\nconst conversationsGetByPhonePairRef = makeFunctionReference<\n \"query\",\n { phonePair: string },\n ConversationRow | null\n>(\"conversations:getByPhonePair\");\n\n// Lives on the existing `conversationMessages` module (pre-#110). PR\n// #110 added `conversationId` indexing on `messageSms` / `messageCalls`,\n// so this query is now reliable for the new conversations table.\nconst conversationMessagesGetByConversationIdRef = makeFunctionReference<\n \"query\",\n { conversationId: string; limit?: number },\n ConversationMessageRow[]\n>(\"conversationMessages:getByConversationId\");\n\n// ---------------------------------------------------------------------------\n// Skipped-query sentinel + result shaping\n// ---------------------------------------------------------------------------\n\nconst SKIP = \"skip\" as const;\n\nfunction toResult<T>(\n value: T | undefined,\n skipped: boolean,\n): UseQueryResult<T> {\n if (skipped) {\n return { data: undefined, loading: false, error: undefined };\n }\n return {\n data: value,\n loading: value === undefined,\n error: undefined,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Filters + options\n// ---------------------------------------------------------------------------\n\nexport interface UseConversationsFilters {\n /**\n * Truth user id (the Better Auth subject). Pass `undefined` to skip\n * the query — useful when the auth session is still loading.\n */\n userId: string | null | undefined;\n /** Page size. Server caps at 100 by default. */\n limit?: number;\n}\n\nexport interface UseMessagesOptions {\n /** Page size. Server caps at 200 by default. */\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to a user's conversations — joined with their per-conversation\n * unread count, sorted by most recent message. Updates live as new\n * SMS / calls land in Convex and the webhook bumps `unreadCount`.\n *\n * Returns `{ data: undefined, loading: false }` while the auth session\n * resolves (`userId === undefined`); not an error, just a skip.\n */\nfunction useConversations(\n filters: UseConversationsFilters,\n): UseQueryResult<ConversationListItem[]> {\n const skipped = !filters.userId;\n const result = useQuery(\n conversationsListForUserRef as FunctionReference<\"query\">,\n skipped\n ? SKIP\n : {\n userId: filters.userId as string,\n limit: filters.limit,\n },\n ) as ConversationListItem[] | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Look up a single conversation by its normalized phonePair (sorted\n * digit-pair, e.g. `\"5125550123|6505551234\"`). Returns `null` if no\n * conversation exists yet for that pair.\n *\n * Use the `phonePairKey(patientPhone, providerPhone)` helper exposed by\n * the Truth Convex module to derive the key from raw phone strings.\n */\nfunction useConversationByPhonePair(\n phonePair: string | null | undefined,\n): UseQueryResult<ConversationRow | null> {\n const skipped = !phonePair;\n const result = useQuery(\n conversationsGetByPhonePairRef as FunctionReference<\"query\">,\n skipped ? SKIP : { phonePair: phonePair as string },\n ) as ConversationRow | null | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Subscribe to a paginated message stream for a single conversation.\n * Calls + SMS merged chronologically, newest-first. Live as the\n * kinesis consumer writes to `messageCalls` / `messageSms`.\n *\n * Pass the Convex `_id` of the conversation row (from\n * `useConversations` / `useConversationByPhonePair`) as\n * `conversationId`.\n */\nfunction useMessages(\n conversationId: string | null | undefined,\n options?: UseMessagesOptions,\n): UseQueryResult<ConversationMessageRow[]> {\n const skipped = !conversationId;\n const result = useQuery(\n conversationMessagesGetByConversationIdRef as FunctionReference<\"query\">,\n skipped\n ? SKIP\n : {\n conversationId: conversationId as string,\n limit: options?.limit,\n },\n ) as ConversationMessageRow[] | undefined;\n\n return toResult(result, skipped);\n}\n\n/**\n * Subscribe to the user's total unread message count across all\n * conversations. Backs the inbox tab badge in CommHub.\n *\n * Returns `0` when the underlying query resolves with no unread\n * messages, `undefined` while loading or when `userId` is missing.\n */\nfunction useUnreadCount(\n userId: string | null | undefined,\n): UseQueryResult<number> {\n const skipped = !userId;\n const result = useQuery(\n conversationsGetUnreadTotalForUserRef as FunctionReference<\"query\">,\n skipped ? SKIP : { userId: userId as string },\n ) as number | undefined;\n\n return toResult(result, skipped);\n}\n\nexport {\n useConversationByPhonePair,\n useConversations,\n useMessages,\n useUnreadCount,\n};\n","/**\n * React hooks for Truth SDK — real-time Convex-backed data access.\n *\n * These hooks use Convex React subscriptions for live-updating data.\n * Must be used within a ConvexProvider (see TruthProvider).\n *\n * @example\n * ```tsx\n * import { usePatients, useAppointments } from '@hipnation-truth/sdk/react';\n *\n * function PatientList() {\n * const patients = usePatients({ limit: 20 });\n * return patients?.map(p => <div key={p._id}>{p.firstName} {p.lastName}</div>);\n * }\n * ```\n */\n\n\"use client\";\n\nimport { useQuery } from \"convex/react\";\nimport { useEffect } from \"react\";\n\n// ---------------------------------------------------------------------------\n// Convex function references\n// ---------------------------------------------------------------------------\n// We use string-based references to avoid importing the generated API\n// (which lives in the consuming app, not the SDK). Convex supports this\n// via the `api` module pattern, but for a published SDK we use\n// makeFunctionReference to stay decoupled from the app's codegen.\n\nimport type { FunctionReference } from \"convex/server\";\nimport { makeFunctionReference } from \"convex/server\";\n\n// Patient query references\nconst patientsListRef = makeFunctionReference<\n \"query\",\n {\n search?: string;\n lastName?: string;\n cursor?: string;\n limit?: number;\n },\n unknown[]\n>(\"patients:list\");\n\nconst patientsGetRef = makeFunctionReference<\n \"query\",\n { id: string },\n unknown | null\n>(\"patients:get\");\n\nconst patientsByElationIdRef = makeFunctionReference<\n \"query\",\n { elationId: string },\n unknown | null\n>(\"patients:getByElationId\");\n\nconst patientsByHintIdRef = makeFunctionReference<\n \"query\",\n { hintId: string },\n unknown | null\n>(\"patients:getByHintId\");\n\n// Appointment query references\nconst appointmentsListRef = makeFunctionReference<\n \"query\",\n {\n patientId?: string;\n status?: string;\n startDate?: string;\n endDate?: string;\n limit?: number;\n },\n unknown[]\n>(\"appointments:list\");\n\nconst appointmentsGetRef = makeFunctionReference<\n \"query\",\n { id: string },\n unknown | null\n>(\"appointments:get\");\n\nconst appointmentsByElationIdRef = makeFunctionReference<\n \"query\",\n { elationId: string },\n unknown | null\n>(\"appointments:getByElationId\");\n\n// ---------------------------------------------------------------------------\n// Patient hooks\n// ---------------------------------------------------------------------------\n\ninterface UsePatientListOptions {\n search?: string;\n lastName?: string;\n limit?: number;\n}\n\n/**\n * Subscribe to a list of patients with optional search and filtering.\n * Returns undefined while loading, then an array of patients.\n */\nfunction usePatients(options?: UsePatientListOptions) {\n return useQuery(patientsListRef as FunctionReference<\"query\">, options ?? {});\n}\n\n/**\n * Subscribe to a single patient by Convex document ID.\n */\nfunction usePatient(id: string) {\n return useQuery(patientsGetRef as FunctionReference<\"query\">, { id });\n}\n\n/**\n * Subscribe to a patient by their Elation ID.\n */\nfunction usePatientByElationId(elationId: string) {\n return useQuery(patientsByElationIdRef as FunctionReference<\"query\">, {\n elationId,\n });\n}\n\n/**\n * Subscribe to a patient by their Hint ID.\n */\nfunction usePatientByHintId(hintId: string) {\n return useQuery(patientsByHintIdRef as FunctionReference<\"query\">, {\n hintId,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Appointment hooks\n// ---------------------------------------------------------------------------\n\ninterface UseAppointmentListOptions {\n patientId?: string;\n status?: string;\n startDate?: string;\n endDate?: string;\n limit?: number;\n}\n\n/**\n * Subscribe to a list of appointments with optional filtering.\n */\nfunction useAppointments(options?: UseAppointmentListOptions) {\n return useQuery(\n appointmentsListRef as FunctionReference<\"query\">,\n options ?? {},\n );\n}\n\n/**\n * Subscribe to a single appointment by Convex document ID.\n */\nfunction useAppointment(id: string) {\n return useQuery(appointmentsGetRef as FunctionReference<\"query\">, { id });\n}\n\n/**\n * Subscribe to an appointment by its Elation ID.\n */\nfunction useAppointmentByElationId(elationId: string) {\n return useQuery(appointmentsByElationIdRef as FunctionReference<\"query\">, {\n elationId,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Physician hooks\n// ---------------------------------------------------------------------------\n\ninterface Physician {\n _id: string;\n elationId: number;\n firstName?: string;\n lastName?: string;\n npi?: string;\n credentials?: string;\n specialties?: string[];\n practice?: number;\n email?: string;\n phone?: string;\n lastSyncedAt: string;\n}\n\nconst physiciansGetByElationIdsRef = makeFunctionReference<\n \"query\",\n { ids: number[] },\n Physician[]\n>(\"physicians:getByElationIds\");\n\nconst physiciansGetByElationIdRef = makeFunctionReference<\n \"query\",\n { id: number },\n Physician | null\n>(\"physicians:getByElationId\");\n\n/**\n * Resolve a batch of physicians by their Elation IDs. Returns an array\n * of physicians that exist in Convex; missing ids are silently dropped.\n *\n * Use this to resolve medication `prescribing_physician` / appointment\n * physician ids to display names, avoiding the per-physician HTTP\n * round-trip through Truth's Elation proxy.\n *\n * Pass `undefined` (or an empty array) to skip the query — the hook\n * returns `undefined` until you pass a populated list.\n */\nfunction usePhysiciansByElationIds(ids: number[] | undefined) {\n return useQuery(\n physiciansGetByElationIdsRef as FunctionReference<\"query\">,\n ids && ids.length > 0 ? { ids } : \"skip\",\n );\n}\n\n/**\n * Subscribe to a single physician by their Elation ID.\n */\nfunction usePhysicianByElationId(id: number | undefined) {\n return useQuery(\n physiciansGetByElationIdRef as FunctionReference<\"query\">,\n id !== undefined ? { id } : \"skip\",\n );\n}\n\n// ---------------------------------------------------------------------------\n// Patient medical hooks (medications, problems, allergies, appointments)\n// ---------------------------------------------------------------------------\n\nconst medicationsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getMedicationsByElationPatient\");\n\nconst problemsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getProblemsByElationPatient\");\n\nconst allergiesByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getAllergiesByElationPatient\");\n\nconst appointmentsByPatientRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown[]\n>(\"medicalRecords:getAppointmentsByElationPatient\");\n\ninterface UsePatientMedicalOptions {\n /**\n * Base URL of the Truth API. Required to trigger the background refresh\n * (e.g. `https://app.truth.communication-hub.com`). If omitted, the hook\n * only reads from Convex and does not refresh.\n */\n apiBaseUrl?: string;\n /** API key used for the refresh call. */\n apiKey?: string;\n /**\n * If true, suppress the background refresh. Useful when you know the\n * data was just refreshed by another component on the page.\n */\n skipRefresh?: boolean;\n}\n\n/**\n * Composite hook that returns a patient's medical records — medications,\n * problems, allergies, appointments — from the Convex cache.\n *\n * On mount (and when `elationId` changes) fires a background refresh\n * against Truth's `/api/patients/medical/refresh` endpoint so stale data\n * is pulled in without blocking render. Returns cached data immediately;\n * Convex subscription updates the UI when refresh completes.\n */\nfunction usePatientMedical(\n elationId: number | undefined,\n options?: UsePatientMedicalOptions,\n) {\n const medications = useQuery(\n medicationsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const problems = useQuery(\n problemsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const allergies = useQuery(\n allergiesByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n const appointments = useQuery(\n appointmentsByPatientRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n ) as unknown[] | undefined;\n\n useEffect(() => {\n if (elationId === undefined || options?.skipRefresh) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/medical/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({ elationId }),\n signal: controller.signal,\n }).catch(() => {\n /* background refresh — failures are non-fatal */\n });\n\n return () => controller.abort();\n }, [elationId, options?.apiBaseUrl, options?.apiKey, options?.skipRefresh]);\n\n return { medications, problems, allergies, appointments };\n}\n\n// ---------------------------------------------------------------------------\n// Patient basic hooks (Elation demographics + Hint memberships/account)\n// ---------------------------------------------------------------------------\n//\n// Replaces the CommHub `getPatientBasicDetails` Hasura Action. Reads\n// from Convex-cached rows (populated by the patients-backfill cron +\n// `/api/patients/basic/refresh` on-demand), falling back to an empty\n// state until the background refresh completes.\n\nconst elationPatientByIdRef = makeFunctionReference<\n \"query\",\n { elationId: number },\n unknown | null\n>(\"elationPatients:getByElationId\");\n\nconst hintPatientByIdRef = makeFunctionReference<\n \"query\",\n { hintId: string },\n unknown | null\n>(\"hintPatients:getByHintId\");\n\nconst pharmacyByNcpdpRef = makeFunctionReference<\n \"query\",\n { ncpdpId: string },\n unknown | null\n>(\"elationPharmacies:getByNcpdpId\");\n\nconst patientPhotoByIdRef = makeFunctionReference<\n \"query\",\n { elationPatientId: number },\n unknown | null\n>(\"elationPatientPhotos:getByElationPatientId\");\n\ninterface UsePatientBasicOptions {\n /** Truth API base URL used for the background refresh. */\n apiBaseUrl?: string;\n /** API key for the refresh call. */\n apiKey?: string;\n /** Suppress the background refresh. */\n skipRefresh?: boolean;\n}\n\ninterface UsePatientBasicResult {\n /**\n * Raw Elation patient payload (matches the shape Elation's\n * `/patients/{id}` returns — `first_name`, `last_name`, `phones`, etc).\n * Returns `undefined` while the cache miss is loading, `null` if the\n * patient isn't in Convex yet (first-open-after-backfill).\n */\n elationPatient: Record<string, unknown> | null | undefined;\n /**\n * Raw Hint patient payload (matches the shape Hint's\n * `/provider/patients/{id}` returns — `memberships`, `account`,\n * `phones`, etc).\n */\n hintPatient: Record<string, unknown> | null | undefined;\n /**\n * The full Convex row for Elation (includes `elationId`, `lastSyncedAt`,\n * `photoS3Key`, `preferredPharmacyNcpdpId`, etc). Use this when you\n * need the structured/typed fields rather than the raw Elation payload.\n */\n elationRow: Record<string, unknown> | null | undefined;\n /**\n * The full Convex row for Hint. Similar relationship to `hintPatient`.\n */\n hintRow: Record<string, unknown> | null | undefined;\n /** True while either cache miss is still pending. */\n loading: boolean;\n}\n\n/**\n * Composite hook returning a patient's basic details — Hint demographics\n * + memberships + account, Elation demographics + clinical metadata —\n * from the Convex cache.\n *\n * On mount (and when inputs change) fires a background refresh against\n * `/api/patients/basic/refresh` so stale rows get pulled fresh without\n * blocking render. Returns cached data immediately; Convex subscription\n * updates the UI when refresh completes.\n */\nfunction usePatientBasic(\n input: { hintId?: string; elationId?: number },\n options?: UsePatientBasicOptions,\n): UsePatientBasicResult {\n const elationRow = useQuery(\n elationPatientByIdRef as FunctionReference<\"query\">,\n input.elationId !== undefined ? { elationId: input.elationId } : \"skip\",\n ) as Record<string, unknown> | null | undefined;\n\n const hintRow = useQuery(\n hintPatientByIdRef as FunctionReference<\"query\">,\n input.hintId !== undefined ? { hintId: input.hintId } : \"skip\",\n ) as Record<string, unknown> | null | undefined;\n\n useEffect(() => {\n if (options?.skipRefresh) {\n return;\n }\n if (!input.hintId && input.elationId === undefined) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/basic/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({\n hintId: input.hintId,\n elationId: input.elationId,\n }),\n signal: controller.signal,\n }).catch(() => {\n /* background refresh — failures are non-fatal */\n });\n\n return () => controller.abort();\n }, [\n input.hintId,\n input.elationId,\n options?.apiBaseUrl,\n options?.apiKey,\n options?.skipRefresh,\n ]);\n\n // Surface the raw payload under elationPatient/hintPatient so consumers\n // can keep using the exact Elation/Hint field names (first_name,\n // memberships, account.past_due_in_cents) with zero mapping.\n const elationPatient =\n elationRow === undefined\n ? undefined\n : elationRow === null\n ? null\n : ((elationRow.raw as Record<string, unknown>) ?? null);\n const hintPatient =\n hintRow === undefined\n ? undefined\n : hintRow === null\n ? null\n : ((hintRow.raw as Record<string, unknown>) ?? null);\n\n const elationLoading =\n input.elationId !== undefined && elationRow === undefined;\n const hintLoading = input.hintId !== undefined && hintRow === undefined;\n\n return {\n elationPatient,\n hintPatient,\n elationRow,\n hintRow,\n loading: elationLoading || hintLoading,\n };\n}\n\n/**\n * Read a shared pharmacy row by NCPDP id. Backs the pharmacy card in\n * the patient panel (from Elation's preferred_pharmacy reference).\n */\nfunction usePharmacyByNcpdpId(ncpdpId: string | undefined) {\n return useQuery(\n pharmacyByNcpdpRef as FunctionReference<\"query\">,\n ncpdpId ? { ncpdpId } : \"skip\",\n );\n}\n\ninterface UsePatientPhotoOptions {\n apiBaseUrl?: string;\n apiKey?: string;\n skipRefresh?: boolean;\n}\n\n/**\n * Subscribe to a patient's profile photo (s3Key + metadata). The\n * consumer constructs a download URL via the Truth attachments resource\n * (signed S3 URL). Fires a background refresh to pull the latest photo\n * binary from Elation + upload to S3.\n */\nfunction usePatientPhoto(\n elationId: number | undefined,\n options?: UsePatientPhotoOptions,\n) {\n const photo = useQuery(\n patientPhotoByIdRef as FunctionReference<\"query\">,\n elationId !== undefined ? { elationPatientId: elationId } : \"skip\",\n );\n\n useEffect(() => {\n if (options?.skipRefresh) {\n return;\n }\n if (elationId === undefined) {\n return;\n }\n const apiBaseUrl = options?.apiBaseUrl;\n const apiKey = options?.apiKey;\n if (!apiBaseUrl || !apiKey) {\n return;\n }\n\n const controller = new AbortController();\n void fetch(`${apiBaseUrl}/api/patients/photo/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": apiKey,\n },\n body: JSON.stringify({ elationId }),\n signal: controller.signal,\n }).catch(() => {\n /* non-fatal */\n });\n\n return () => controller.abort();\n }, [elationId, options?.apiBaseUrl, options?.apiKey, options?.skipRefresh]);\n\n return photo;\n}\n\n// ---------------------------------------------------------------------------\n// Conversation messages (Dialpad calls + SMS merged)\n// ---------------------------------------------------------------------------\n\ninterface ConversationMessage {\n kind: \"call\" | \"sms\";\n id: string;\n providerId: string;\n state: string | null;\n direction: string | null;\n fromNumber: string | null;\n toNumber: string | null;\n voicemailLink: string | null;\n duration: number | null;\n text: string | null;\n mms: boolean;\n mmsUrl: string | null;\n messageStatus: string | null;\n occurredAt: string;\n conversationId: string | null;\n patientId: string | null;\n}\n\nconst messagesByPhonesRef = makeFunctionReference<\n \"query\",\n { phoneA: string; phoneB: string; limit?: number },\n ConversationMessage[]\n>(\"conversationMessages:getByPhones\");\n\nconst messagesByConversationIdRef = makeFunctionReference<\n \"query\",\n { conversationId: string; limit?: number },\n ConversationMessage[]\n>(\"conversationMessages:getByConversationId\");\n\ninterface UseConversationMessagesOptions {\n /** Max items to return (default 200). */\n limit?: number;\n}\n\n/**\n * Subscribe to a conversation's calls + SMS merged chronologically.\n * Pass the patient phone + the provider phone — Truth computes a\n * normalized pair key server-side so formatting differences don't\n * matter.\n *\n * Returns `undefined` while loading, then `ConversationMessage[]`\n * sorted newest-first. Updates live as new webhook events land in\n * Convex via the kinesis consumer.\n */\nfunction useConversationMessages(\n input: { phoneA?: string; phoneB?: string; conversationId?: string },\n options?: UseConversationMessagesOptions,\n): ConversationMessage[] | undefined {\n const hasPair = !!input.phoneA && !!input.phoneB;\n const byPair = useQuery(\n messagesByPhonesRef as FunctionReference<\"query\">,\n hasPair\n ? {\n phoneA: input.phoneA as string,\n phoneB: input.phoneB as string,\n limit: options?.limit,\n }\n : \"skip\",\n ) as ConversationMessage[] | undefined;\n\n const byConvo = useQuery(\n messagesByConversationIdRef as FunctionReference<\"query\">,\n !hasPair && input.conversationId\n ? { conversationId: input.conversationId, limit: options?.limit }\n : \"skip\",\n ) as ConversationMessage[] | undefined;\n\n return hasPair ? byPair : byConvo;\n}\n\nexport {\n usePatients,\n usePatient,\n usePatientByElationId,\n usePatientByHintId,\n useAppointments,\n useAppointment,\n useAppointmentByElationId,\n usePhysiciansByElationIds,\n usePhysicianByElationId,\n usePatientMedical,\n usePatientBasic,\n usePharmacyByNcpdpId,\n usePatientPhoto,\n useConversationMessages,\n};\nexport type { ConversationMessage, UseConversationMessagesOptions };\nexport type {\n UsePatientListOptions,\n UseAppointmentListOptions,\n UsePatientMedicalOptions,\n UsePatientBasicOptions,\n UsePatientBasicResult,\n UsePatientPhotoOptions,\n Physician,\n};\n","/**\n * useNotifications — Truth SDK React hook for push notifications.\n *\n * Shape-compatible with `expo-notifications` where practical so\n * consumers port with minimal change. The hook dynamically imports\n * `expo-notifications` so the SDK doesn't hard-depend on Expo —\n * consumers running on a non-Expo runtime (e.g. a server test harness)\n * can still import the SDK without Metro blowing up.\n *\n * Exposes:\n * - permissionStatus — \"granted\" / \"denied\" / \"undetermined\"\n * - devicePushToken — native APNs/FCM token (undefined until register)\n * - register() — request permission, fetch token, register with Truth\n * - unregister() — revoke the device\n * - addReceivedListener / addResponseListener — re-exports of expo's\n * - getBadgeCount / setBadgeCount — re-exports of expo's\n *\n * Web push (VAPID subscription) lands in Phase 3.\n */\n\n\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n isWebPushSupported,\n registerServiceWorker,\n subscribeToPush,\n subscriptionToJSON,\n} from \"../web-push\";\n\n// `expo-notifications` is an optional peer dep. We lazy-import it via\n// a string-indirection so TypeScript + tsup don't try to resolve its\n// types at build time (the SDK ships without a hard dep so non-Expo\n// consumers — web, Node services — can still import from @hipnation-truth/sdk/react).\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ExpoNotificationsModule = any;\n\nasync function loadExpo(): Promise<ExpoNotificationsModule | null> {\n // Hermes rejects dynamic `import()`, so use Metro's `require()` which\n // is provided in every React Native module's scope. `expo-notifications`\n // is an optional peer dep — non-Expo consumers (web, Node) hit the\n // catch and we return null.\n try {\n // @ts-ignore — require is available at runtime in Metro/Node;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n return require(\"expo-notifications\") as ExpoNotificationsModule;\n } catch {\n return null;\n }\n}\n\nexport type PermissionStatus =\n | \"granted\"\n | \"denied\"\n | \"undetermined\"\n | \"unknown\";\n\nexport interface UseNotificationsOptions {\n /** Truth API base URL, e.g. https://app.truth.communication-hub.com */\n apiBaseUrl: string;\n /** `hn_live_*` API key for the caller's application. */\n apiKey: string;\n /** Current user id — used when registering the device. */\n userId: string | null | undefined;\n /** Optional app version string stored on the device row. */\n appVersion?: string;\n /**\n * Auto-register on mount when permission is already granted.\n * Default: true. Set false if you want to control the registration\n * lifecycle yourself.\n */\n autoRegister?: boolean;\n /** VAPID public key for web push. Fetched automatically if omitted. */\n vapidPublicKey?: string;\n /** Path to the service worker file. Default: \"/truth-sw.js\" */\n serviceWorkerPath?: string;\n}\n\nexport interface UseNotificationsResult {\n permissionStatus: PermissionStatus;\n devicePushToken: string | null;\n register: () => Promise<{ ok: boolean; reason?: string }>;\n unregister: () => Promise<void>;\n addReceivedListener: (listener: (n: unknown) => void) => () => void;\n addResponseListener: (listener: (r: unknown) => void) => () => void;\n getBadgeCount: () => Promise<number>;\n setBadgeCount: (count: number) => Promise<void>;\n}\n\nexport function useNotifications(\n options: UseNotificationsOptions,\n): UseNotificationsResult {\n const [permissionStatus, setPermissionStatus] =\n useState<PermissionStatus>(\"unknown\");\n const [devicePushToken, setDevicePushToken] = useState<string | null>(null);\n const expoRef = useRef<ExpoNotificationsModule | null>(null);\n const isWebRef = useRef(false);\n const vapidKeyRef = useRef<string | null>(options.vapidPublicKey ?? null);\n\n useEffect(() => {\n let mounted = true;\n void (async () => {\n const expo = await loadExpo();\n if (!mounted) {\n return;\n }\n expoRef.current = expo;\n\n if (expo) {\n try {\n const perm = await expo.getPermissionsAsync();\n if (!mounted) {\n return;\n }\n setPermissionStatus(mapStatus(perm?.status));\n } catch {\n setPermissionStatus(\"unknown\");\n }\n return;\n }\n\n if (isWebPushSupported()) {\n isWebRef.current = true;\n if (!vapidKeyRef.current) {\n try {\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/vapid-key`,\n {\n headers: {\n Accept: \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n },\n );\n if (res.ok) {\n const data = await res.json();\n vapidKeyRef.current = data.vapidPublicKey ?? null;\n }\n } catch {\n // best-effort\n }\n }\n if (!mounted) {\n return;\n }\n const webPerm =\n typeof Notification !== \"undefined\"\n ? Notification.permission\n : \"default\";\n setPermissionStatus(\n webPerm === \"granted\"\n ? \"granted\"\n : webPerm === \"denied\"\n ? \"denied\"\n : \"undetermined\",\n );\n } else {\n setPermissionStatus(\"unknown\");\n }\n })();\n return () => {\n mounted = false;\n };\n }, [options.apiBaseUrl, options.apiKey]);\n\n const register = useCallback(async () => {\n if (!options.userId) {\n return { ok: false, reason: \"missing_userId\" };\n }\n\n // Web push path\n if (isWebRef.current) {\n const vapidKey = vapidKeyRef.current;\n if (!vapidKey) {\n return { ok: false, reason: \"no_vapid_key\" };\n }\n\n const webPerm = await Notification.requestPermission();\n setPermissionStatus(\n webPerm === \"granted\"\n ? \"granted\"\n : webPerm === \"denied\"\n ? \"denied\"\n : \"undetermined\",\n );\n if (webPerm !== \"granted\") {\n return { ok: false, reason: \"permission_denied\" };\n }\n\n try {\n const swPath = options.serviceWorkerPath ?? \"/truth-sw.js\";\n const registration = await registerServiceWorker(swPath);\n await navigator.serviceWorker.ready;\n const subscription = await subscribeToPush(registration, vapidKey);\n const subJSON = subscriptionToJSON(subscription);\n setDevicePushToken(subscription.endpoint);\n\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/devices/register`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({\n userId: options.userId,\n platform: \"web\",\n webPushSubscription: subJSON,\n appVersion: options.appVersion,\n locale: navigator.language,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n }),\n },\n );\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n return {\n ok: false,\n reason: `register_failed_${res.status}: ${text.slice(0, 120)}`,\n };\n }\n return { ok: true };\n } catch (err) {\n return {\n ok: false,\n reason: `web_push_error: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n }\n\n // Native (expo-notifications) path\n const expo = expoRef.current ?? (await loadExpo());\n expoRef.current = expo;\n if (!expo) {\n return { ok: false, reason: \"expo_notifications_missing\" };\n }\n\n let perm = await expo.getPermissionsAsync();\n if (perm?.status !== \"granted\") {\n perm = await expo.requestPermissionsAsync();\n }\n setPermissionStatus(mapStatus(perm?.status));\n if (perm?.status !== \"granted\") {\n return { ok: false, reason: \"permission_denied\" };\n }\n\n const tokenResp = await expo.getDevicePushTokenAsync();\n const nativeToken = tokenResp?.data;\n const platform = detectPlatform(tokenResp?.type);\n if (!nativeToken || (platform !== \"ios\" && platform !== \"android\")) {\n return { ok: false, reason: \"no_native_token\" };\n }\n\n setDevicePushToken(nativeToken);\n\n const res = await fetch(\n `${options.apiBaseUrl}/api/notifications/devices/register`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({\n userId: options.userId,\n platform,\n nativeToken,\n appVersion: options.appVersion,\n locale:\n typeof navigator !== \"undefined\" ? navigator.language : undefined,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n }),\n },\n );\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n return {\n ok: false,\n reason: `register_failed_${res.status}: ${text.slice(0, 120)}`,\n };\n }\n return { ok: true };\n }, [\n options.apiBaseUrl,\n options.apiKey,\n options.userId,\n options.appVersion,\n options.serviceWorkerPath,\n ]);\n\n const unregister = useCallback(async () => {\n if (!devicePushToken) {\n return;\n }\n await fetch(`${options.apiBaseUrl}/api/notifications/devices/unregister`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": options.apiKey,\n },\n body: JSON.stringify({ nativeToken: devicePushToken }),\n }).catch(() => {\n /* non-fatal */\n });\n setDevicePushToken(null);\n }, [options.apiBaseUrl, options.apiKey, devicePushToken]);\n\n const addReceivedListener = useCallback(\n (listener: (n: unknown) => void): (() => void) => {\n if (isWebRef.current) {\n if (\n typeof navigator === \"undefined\" ||\n !(\"serviceWorker\" in navigator)\n ) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === \"TRUTH_PUSH_RECEIVED\") {\n listener(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () =>\n navigator.serviceWorker.removeEventListener(\"message\", handler);\n }\n const expo = expoRef.current;\n if (!expo?.addNotificationReceivedListener) {\n return () => {};\n }\n const sub = expo.addNotificationReceivedListener(listener);\n return () => sub.remove?.();\n },\n [],\n );\n\n const addResponseListener = useCallback(\n (listener: (r: unknown) => void): (() => void) => {\n if (isWebRef.current) {\n if (\n typeof navigator === \"undefined\" ||\n !(\"serviceWorker\" in navigator)\n ) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === \"TRUTH_PUSH_TAPPED\") {\n listener(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () =>\n navigator.serviceWorker.removeEventListener(\"message\", handler);\n }\n const expo = expoRef.current;\n if (!expo?.addNotificationResponseReceivedListener) {\n return () => {};\n }\n const sub = expo.addNotificationResponseReceivedListener(listener);\n return () => sub.remove?.();\n },\n [],\n );\n\n const getBadgeCount = useCallback(async (): Promise<number> => {\n const expo = expoRef.current;\n if (!expo?.getBadgeCountAsync) {\n return 0;\n }\n return (await expo.getBadgeCountAsync()) ?? 0;\n }, []);\n\n const setBadgeCount = useCallback(async (count: number): Promise<void> => {\n const expo = expoRef.current;\n if (!expo?.setBadgeCountAsync) {\n return;\n }\n await expo.setBadgeCountAsync(count);\n }, []);\n\n // Auto-register once on mount when conditions are met\n const autoRegister = options.autoRegister !== false;\n useEffect(() => {\n if (!autoRegister) {\n return;\n }\n if (permissionStatus !== \"granted\") {\n return;\n }\n if (devicePushToken) {\n return;\n }\n if (!options.userId) {\n return;\n }\n void register();\n }, [\n autoRegister,\n permissionStatus,\n devicePushToken,\n options.userId,\n register,\n ]);\n\n return {\n permissionStatus,\n devicePushToken,\n register,\n unregister,\n addReceivedListener,\n addResponseListener,\n getBadgeCount,\n setBadgeCount,\n };\n}\n\nfunction mapStatus(status: string | undefined): PermissionStatus {\n if (status === \"granted\") {\n return \"granted\";\n }\n if (status === \"denied\") {\n return \"denied\";\n }\n if (status === \"undetermined\") {\n return \"undetermined\";\n }\n return \"unknown\";\n}\n\nfunction detectPlatform(\n tokenType: string | undefined,\n): \"ios\" | \"android\" | \"web\" | \"unknown\" {\n if (tokenType === \"apns\") {\n return \"ios\";\n }\n if (tokenType === \"fcm\") {\n return \"android\";\n }\n if (tokenType === \"web\") {\n return \"web\";\n }\n return \"unknown\";\n}\n","/**\n * Web Push helpers for browser environments.\n *\n * Handles service worker registration, VAPID push subscription, and\n * message forwarding from the service worker to the main thread.\n */\n\nexport interface WebPushConfig {\n vapidPublicKey: string;\n serviceWorkerPath?: string;\n}\n\nexport function isWebPushSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n \"PushManager\" in window\n );\n}\n\nexport async function registerServiceWorker(\n path = \"/truth-sw.js\",\n): Promise<ServiceWorkerRegistration> {\n return navigator.serviceWorker.register(path);\n}\n\nexport async function subscribeToPush(\n registration: ServiceWorkerRegistration,\n vapidPublicKey: string,\n): Promise<PushSubscription> {\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n return existing;\n }\n\n return registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(\n vapidPublicKey,\n ) as unknown as ArrayBuffer,\n });\n}\n\nexport function subscriptionToJSON(sub: PushSubscription): {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n} {\n const json = sub.toJSON();\n return {\n endpoint: sub.endpoint,\n keys: {\n p256dh: json.keys?.p256dh ?? \"\",\n auth: json.keys?.auth ?? \"\",\n },\n };\n}\n\nfunction urlBase64ToUint8Array(base64String: string): Uint8Array {\n const padding = \"=\".repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const rawData = atob(base64);\n const outputArray = new Uint8Array(rawData.length);\n for (let i = 0; i < rawData.length; ++i) {\n outputArray[i] = rawData.charCodeAt(i);\n }\n return outputArray;\n}\n\nexport function onServiceWorkerMessage(\n type: string,\n callback: (payload: unknown) => void,\n): () => void {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return () => {};\n }\n const handler = (event: MessageEvent) => {\n if (event.data?.type === type) {\n callback(event.data.payload);\n }\n };\n navigator.serviceWorker.addEventListener(\"message\", handler);\n return () => navigator.serviceWorker.removeEventListener(\"message\", handler);\n}\n","/**\n * TruthProvider — Convex React provider pre-configured for Truth.\n *\n * Wraps children in a ConvexProvider connected to the correct\n * Truth Convex deployment for the given environment.\n *\n * @example\n * ```tsx\n * import { TruthProvider } from '@hipnation-truth/sdk/react';\n *\n * function App() {\n * return (\n * <TruthProvider environment=\"sandbox\">\n * <MyApp />\n * </TruthProvider>\n * );\n * }\n * ```\n */\n\n\"use client\";\n\nimport { ConvexProvider, ConvexReactClient } from \"convex/react\";\nimport type { ReactNode } from \"react\";\nimport { createElement, useMemo } from \"react\";\n\n// Mirrors CONVEX_URLS in client.ts. UAT shares prod resources.\n//\n// Exported so `resolveConvexUrl` can be unit-tested without booting\n// React — a regression in this map shipped SDK 0.4.1 that pointed\n// UAT at sandbox Convex. The test in provider.test.ts locks every\n// stage → URL pair down.\nexport const CONVEX_URLS: Record<string, string> = {\n local: \"https://courteous-duck-623.convex.cloud\",\n staging: \"https://courteous-duck-623.convex.cloud\",\n stg: \"https://courteous-duck-623.convex.cloud\",\n sandbox: \"https://courteous-duck-623.convex.cloud\",\n uat: \"https://gallant-gecko-217.convex.cloud\",\n production: \"https://gallant-gecko-217.convex.cloud\",\n};\n\n/**\n * Resolve the Convex URL for a given environment. Honors an explicit\n * override and falls back to sandbox for unknown environments.\n */\nexport function resolveConvexUrl(\n environment: string | undefined,\n override?: string,\n): string {\n if (override) {\n return override;\n }\n const env = environment ?? \"sandbox\";\n return CONVEX_URLS[env] ?? CONVEX_URLS.sandbox;\n}\n\ninterface TruthProviderProps {\n /** Truth environment — determines which Convex deployment to connect to */\n environment?: string;\n /** Override the Convex URL directly */\n convexUrl?: string;\n children: ReactNode;\n}\n\nfunction TruthProvider({\n environment = \"sandbox\",\n convexUrl,\n children,\n}: TruthProviderProps) {\n const url = resolveConvexUrl(environment, convexUrl);\n\n const client = useMemo(() => new ConvexReactClient(url), [url]);\n\n return createElement(ConvexProvider, { client }, children);\n}\n\nexport { TruthProvider };\nexport type { TruthProviderProps };\n","/**\n * React tracking hook — fire-and-forget event tracking from client components.\n *\n * Creates a lightweight Tracker instance scoped to the browser session\n * and exposes a useTrack() hook for emitting events to Truth's Kinesis stream.\n *\n * @example\n * ```tsx\n * import { useTruth } from '@hipnation-truth/sdk/react';\n *\n * function ConversationView({ conversationId }) {\n * const truth = useTruth();\n *\n * useEffect(() => {\n * truth.track('conversation.marked_read.v1', {\n * read_by_actor_id: userId,\n * unread_count_before: 5,\n * unread_count_after: 0,\n * }, { subject: { conversation_id: conversationId } });\n * }, [conversationId]);\n * }\n * ```\n */\n\n\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { createContext, createElement, useContext, useMemo } from \"react\";\nimport type {\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"../tracking/events\";\nimport { Tracker } from \"../tracking/tracker\";\nimport type { ActorContext } from \"../types/config\";\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\ninterface TruthTrackingContextValue {\n track: <T extends EventType>(\n eventType: T,\n payload: EventPayloadMap[T],\n options?: TrackOptions,\n ) => void;\n identify: (actorId: string, actorType: ActorContext[\"actorType\"]) => void;\n}\n\nconst TruthTrackingContext = createContext<TruthTrackingContextValue | null>(\n null,\n);\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\ninterface TruthTrackingProviderProps {\n /** Truth environment — determines API URL for event delivery */\n environment?: string;\n /** Event source identifier */\n source?: string;\n /** Source version (git SHA) */\n sourceVersion?: string;\n /** Default tenant ID */\n tenantId?: string;\n /** API key for authentication */\n apiKey?: string;\n children: ReactNode;\n}\n\nfunction TruthTrackingProvider({\n environment = \"sandbox\",\n source = \"communication-hub.frontend\",\n sourceVersion = \"unknown\",\n tenantId = \"hipnation\",\n apiKey = \"\",\n children,\n}: TruthTrackingProviderProps) {\n const value = useMemo<TruthTrackingContextValue>(() => {\n const tracker = new Tracker({\n apiKey,\n environment,\n source,\n sourceVersion,\n tenantId,\n batchSize: 10,\n flushIntervalMs: 5000,\n });\n\n return {\n track: (eventType, payload, options) => {\n tracker.track(eventType, payload, options);\n },\n identify: (actorId, actorType) => {\n tracker.setActor({ actorId, actorType });\n },\n };\n }, [apiKey, environment, source, sourceVersion, tenantId]);\n\n return createElement(TruthTrackingContext.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Access the Truth tracking context. Must be within a TruthTrackingProvider.\n * Returns `{ track, identify }` for emitting events to Kinesis.\n */\nfunction useTruth(): TruthTrackingContextValue {\n const ctx = useContext(TruthTrackingContext);\n if (!ctx) {\n throw new Error(\"useTruth must be used within a TruthTrackingProvider\");\n }\n return ctx;\n}\n\nexport { TruthTrackingProvider, useTruth };\nexport type { TruthTrackingProviderProps, TruthTrackingContextValue };\n","/**\n * Event tracker with batching, retry, and flush support.\n *\n * Buffers events in an internal queue and flushes them to the Truth API\n * endpoint. Flushes occur when the buffer reaches `batchSize` or every\n * `flushIntervalMs` milliseconds, whichever comes first.\n */\n\nimport type { ActorContext } from \"../types/config\";\nimport type {\n EventEnvelope,\n EventPayloadMap,\n EventType,\n TrackOptions,\n} from \"./events\";\n\n// ---------------------------------------------------------------------------\n// UUID v7 helper (no external dependency)\n// ---------------------------------------------------------------------------\n\n/**\n * Generates a UUID v7 string. Uses crypto.randomUUID where available,\n * falling back to a timestamp + random bytes implementation.\n *\n * UUID v7 layout (RFC 9562):\n * 48 bits - unix timestamp (ms)\n * 4 bits - version (0b0111)\n * 12 bits - random\n * 2 bits - variant (0b10)\n * 62 bits - random\n */\nfunction generateUuidV7(): string {\n const now = Date.now();\n\n // 6 bytes of timestamp\n const timeBytes = new Uint8Array(6);\n let ts = now;\n for (let i = 5; i >= 0; i--) {\n timeBytes[i] = ts & 0xff;\n ts = Math.floor(ts / 256);\n }\n\n // 10 bytes of random\n const randomBytes = new Uint8Array(10);\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n globalThis.crypto.getRandomValues\n ) {\n globalThis.crypto.getRandomValues(randomBytes);\n } else {\n for (let i = 0; i < 10; i++) {\n randomBytes[i] = Math.floor(Math.random() * 256);\n }\n }\n\n // Assemble 16 bytes\n const bytes = new Uint8Array(16);\n bytes.set(timeBytes, 0);\n bytes.set(randomBytes, 6);\n\n // Set version (bits 48-51 to 0b0111)\n bytes[6] = (bytes[6] & 0x0f) | 0x70;\n\n // Set variant (bits 64-65 to 0b10)\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n\n // Format as hex string with dashes\n const hex = Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return [\n hex.slice(0, 8),\n hex.slice(8, 12),\n hex.slice(12, 16),\n hex.slice(16, 20),\n hex.slice(20, 32),\n ].join(\"-\");\n}\n\n// ---------------------------------------------------------------------------\n// Environment-based API URL resolution\n// ---------------------------------------------------------------------------\n\n// Environment → Truth API base URL.\n//\n// Topology:\n// local / staging / stg / sandbox → sandbox Truth (app.sandbox.*)\n// uat / production → production Truth (app.truth.*)\n//\n// UAT shares production resources (same Fly Postgres + Hasura + Truth +\n// Convex + EHR tokens as prod). Staging is the isolated sandbox env.\nconst API_URLS: Record<string, string> = {\n local: \"http://localhost:3000\",\n staging: \"https://app.sandbox.communication-hub.com\",\n stg: \"https://app.sandbox.communication-hub.com\",\n sandbox: \"https://app.sandbox.communication-hub.com\",\n uat: \"https://app.truth.communication-hub.com\",\n production: \"https://app.truth.communication-hub.com\",\n};\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_BATCH_SIZE = 25;\nconst DEFAULT_FLUSH_INTERVAL_MS = 5_000;\nconst MAX_RETRIES = 3;\nconst BASE_RETRY_DELAY_MS = 500;\n\n// ---------------------------------------------------------------------------\n// Tracker configuration\n// ---------------------------------------------------------------------------\n\ninterface TrackerConfig {\n apiKey: string;\n environment: string;\n source: string;\n sourceVersion: string;\n tenantId: string;\n batchSize: number;\n flushIntervalMs: number;\n apiBaseUrl?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Tracker class\n// ---------------------------------------------------------------------------\n\nclass Tracker {\n private readonly config: TrackerConfig;\n readonly apiUrl: string;\n private queue: EventEnvelope[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private defaultActor: ActorContext | undefined;\n private isFlushing = false;\n private isShutdown = false;\n\n constructor(config: TrackerConfig) {\n this.config = config;\n this.apiUrl =\n config.apiBaseUrl ?? API_URLS[config.environment] ?? API_URLS.local;\n\n this.startFlushInterval();\n this.registerShutdownHooks();\n }\n\n /**\n * Set the default actor context for subsequent events.\n */\n setActor(actor: ActorContext): void {\n this.defaultActor = actor;\n }\n\n /**\n * Enqueue a typed event for delivery. This is fire-and-forget from\n * the caller's perspective -- events are buffered and flushed in batches.\n */\n track<T extends EventType>(\n eventType: T,\n payload: EventPayloadMap[T],\n options?: TrackOptions,\n ): void {\n if (this.isShutdown) {\n return;\n }\n\n const now = new Date().toISOString();\n\n const envelope: EventEnvelope = {\n event_id: generateUuidV7(),\n event_type: eventType,\n schema_version: 1,\n occurred_at: options?.occurredAt ?? now,\n received_at: now,\n source: this.config.source,\n source_version: this.config.sourceVersion,\n tenant_id: options?.tenantId ?? this.config.tenantId,\n actor:\n options?.actor ??\n (this.defaultActor\n ? {\n actor_id: this.defaultActor.actorId,\n actor_type: this.defaultActor.actorType,\n }\n : undefined),\n subject: options?.subject,\n compliance: options?.compliance,\n payload: payload as unknown as Record<string, unknown>,\n };\n\n this.queue.push(envelope);\n\n if (this.queue.length >= this.config.batchSize) {\n void this.flush();\n }\n }\n\n /**\n * Force an immediate flush of all buffered events.\n * Returns a promise that resolves when the flush completes.\n */\n async flush(): Promise<void> {\n if (this.queue.length === 0 || this.isFlushing) {\n return;\n }\n\n this.isFlushing = true;\n const batch = this.queue.splice(0, this.config.batchSize);\n\n try {\n await this.sendBatch(batch);\n } catch {\n // Re-queue events that failed to send (prepend to maintain ordering)\n this.queue.unshift(...batch);\n } finally {\n this.isFlushing = false;\n }\n\n // If there are still events in the queue, flush again\n if (this.queue.length >= this.config.batchSize) {\n await this.flush();\n }\n }\n\n /**\n * Gracefully shut down the tracker. Flushes remaining events and\n * clears the flush interval.\n */\n async shutdown(): Promise<void> {\n this.isShutdown = true;\n this.stopFlushInterval();\n await this.flush();\n }\n\n /**\n * Send a batch of events to the Truth API with exponential backoff retry.\n */\n private async sendBatch(batch: EventEnvelope[]): Promise<void> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(`${this.apiUrl}/api/events/ingest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": this.config.apiKey,\n },\n body: JSON.stringify({ events: batch }),\n });\n\n if (response.ok) {\n return;\n }\n\n // Don't retry 4xx client errors (except 429)\n if (\n response.status >= 400 &&\n response.status < 500 &&\n response.status !== 429\n ) {\n return;\n }\n\n lastError = new Error(\n `HTTP ${response.status}: ${response.statusText}`,\n );\n } catch (error) {\n lastError = error;\n }\n\n // Exponential backoff with jitter\n if (attempt < MAX_RETRIES) {\n const delay = BASE_RETRY_DELAY_MS * 2 ** attempt;\n const jitter = Math.random() * delay * 0.5;\n await sleep(delay + jitter);\n }\n }\n\n throw lastError;\n }\n\n private startFlushInterval(): void {\n if (this.config.flushIntervalMs > 0) {\n this.flushTimer = setInterval(() => {\n void this.flush();\n }, this.config.flushIntervalMs);\n\n // Unref the timer so it doesn't prevent Node.js from exiting\n if (typeof this.flushTimer === \"object\" && \"unref\" in this.flushTimer) {\n this.flushTimer.unref();\n }\n }\n }\n\n private stopFlushInterval(): void {\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n }\n\n private registerShutdownHooks(): void {\n // Only register in Node.js environments\n if (typeof globalThis.process !== \"undefined\" && globalThis.process.on) {\n const shutdownHandler = () => {\n void this.shutdown();\n };\n\n globalThis.process.on(\"beforeExit\", shutdownHandler);\n globalThis.process.on(\"SIGTERM\", shutdownHandler);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport {\n Tracker,\n generateUuidV7,\n DEFAULT_BATCH_SIZE,\n DEFAULT_FLUSH_INTERVAL_MS,\n};\nexport type { TrackerConfig };\n"],"mappings":"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","names":["useQuery","makeFunctionReference","useEffect","useEffect","_a","res","createElement","useMemo","useMemo","createElement"]}
|