@everfur/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/animations/package.json +8 -0
- package/chat/package.json +8 -0
- package/client/package.json +8 -0
- package/core/package.json +8 -0
- package/dist/CameraPort-Cv31Pz7g.d.cts +29 -0
- package/dist/CameraPort-Cv31Pz7g.d.ts +29 -0
- package/dist/ChatController-CKdBvPj2.d.ts +146 -0
- package/dist/ChatController-CpUMvvZf.d.cts +146 -0
- package/dist/EverfurResult-D92-uL82.d.cts +240 -0
- package/dist/EverfurResult-D92-uL82.d.ts +240 -0
- package/dist/FilePort-BabWrv7I.d.cts +22 -0
- package/dist/FilePort-BabWrv7I.d.ts +22 -0
- package/dist/PhotoController-BItt5M7u.d.cts +43 -0
- package/dist/PhotoController-D8zMTdcW.d.ts +43 -0
- package/dist/TelemetryPort-BDNr00hu.d.cts +12 -0
- package/dist/TelemetryPort-BDNr00hu.d.ts +12 -0
- package/dist/animations/index.cjs +1997 -0
- package/dist/animations/index.d.cts +517 -0
- package/dist/animations/index.d.ts +517 -0
- package/dist/animations/index.js +1972 -0
- package/dist/cacheEpoch-DknKn3S0.d.cts +36 -0
- package/dist/cacheEpoch-DknKn3S0.d.ts +36 -0
- package/dist/chat/index.cjs +1170 -0
- package/dist/chat/index.d.cts +59 -0
- package/dist/chat/index.d.ts +59 -0
- package/dist/chat/index.js +1167 -0
- package/dist/client/index.cjs +2317 -0
- package/dist/client/index.d.cts +65 -0
- package/dist/client/index.d.ts +65 -0
- package/dist/client/index.js +2217 -0
- package/dist/config-BSjBdxrZ.d.cts +501 -0
- package/dist/config-CiJ0PVBB.d.ts +501 -0
- package/dist/core/index.cjs +3175 -0
- package/dist/core/index.d.cts +70 -0
- package/dist/core/index.d.ts +70 -0
- package/dist/core/index.js +3163 -0
- package/dist/identity-Brl-lDd6.d.cts +91 -0
- package/dist/identity-DK9zORrG.d.ts +91 -0
- package/dist/ids-CJ1S6adf.d.cts +46 -0
- package/dist/ids-CJ1S6adf.d.ts +46 -0
- package/dist/index.cjs +4488 -0
- package/dist/index.d.cts +156 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +4464 -0
- package/dist/petsRepository-BEGb97M9.d.cts +326 -0
- package/dist/petsRepository-Bu18r2kK.d.ts +326 -0
- package/dist/photo/index.cjs +2189 -0
- package/dist/photo/index.d.cts +43 -0
- package/dist/photo/index.d.ts +43 -0
- package/dist/photo/index.js +2186 -0
- package/dist/projection-CeIUsbSk.d.cts +8 -0
- package/dist/projection-CeIUsbSk.d.ts +8 -0
- package/dist/records/index.cjs +2840 -0
- package/dist/records/index.d.cts +224 -0
- package/dist/records/index.d.ts +224 -0
- package/dist/records/index.js +2834 -0
- package/dist/requestFunnel-DuUH-kAe.d.cts +28 -0
- package/dist/requestFunnel-dio5OmR9.d.ts +28 -0
- package/dist/resolve-Dq_4_agU.d.cts +86 -0
- package/dist/resolve-Dq_4_agU.d.ts +86 -0
- package/dist/runtime-BgQnA594.d.cts +349 -0
- package/dist/runtime-CBA-LvdM.d.ts +349 -0
- package/dist/server/index.cjs +533 -0
- package/dist/server/index.d.cts +48 -0
- package/dist/server/index.d.ts +48 -0
- package/dist/server/index.js +530 -0
- package/dist/testing/index.cjs +825 -0
- package/dist/testing/index.d.cts +113 -0
- package/dist/testing/index.d.ts +113 -0
- package/dist/testing/index.js +822 -0
- package/dist/testing/rn/index.cjs +449 -0
- package/dist/testing/rn/index.d.cts +49 -0
- package/dist/testing/rn/index.d.ts +49 -0
- package/dist/testing/rn/index.js +444 -0
- package/dist/video/index.cjs +1941 -0
- package/dist/video/index.d.cts +66 -0
- package/dist/video/index.d.ts +66 -0
- package/dist/video/index.js +1938 -0
- package/package.json +311 -0
- package/photo/package.json +8 -0
- package/records/package.json +8 -0
- package/server/device-blocked.cjs +15 -0
- package/server/package.json +9 -0
- package/testing/package.json +8 -0
- package/testing/rn/package.json +8 -0
- package/video/package.json +8 -0
|
@@ -0,0 +1,2834 @@
|
|
|
1
|
+
import { createContext, useState, useRef, useEffect, useCallback, useMemo, useReducer, useContext, useSyncExternalStore } from 'react';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import 'react-native-safe-area-context';
|
|
4
|
+
import { View, ActivityIndicator, Pressable, Text, AccessibilityInfo, ScrollView, StyleSheet } from 'react-native';
|
|
5
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
// src/rn/records/useRecords.ts
|
|
8
|
+
var CONTRACT_RETRYABILITY = Object.freeze([
|
|
9
|
+
Object.freeze({ wireCode: "AccessDeniedException", envelope: "http_envelope", httpStatus: 403, retryable: false }),
|
|
10
|
+
Object.freeze({ wireCode: "AuthorizationException", envelope: "http_envelope", httpStatus: 401, retryable: false }),
|
|
11
|
+
Object.freeze({ wireCode: "ConflictException", envelope: "http_envelope", httpStatus: 409, retryable: true }),
|
|
12
|
+
Object.freeze({ wireCode: "DAILY_QUOTA_EXCEEDED", envelope: "http_envelope", httpStatus: 429, retryable: true }),
|
|
13
|
+
Object.freeze({ wireCode: "IDEMPOTENCY_KEY_IN_FLIGHT", envelope: "http_envelope", httpStatus: 409, retryable: true }),
|
|
14
|
+
Object.freeze({ wireCode: "IDEMPOTENCY_KEY_REUSED_DIFFERENT_REQUEST", envelope: "http_envelope", httpStatus: 409, retryable: false }),
|
|
15
|
+
Object.freeze({ wireCode: "InternalFailureException", envelope: "http_envelope", httpStatus: 500, retryable: true }),
|
|
16
|
+
Object.freeze({ wireCode: "NotImplementedException", envelope: "http_envelope", httpStatus: 501, retryable: false }),
|
|
17
|
+
Object.freeze({ wireCode: "PaymentRequiredException", envelope: "http_envelope", httpStatus: 402, retryable: false }),
|
|
18
|
+
Object.freeze({ wireCode: "ResourceNotFoundException", envelope: "http_envelope", httpStatus: 404, retryable: false }),
|
|
19
|
+
Object.freeze({ wireCode: "ServiceUnavailableException", envelope: "http_envelope", httpStatus: 503, retryable: true }),
|
|
20
|
+
Object.freeze({ wireCode: "TIER_UPGRADE_REQUIRED", envelope: "http_envelope", httpStatus: 402, retryable: false }),
|
|
21
|
+
Object.freeze({ wireCode: "ThrottlingException", envelope: "http_envelope", httpStatus: 429, retryable: true }),
|
|
22
|
+
Object.freeze({ wireCode: "ValidationException", envelope: "http_envelope", httpStatus: 400, retryable: false }),
|
|
23
|
+
Object.freeze({ wireCode: "accessDenied", envelope: "http_envelope", httpStatus: 403, retryable: false }),
|
|
24
|
+
Object.freeze({ wireCode: "authExpired", envelope: "http_envelope", httpStatus: 401, retryable: true }),
|
|
25
|
+
Object.freeze({ wireCode: "internal_error", envelope: "sse_frame", httpStatus: 200, retryable: false }),
|
|
26
|
+
Object.freeze({ wireCode: "invalidRequest", envelope: "http_envelope", httpStatus: 400, retryable: false }),
|
|
27
|
+
Object.freeze({ wireCode: "not_found", envelope: "sse_frame", httpStatus: 200, retryable: false }),
|
|
28
|
+
Object.freeze({ wireCode: "session_required", envelope: "http_envelope", httpStatus: 403, retryable: false }),
|
|
29
|
+
Object.freeze({ wireCode: "timeout", envelope: "sse_frame", httpStatus: 200, retryable: true }),
|
|
30
|
+
Object.freeze({ wireCode: "unavailable", envelope: "http_envelope", httpStatus: 503, retryable: true })
|
|
31
|
+
]);
|
|
32
|
+
Object.freeze(
|
|
33
|
+
CONTRACT_RETRYABILITY.map((row) => row.wireCode)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// src/client/errors/contract.ts
|
|
37
|
+
var NORMALIZED_FOLD = Object.freeze({
|
|
38
|
+
// the default status-mapped envelope
|
|
39
|
+
ValidationException: "validationFailed",
|
|
40
|
+
AuthorizationException: "authRejected",
|
|
41
|
+
PaymentRequiredException: "paymentRequired",
|
|
42
|
+
AccessDeniedException: "accessDenied",
|
|
43
|
+
ResourceNotFoundException: "resourceNotFound",
|
|
44
|
+
ConflictException: "conflict",
|
|
45
|
+
ThrottlingException: "rateLimited",
|
|
46
|
+
InternalFailureException: "internalError",
|
|
47
|
+
NotImplementedException: "notImplemented",
|
|
48
|
+
ServiceUnavailableException: "serviceUnavailable",
|
|
49
|
+
// dict-detail codes on the widget surface
|
|
50
|
+
authExpired: "authExpired",
|
|
51
|
+
unavailable: "serviceUnavailable",
|
|
52
|
+
session_required: "sessionRequired",
|
|
53
|
+
accessDenied: "accessDenied",
|
|
54
|
+
invalidRequest: "sdkUpdateRequired",
|
|
55
|
+
// typed codes
|
|
56
|
+
IDEMPOTENCY_KEY_IN_FLIGHT: "idempotencyInFlight",
|
|
57
|
+
IDEMPOTENCY_KEY_REUSED_DIFFERENT_REQUEST: "idempotencyConflict",
|
|
58
|
+
DAILY_QUOTA_EXCEEDED: "quotaExceeded",
|
|
59
|
+
TIER_UPGRADE_REQUIRED: "paymentRequired",
|
|
60
|
+
// SSE error frames
|
|
61
|
+
not_found: "conversationNotFound",
|
|
62
|
+
timeout: "streamTimeout",
|
|
63
|
+
internal_error: "internalError"
|
|
64
|
+
});
|
|
65
|
+
var CONTRACT_ERROR_ROWS = Object.freeze(
|
|
66
|
+
CONTRACT_RETRYABILITY.map(
|
|
67
|
+
(generated) => Object.freeze({
|
|
68
|
+
wireCode: generated.wireCode,
|
|
69
|
+
envelope: generated.envelope,
|
|
70
|
+
retryable: generated.retryable,
|
|
71
|
+
normalized: NORMALIZED_FOLD[generated.wireCode]
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
var BY_UPPER = new Map(
|
|
76
|
+
CONTRACT_ERROR_ROWS.map((r) => [r.wireCode.toUpperCase(), r])
|
|
77
|
+
);
|
|
78
|
+
function contractErrorFor(wireCode) {
|
|
79
|
+
if (typeof wireCode !== "string") return void 0;
|
|
80
|
+
const key = wireCode.trim().toUpperCase();
|
|
81
|
+
return key.length > 0 ? BY_UPPER.get(key) : void 0;
|
|
82
|
+
}
|
|
83
|
+
var CATEGORY_OF = Object.freeze({
|
|
84
|
+
// the caller's sign-in state is wrong and a credential change is the remedy
|
|
85
|
+
authExpired: "authentication",
|
|
86
|
+
authRejected: "authentication",
|
|
87
|
+
sessionRequired: "authentication",
|
|
88
|
+
// signed in, correctly identified, and still not allowed
|
|
89
|
+
accessDenied: "authorization",
|
|
90
|
+
// a plan gate, not a permission failure: the remedy is commercial
|
|
91
|
+
paymentRequired: "entitlement",
|
|
92
|
+
// slow down / come back later
|
|
93
|
+
quotaExceeded: "rateLimit",
|
|
94
|
+
rateLimited: "rateLimit",
|
|
95
|
+
// the request itself was wrong; retrying it unchanged can never succeed
|
|
96
|
+
validationFailed: "invalidRequest",
|
|
97
|
+
sdkUpdateRequired: "invalidRequest",
|
|
98
|
+
// the target is gone, hidden, or was never visible to this caller
|
|
99
|
+
conversationNotFound: "notFound",
|
|
100
|
+
petNotFound: "notFound",
|
|
101
|
+
resourceNotFound: "notFound",
|
|
102
|
+
// something else already holds the thing
|
|
103
|
+
conflict: "conflict",
|
|
104
|
+
idempotencyConflict: "conflict",
|
|
105
|
+
idempotencyInFlight: "conflict",
|
|
106
|
+
// the pipe, not the request: timeouts, drops, a briefly unreachable dependency
|
|
107
|
+
streamTimeout: "connection",
|
|
108
|
+
streamIncomplete: "connection",
|
|
109
|
+
upstreamLlmTimeout: "connection",
|
|
110
|
+
serviceUnavailable: "connection",
|
|
111
|
+
// the server admits fault
|
|
112
|
+
internalError: "server",
|
|
113
|
+
notImplemented: "server",
|
|
114
|
+
mediaUploadFailed: "media",
|
|
115
|
+
moderationBlocked: "policy",
|
|
116
|
+
unknown: "unknown"
|
|
117
|
+
});
|
|
118
|
+
function categoryFor(code) {
|
|
119
|
+
return CATEGORY_OF[code];
|
|
120
|
+
}
|
|
121
|
+
function fromWire(wire) {
|
|
122
|
+
const contractRow = contractErrorFor(wire);
|
|
123
|
+
if (contractRow) return contractRow.normalized;
|
|
124
|
+
const upper = (wire ?? "").trim().toUpperCase();
|
|
125
|
+
const canonical = upper.replace(/[^A-Z0-9]/g, "");
|
|
126
|
+
if (canonical === "AUTHREJECTED") return "authRejected";
|
|
127
|
+
if (upper.startsWith("AUTH")) return "authExpired";
|
|
128
|
+
switch (canonical) {
|
|
129
|
+
case "DAILYQUOTAEXCEEDED":
|
|
130
|
+
case "QUOTAEXCEEDED":
|
|
131
|
+
return "quotaExceeded";
|
|
132
|
+
case "RATELIMITED":
|
|
133
|
+
case "TOOMANYREQUESTS":
|
|
134
|
+
return "rateLimited";
|
|
135
|
+
case "IDEMPOTENCYKEYCONFLICT":
|
|
136
|
+
case "IDEMPOTENCYCONFLICT":
|
|
137
|
+
return "idempotencyConflict";
|
|
138
|
+
case "ACCESSDENIED":
|
|
139
|
+
case "FORBIDDEN":
|
|
140
|
+
return "accessDenied";
|
|
141
|
+
case "NOTFOUND":
|
|
142
|
+
case "CONVERSATIONNOTFOUND":
|
|
143
|
+
return "conversationNotFound";
|
|
144
|
+
case "PETNOTFOUND":
|
|
145
|
+
return "petNotFound";
|
|
146
|
+
case "TIMEOUT":
|
|
147
|
+
case "STREAMTIMEOUT":
|
|
148
|
+
return "streamTimeout";
|
|
149
|
+
// Analysis-domain deterministic 400s (NON-retryable). Additive per SPEC-00 §3.1 / SPEC-11b §2.6:
|
|
150
|
+
// fold to a NON-retried target so the SDK never retries a permanently-failing bad-input request.
|
|
151
|
+
// (Comment above the group so the empty fall-through cases stay lint-clean under no-fallthrough.)
|
|
152
|
+
case "MEDIAUPLOADFAILED":
|
|
153
|
+
case "UNRECOGNIZEDVIDEOFORMAT":
|
|
154
|
+
case "FILETOOSMALL":
|
|
155
|
+
return "mediaUploadFailed";
|
|
156
|
+
case "MODERATIONBLOCKED":
|
|
157
|
+
return "moderationBlocked";
|
|
158
|
+
case "UPSTREAMLLMTIMEOUT":
|
|
159
|
+
return "upstreamLlmTimeout";
|
|
160
|
+
case "INTERNALERROR":
|
|
161
|
+
case "INTERNALSERVERERROR":
|
|
162
|
+
return "internalError";
|
|
163
|
+
case "STREAMINCOMPLETE":
|
|
164
|
+
case "CHATSTREAMINCOMPLETE":
|
|
165
|
+
return "streamIncomplete";
|
|
166
|
+
// Deterministic 400s that can only come from an SDK bug -> unknown (NON-retryable). NEVER internalError
|
|
167
|
+
// (which IS auto-retried): mapping a bad-input 400 to a retryable code retries a request that never succeeds.
|
|
168
|
+
case "INVALIDANALYSISTYPE":
|
|
169
|
+
case "INVALIDUPLOADPURPOSE":
|
|
170
|
+
return "unknown";
|
|
171
|
+
default:
|
|
172
|
+
return "unknown";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
var AUTO_RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
176
|
+
"streamTimeout",
|
|
177
|
+
"rateLimited",
|
|
178
|
+
"internalError",
|
|
179
|
+
"upstreamLlmTimeout",
|
|
180
|
+
"streamIncomplete"
|
|
181
|
+
]);
|
|
182
|
+
function isAutoRetryable(code) {
|
|
183
|
+
return AUTO_RETRYABLE_CODES.has(code);
|
|
184
|
+
}
|
|
185
|
+
var RETRYABLE_CONTRACT_CODES = /* @__PURE__ */ new Set([
|
|
186
|
+
"serviceUnavailable",
|
|
187
|
+
"conflict",
|
|
188
|
+
"idempotencyInFlight"
|
|
189
|
+
]);
|
|
190
|
+
function isRetryableCode(code) {
|
|
191
|
+
return isAutoRetryable(code) || RETRYABLE_CONTRACT_CODES.has(code);
|
|
192
|
+
}
|
|
193
|
+
var BACKOFF_BASE_MS = 500;
|
|
194
|
+
var BACKOFF_CAP_MS = 8e3;
|
|
195
|
+
var MAX_RETRY_ATTEMPTS = 4;
|
|
196
|
+
function backoffDelayMs(attempt, rng = Math.random) {
|
|
197
|
+
const bounded = Math.max(0, attempt);
|
|
198
|
+
const ceiling = Math.min(BACKOFF_BASE_MS * 2 ** bounded, BACKOFF_CAP_MS);
|
|
199
|
+
return Math.floor(rng() * ceiling);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/client/errors/EverfurError.ts
|
|
203
|
+
var USER_FACING_COPY = Object.freeze({
|
|
204
|
+
authExpired: "Please sign in again to continue.",
|
|
205
|
+
authRejected: "Please sign in again to continue.",
|
|
206
|
+
sessionRequired: "This app needs to be updated to keep using Everfur.",
|
|
207
|
+
accessDenied: "This feature isn't available on your account right now.",
|
|
208
|
+
paymentRequired: "This is part of a paid plan.",
|
|
209
|
+
quotaExceeded: "You have used today's messages. This resets tomorrow.",
|
|
210
|
+
rateLimited: "You are sending messages quickly. Try again in a few seconds.",
|
|
211
|
+
validationFailed: "Something in that request was not right. Check the details and try again.",
|
|
212
|
+
sdkUpdateRequired: "This app needs to be updated to keep using Everfur.",
|
|
213
|
+
conflict: "That is already running. Wait for it to finish, then try again.",
|
|
214
|
+
idempotencyConflict: "That message was already delivered.",
|
|
215
|
+
idempotencyInFlight: "That is still processing. Give it a moment.",
|
|
216
|
+
conversationNotFound: "This conversation is no longer available.",
|
|
217
|
+
petNotFound: "We could not find that pet. Please pick a pet and try again.",
|
|
218
|
+
resourceNotFound: "We could not find that.",
|
|
219
|
+
streamTimeout: "The response timed out. Tap retry to try again.",
|
|
220
|
+
mediaUploadFailed: "We could not upload that photo. Please attach it again.",
|
|
221
|
+
moderationBlocked: "This request cannot be answered here.",
|
|
222
|
+
upstreamLlmTimeout: "The response timed out. Tap retry to try again.",
|
|
223
|
+
internalError: "We hit a snag on our end. Please try again.",
|
|
224
|
+
notImplemented: "That is not available yet.",
|
|
225
|
+
serviceUnavailable: "That service is briefly unavailable. Try again shortly.",
|
|
226
|
+
streamIncomplete: "The connection dropped mid-answer. Tap retry to continue.",
|
|
227
|
+
unknown: "Something went wrong. Please try again."
|
|
228
|
+
});
|
|
229
|
+
function userFacingError(code) {
|
|
230
|
+
return USER_FACING_COPY[code];
|
|
231
|
+
}
|
|
232
|
+
function defineHidden(target, key, value) {
|
|
233
|
+
Object.defineProperty(target, key, {
|
|
234
|
+
value,
|
|
235
|
+
enumerable: false,
|
|
236
|
+
writable: false,
|
|
237
|
+
configurable: false
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function makeEverfurError(code, opts) {
|
|
241
|
+
contractErrorFor(opts?.wireCode);
|
|
242
|
+
const error = {
|
|
243
|
+
code,
|
|
244
|
+
category: categoryFor(code),
|
|
245
|
+
retryable: opts?.retryable ?? isRetryableCode(code),
|
|
246
|
+
displayMessage: userFacingError(code)
|
|
247
|
+
};
|
|
248
|
+
if (opts?.httpStatus !== void 0) error.httpStatus = opts.httpStatus;
|
|
249
|
+
if (opts?.requestId) error.requestId = opts.requestId;
|
|
250
|
+
if (opts?.wireMessage) defineHidden(error, "wireMessage", opts.wireMessage);
|
|
251
|
+
if (opts?.raw !== void 0 && opts.raw !== null) defineHidden(error, "raw", opts.raw);
|
|
252
|
+
return Object.freeze(error);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/client/errors/EverfurResult.ts
|
|
256
|
+
function ok(value, warning) {
|
|
257
|
+
return Object.freeze(
|
|
258
|
+
{ ok: true, value }
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
function err(error) {
|
|
262
|
+
return Object.freeze({ ok: false, error });
|
|
263
|
+
}
|
|
264
|
+
var EverfurConfigError = class _EverfurConfigError extends Error {
|
|
265
|
+
constructor(kind, message) {
|
|
266
|
+
super(message);
|
|
267
|
+
this.name = "EverfurConfigError";
|
|
268
|
+
this.kind = kind;
|
|
269
|
+
Object.setPrototypeOf(this, _EverfurConfigError.prototype);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
function brandPetRef(raw) {
|
|
273
|
+
return raw;
|
|
274
|
+
}
|
|
275
|
+
function brandRequestId(raw) {
|
|
276
|
+
return raw;
|
|
277
|
+
}
|
|
278
|
+
function brandDocumentId(raw) {
|
|
279
|
+
return raw;
|
|
280
|
+
}
|
|
281
|
+
var KNOWN_STATUSES = /* @__PURE__ */ new Set([
|
|
282
|
+
"awaiting_upload",
|
|
283
|
+
"received",
|
|
284
|
+
"parsing",
|
|
285
|
+
"in_review",
|
|
286
|
+
"published",
|
|
287
|
+
"failed",
|
|
288
|
+
"revoked"
|
|
289
|
+
]);
|
|
290
|
+
var STATUS_ALIASES = Object.freeze({
|
|
291
|
+
records_received: "received",
|
|
292
|
+
// the server's spelling; `received` is never sent verbatim
|
|
293
|
+
parsing_failed: "failed",
|
|
294
|
+
// TERMINAL. Folding to `parsing` polled forever on a dead request.
|
|
295
|
+
clinic_declined: "failed"
|
|
296
|
+
// TERMINAL. The clinic refused; nothing further will arrive.
|
|
297
|
+
});
|
|
298
|
+
function normalizeStatus(raw) {
|
|
299
|
+
const aliased = Object.prototype.hasOwnProperty.call(STATUS_ALIASES, raw) ? STATUS_ALIASES[raw] : void 0;
|
|
300
|
+
if (aliased !== void 0) return aliased;
|
|
301
|
+
const value = raw;
|
|
302
|
+
return KNOWN_STATUSES.has(value) ? value : "parsing";
|
|
303
|
+
}
|
|
304
|
+
function normalizeOrigin(raw) {
|
|
305
|
+
return raw === "owner_reported" || raw === "clinic_document" ? raw : null;
|
|
306
|
+
}
|
|
307
|
+
var requestSchema = z.object({
|
|
308
|
+
request_id: z.string().min(1),
|
|
309
|
+
pet_ref: z.string().min(1),
|
|
310
|
+
status: z.string(),
|
|
311
|
+
status_label: z.string().optional(),
|
|
312
|
+
document_count: z.number().optional(),
|
|
313
|
+
held_item_count: z.number().optional(),
|
|
314
|
+
received_at: z.string().nullish(),
|
|
315
|
+
parsing_started_at: z.string().nullish(),
|
|
316
|
+
review_started_at: z.string().nullish(),
|
|
317
|
+
created_at: z.string().optional(),
|
|
318
|
+
updated_at: z.string().optional()
|
|
319
|
+
}).passthrough();
|
|
320
|
+
function mapRequestView(wire) {
|
|
321
|
+
const parsed = requestSchema.safeParse(wire);
|
|
322
|
+
if (!parsed.success) return null;
|
|
323
|
+
const w = parsed.data;
|
|
324
|
+
return Object.freeze({
|
|
325
|
+
id: brandRequestId(w.request_id),
|
|
326
|
+
petRef: brandPetRef(w.pet_ref),
|
|
327
|
+
status: normalizeStatus(w.status),
|
|
328
|
+
statusLabel: w.status_label ?? "",
|
|
329
|
+
documentCount: numberOr(w.document_count, 0),
|
|
330
|
+
heldItemCount: numberOr(w.held_item_count, 0),
|
|
331
|
+
receivedAt: w.received_at ?? null,
|
|
332
|
+
parsingStartedAt: w.parsing_started_at ?? null,
|
|
333
|
+
reviewStartedAt: w.review_started_at ?? null,
|
|
334
|
+
createdAt: w.created_at ?? "",
|
|
335
|
+
updatedAt: w.updated_at ?? ""
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function mapRequestList(wire) {
|
|
339
|
+
const list = Array.isArray(wire) ? wire : [];
|
|
340
|
+
const out = [];
|
|
341
|
+
for (const entry of list) {
|
|
342
|
+
const view = mapRequestView(entry);
|
|
343
|
+
if (view !== null) out.push(view);
|
|
344
|
+
}
|
|
345
|
+
return Object.freeze(out);
|
|
346
|
+
}
|
|
347
|
+
var acceptedSchema = z.object({
|
|
348
|
+
source_doc_id: z.string().min(1),
|
|
349
|
+
request_id: z.string().min(1)
|
|
350
|
+
});
|
|
351
|
+
function mapUploadedDocument(wire) {
|
|
352
|
+
const parsed = acceptedSchema.safeParse(wire);
|
|
353
|
+
if (!parsed.success) return null;
|
|
354
|
+
return Object.freeze({
|
|
355
|
+
documentId: brandDocumentId(parsed.data.source_doc_id),
|
|
356
|
+
requestId: brandRequestId(parsed.data.request_id),
|
|
357
|
+
status: "received"
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
var identitySchema = z.object({ label: z.string(), value: z.string() });
|
|
361
|
+
var entitySchema = z.object({
|
|
362
|
+
key: z.string(),
|
|
363
|
+
category: z.string().optional(),
|
|
364
|
+
label: z.string().optional(),
|
|
365
|
+
value: z.string().nullish(),
|
|
366
|
+
unit: z.string().nullish(),
|
|
367
|
+
observed_at: z.string().nullish(),
|
|
368
|
+
status: z.string().nullish(),
|
|
369
|
+
origin: z.string().nullish()
|
|
370
|
+
});
|
|
371
|
+
var seriesPointSchema = z.object({
|
|
372
|
+
at: z.string(),
|
|
373
|
+
value: z.number().nullish(),
|
|
374
|
+
display: z.string().nullish(),
|
|
375
|
+
origin: z.string().nullish()
|
|
376
|
+
});
|
|
377
|
+
var seriesSchema = z.object({
|
|
378
|
+
key: z.string(),
|
|
379
|
+
label: z.string().optional(),
|
|
380
|
+
points: z.array(seriesPointSchema).optional()
|
|
381
|
+
});
|
|
382
|
+
var forwardSchema = z.object({
|
|
383
|
+
title: z.string(),
|
|
384
|
+
detail: z.string().nullish(),
|
|
385
|
+
due_at: z.string().nullish()
|
|
386
|
+
});
|
|
387
|
+
var documentSchema = z.object({
|
|
388
|
+
doc_id: z.string(),
|
|
389
|
+
title: z.string().nullish(),
|
|
390
|
+
page_count: z.number().nullish(),
|
|
391
|
+
received_at: z.string().nullish()
|
|
392
|
+
});
|
|
393
|
+
var contentSchema = z.object({
|
|
394
|
+
identity: z.array(identitySchema).optional(),
|
|
395
|
+
entities: z.array(entitySchema).optional(),
|
|
396
|
+
entity_series: z.array(seriesSchema).optional(),
|
|
397
|
+
forward_looking: z.array(forwardSchema).optional(),
|
|
398
|
+
documents: z.array(documentSchema).optional()
|
|
399
|
+
});
|
|
400
|
+
function mapRecordView(petRef, wire) {
|
|
401
|
+
const w = isObject(wire) ? wire : {};
|
|
402
|
+
const schemaVersion = strOr(w["schema_version"], "epdf/1.0");
|
|
403
|
+
const disclaimer = strOr(w["disclaimer"], "");
|
|
404
|
+
const pet = brandPetRef(typeof w["pet_ref"] === "string" && w["pet_ref"].length > 0 ? w["pet_ref"] : petRef);
|
|
405
|
+
if (w["withheld"] === true) {
|
|
406
|
+
return withheldView(pet, schemaVersion, disclaimer);
|
|
407
|
+
}
|
|
408
|
+
const content = contentSchema.safeParse(w);
|
|
409
|
+
if (!content.success) {
|
|
410
|
+
return withheldView(pet, schemaVersion, disclaimer);
|
|
411
|
+
}
|
|
412
|
+
const c = content.data;
|
|
413
|
+
const identity = (c.identity ?? []).map(
|
|
414
|
+
(i) => Object.freeze({ label: i.label, value: i.value })
|
|
415
|
+
);
|
|
416
|
+
const entities = (c.entities ?? []).map(
|
|
417
|
+
(e) => Object.freeze({
|
|
418
|
+
key: e.key,
|
|
419
|
+
category: e.category ?? "",
|
|
420
|
+
label: e.label ?? e.key,
|
|
421
|
+
value: e.value ?? null,
|
|
422
|
+
unit: e.unit ?? null,
|
|
423
|
+
observedAt: e.observed_at ?? null,
|
|
424
|
+
status: e.status ?? null,
|
|
425
|
+
origin: normalizeOrigin(e.origin)
|
|
426
|
+
})
|
|
427
|
+
);
|
|
428
|
+
const entitySeries = (c.entity_series ?? []).map(
|
|
429
|
+
(s) => Object.freeze({
|
|
430
|
+
key: s.key,
|
|
431
|
+
label: s.label ?? s.key,
|
|
432
|
+
points: Object.freeze(
|
|
433
|
+
(s.points ?? []).map(
|
|
434
|
+
(p) => Object.freeze({
|
|
435
|
+
at: p.at,
|
|
436
|
+
value: p.value ?? null,
|
|
437
|
+
display: p.display ?? null,
|
|
438
|
+
origin: normalizeOrigin(p.origin)
|
|
439
|
+
})
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
const forwardLooking = (c.forward_looking ?? []).map(
|
|
445
|
+
(f) => Object.freeze({ title: f.title, detail: f.detail ?? null, dueAt: f.due_at ?? null })
|
|
446
|
+
);
|
|
447
|
+
const documents = {};
|
|
448
|
+
for (const d of c.documents ?? []) {
|
|
449
|
+
documents[d.doc_id] = Object.freeze({
|
|
450
|
+
docId: d.doc_id,
|
|
451
|
+
title: d.title ?? null,
|
|
452
|
+
pageCount: d.page_count ?? null,
|
|
453
|
+
receivedAt: d.received_at ?? null
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
const isEmpty = w["is_empty"] === true || identity.length === 0 && entities.length === 0 && entitySeries.length === 0 && forwardLooking.length === 0 && Object.keys(documents).length === 0;
|
|
457
|
+
return Object.freeze({
|
|
458
|
+
petRef: pet,
|
|
459
|
+
schemaVersion,
|
|
460
|
+
isEmpty,
|
|
461
|
+
withheld: false,
|
|
462
|
+
generatedAt: strOrNull(w["generated_at"]),
|
|
463
|
+
identity: Object.freeze(identity),
|
|
464
|
+
entities: Object.freeze(entities),
|
|
465
|
+
entitySeries: Object.freeze(entitySeries),
|
|
466
|
+
forwardLooking: Object.freeze(forwardLooking),
|
|
467
|
+
documents: Object.freeze(documents),
|
|
468
|
+
disclaimer
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
function withheldView(petRef, schemaVersion, disclaimer) {
|
|
472
|
+
return Object.freeze({
|
|
473
|
+
petRef,
|
|
474
|
+
schemaVersion,
|
|
475
|
+
isEmpty: false,
|
|
476
|
+
withheld: true,
|
|
477
|
+
generatedAt: null,
|
|
478
|
+
identity: Object.freeze([]),
|
|
479
|
+
entities: Object.freeze([]),
|
|
480
|
+
entitySeries: Object.freeze([]),
|
|
481
|
+
forwardLooking: Object.freeze([]),
|
|
482
|
+
documents: Object.freeze({}),
|
|
483
|
+
disclaimer
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
var consentSchema = z.object({
|
|
487
|
+
pet_ref: z.string().optional(),
|
|
488
|
+
granted: z.boolean().optional(),
|
|
489
|
+
version: z.string().nullish(),
|
|
490
|
+
granted_at: z.string().nullish()
|
|
491
|
+
});
|
|
492
|
+
function mapConsent(petRef, wire) {
|
|
493
|
+
const parsed = consentSchema.safeParse(wire);
|
|
494
|
+
const data = parsed.success ? parsed.data : {};
|
|
495
|
+
return Object.freeze({
|
|
496
|
+
petRef: brandPetRef(typeof data.pet_ref === "string" && data.pet_ref.length > 0 ? data.pet_ref : petRef),
|
|
497
|
+
granted: data.granted === true,
|
|
498
|
+
version: data.version ?? null,
|
|
499
|
+
grantedAt: data.granted_at ?? null
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
function isObject(v) {
|
|
503
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
504
|
+
}
|
|
505
|
+
function numberOr(v, fallback) {
|
|
506
|
+
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
507
|
+
}
|
|
508
|
+
function strOr(v, fallback) {
|
|
509
|
+
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
510
|
+
}
|
|
511
|
+
function strOrNull(v) {
|
|
512
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/core/records/models.ts
|
|
516
|
+
var RECORDS_TERMINAL = Object.freeze([
|
|
517
|
+
"published",
|
|
518
|
+
"failed",
|
|
519
|
+
"revoked"
|
|
520
|
+
]);
|
|
521
|
+
function isTerminalStatus(status) {
|
|
522
|
+
return RECORDS_TERMINAL.includes(status);
|
|
523
|
+
}
|
|
524
|
+
function recordsDenialReasonOf(error) {
|
|
525
|
+
if (error === null) return null;
|
|
526
|
+
const reason = error.recordsReason;
|
|
527
|
+
return reason === "consent_required" || reason === "session_required" || reason === "entitlement" ? reason : null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/core/records/ports.ts
|
|
531
|
+
var noopRecordsTelemetry = Object.freeze({
|
|
532
|
+
record() {
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
536
|
+
var CONSENT_VERSION_MAX = 64;
|
|
537
|
+
var IDEMPOTENCY_KEY_MAX = 128;
|
|
538
|
+
var PET_REF_MAX = 128;
|
|
539
|
+
var RECORDS_UPLOAD_CONTENT_TYPE = "application/pdf";
|
|
540
|
+
var UPLOAD_DEADLINE_MS = 9e4;
|
|
541
|
+
|
|
542
|
+
// src/core/records/RecordsController.ts
|
|
543
|
+
function createRecordsController(deps) {
|
|
544
|
+
return new RecordsControllerImpl(deps);
|
|
545
|
+
}
|
|
546
|
+
var RecordsControllerImpl = class {
|
|
547
|
+
constructor(deps) {
|
|
548
|
+
/** In-memory record cache (per pet). NEVER persisted to disk. Cleared on revoke. */
|
|
549
|
+
this.recordCache = /* @__PURE__ */ new Map();
|
|
550
|
+
/** Parse-gap dedup: a request's gap metric fires at most once per id. */
|
|
551
|
+
this.reportedGaps = /* @__PURE__ */ new Set();
|
|
552
|
+
this.repository = deps.repository;
|
|
553
|
+
this.uploads = deps.uploads;
|
|
554
|
+
this.telemetry = deps.telemetry ?? noopRecordsTelemetry;
|
|
555
|
+
this.newIdempotencyKey = deps.newIdempotencyKey ?? defaultIdempotencyFactory();
|
|
556
|
+
this.timers = deps.timers ?? defaultTimers;
|
|
557
|
+
this.uploadDeadlineMs = deps.uploadDeadlineMs ?? UPLOAD_DEADLINE_MS;
|
|
558
|
+
}
|
|
559
|
+
// ---- request lifecycle ------------------------------------------------
|
|
560
|
+
async createUploadRequest(input) {
|
|
561
|
+
const res = await this.repository.createRequest({
|
|
562
|
+
pet_ref: input.petRef,
|
|
563
|
+
idempotency_key: input.idempotencyKey
|
|
564
|
+
});
|
|
565
|
+
if (!res.ok) return this.onRequestError(res.error);
|
|
566
|
+
const view = mapRequestView(res.value);
|
|
567
|
+
if (view === null) return err(internal());
|
|
568
|
+
this.observeRequest(view);
|
|
569
|
+
return ok(view);
|
|
570
|
+
}
|
|
571
|
+
async getRequest(id) {
|
|
572
|
+
const res = await this.repository.getRequest(id);
|
|
573
|
+
if (!res.ok) return this.onRequestError(res.error);
|
|
574
|
+
const view = mapRequestView(res.value);
|
|
575
|
+
if (view === null) return err(internal());
|
|
576
|
+
this.observeRequest(view);
|
|
577
|
+
return ok(view);
|
|
578
|
+
}
|
|
579
|
+
async listRequests() {
|
|
580
|
+
const res = await this.repository.listRequests();
|
|
581
|
+
if (!res.ok) return this.onRequestError(res.error);
|
|
582
|
+
const views = mapRequestList(res.value);
|
|
583
|
+
for (const view of views) this.observeRequest(view);
|
|
584
|
+
return ok(views);
|
|
585
|
+
}
|
|
586
|
+
// ---- two-phase upload (§3.2) ------------------------------------------
|
|
587
|
+
async uploadDocument(input) {
|
|
588
|
+
const { requestId, file } = input;
|
|
589
|
+
const rejection = validateFile(file);
|
|
590
|
+
if (rejection !== null) {
|
|
591
|
+
this.telemetry.record("records.upload.rejected", { reason: rejection });
|
|
592
|
+
return err(uploadFailed(`upload rejected: ${rejection}`));
|
|
593
|
+
}
|
|
594
|
+
const initiated = await this.repository.initiateUpload(requestId, {
|
|
595
|
+
content_type: RECORDS_UPLOAD_CONTENT_TYPE,
|
|
596
|
+
file_size: file.sizeBytes
|
|
597
|
+
});
|
|
598
|
+
if (!initiated.ok) {
|
|
599
|
+
return this.onRequestError(initiated.error);
|
|
600
|
+
}
|
|
601
|
+
const s3 = await this.postToS3(initiated.value, file, input);
|
|
602
|
+
if (!s3.ok) {
|
|
603
|
+
this.telemetry.record("records.upload.failed", { reason: s3.reason });
|
|
604
|
+
return err(uploadFailed(s3.reason));
|
|
605
|
+
}
|
|
606
|
+
const completed = await this.repository.completeUpload(requestId, {
|
|
607
|
+
upload_id: initiated.value.upload_id
|
|
608
|
+
});
|
|
609
|
+
if (!completed.ok) {
|
|
610
|
+
if (completed.error.code === "mediaUploadFailed") {
|
|
611
|
+
this.telemetry.record("records.upload.rejected", {
|
|
612
|
+
reason: reasonFromWire(completed.error) ?? "corrupt"
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
return this.onRequestError(completed.error);
|
|
616
|
+
}
|
|
617
|
+
const doc = mapUploadedDocument(completed.value);
|
|
618
|
+
if (doc === null) return err(internal());
|
|
619
|
+
return ok(doc);
|
|
620
|
+
}
|
|
621
|
+
async postToS3(policy, file, input) {
|
|
622
|
+
const deadline = new AbortController();
|
|
623
|
+
const signal = mergeSignals(deadline.signal, input.signal);
|
|
624
|
+
const handle = this.timers.setTimeout(() => deadline.abort(), this.uploadDeadlineMs);
|
|
625
|
+
try {
|
|
626
|
+
const opts = { signal };
|
|
627
|
+
if (input.onProgress) opts.onProgress = input.onProgress;
|
|
628
|
+
await this.uploads.postMultipart(policy, file, opts);
|
|
629
|
+
return { ok: true };
|
|
630
|
+
} catch {
|
|
631
|
+
const reason = deadline.signal.aborted && input.signal?.aborted !== true ? "timeout" : "upload_error";
|
|
632
|
+
return { ok: false, reason };
|
|
633
|
+
} finally {
|
|
634
|
+
this.timers.clearTimeout(handle);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// ---- record read (consent-gated, redaction-fail-closed) ---------------
|
|
638
|
+
async getRecord(pet) {
|
|
639
|
+
const res = await this.repository.getRecord(pet);
|
|
640
|
+
if (!res.ok) {
|
|
641
|
+
const reason = recordsDenialReasonOf(res.error);
|
|
642
|
+
if (reason === "consent_required") {
|
|
643
|
+
this.telemetry.record("records.consent.denied", { pet_ref: pet });
|
|
644
|
+
} else if (reason === "entitlement") {
|
|
645
|
+
this.telemetry.record("records.capability.denied", { pet_ref: pet });
|
|
646
|
+
}
|
|
647
|
+
return err(res.error);
|
|
648
|
+
}
|
|
649
|
+
const view = mapRecordView(pet, res.value);
|
|
650
|
+
if (view.withheld) {
|
|
651
|
+
this.telemetry.record("records.redaction.withheld", { pet_ref: pet });
|
|
652
|
+
this.recordCache.delete(pet);
|
|
653
|
+
return ok(view);
|
|
654
|
+
}
|
|
655
|
+
this.recordCache.set(pet, view);
|
|
656
|
+
return ok(view);
|
|
657
|
+
}
|
|
658
|
+
peekCachedRecord(pet) {
|
|
659
|
+
return this.recordCache.get(pet) ?? null;
|
|
660
|
+
}
|
|
661
|
+
// ---- consent ----------------------------------------------------------
|
|
662
|
+
async getConsent(pet) {
|
|
663
|
+
const res = await this.repository.getConsent(pet);
|
|
664
|
+
if (!res.ok) {
|
|
665
|
+
return err(res.error);
|
|
666
|
+
}
|
|
667
|
+
return ok(mapConsent(pet, res.value));
|
|
668
|
+
}
|
|
669
|
+
async grantConsent(input) {
|
|
670
|
+
const res = await this.repository.putConsent(
|
|
671
|
+
input.petRef,
|
|
672
|
+
{ consent_version: input.consentVersion, idempotency_key: input.idempotencyKey }
|
|
673
|
+
);
|
|
674
|
+
if (!res.ok) {
|
|
675
|
+
const reason = recordsDenialReasonOf(res.error);
|
|
676
|
+
if (reason === "entitlement") this.telemetry.record("records.capability.denied", { pet_ref: input.petRef });
|
|
677
|
+
return err(res.error);
|
|
678
|
+
}
|
|
679
|
+
return ok(mapConsent(input.petRef, res.value));
|
|
680
|
+
}
|
|
681
|
+
async revokeConsent(pet) {
|
|
682
|
+
const res = await this.repository.deleteConsent(pet);
|
|
683
|
+
if (!res.ok) return err(res.error);
|
|
684
|
+
this.recordCache.delete(pet);
|
|
685
|
+
return ok(mapConsent(pet, res.value));
|
|
686
|
+
}
|
|
687
|
+
// ---- telemetry ---------------------------------------------------------
|
|
688
|
+
reportCapabilityDenied(pet) {
|
|
689
|
+
this.telemetry.record("records.capability.denied", pet ? { pet_ref: pet } : {});
|
|
690
|
+
}
|
|
691
|
+
// ---- internals ---------------------------------------------------------
|
|
692
|
+
/** Emit `records.parse.gap` at most once when a request first shows a manual-review gap (R6). */
|
|
693
|
+
observeRequest(view) {
|
|
694
|
+
if (view.reviewStartedAt !== null && !this.reportedGaps.has(view.id)) {
|
|
695
|
+
this.reportedGaps.add(view.id);
|
|
696
|
+
this.telemetry.record("records.parse.gap", { request_id: view.id });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/** Shared error path for the request lane: emit capability-denied on an entitlement 403, then surface. */
|
|
700
|
+
onRequestError(error) {
|
|
701
|
+
const reason = recordsDenialReasonOf(error);
|
|
702
|
+
if (reason === "entitlement") this.telemetry.record("records.capability.denied", {});
|
|
703
|
+
return err(error);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
function validateFile(file) {
|
|
707
|
+
if (file.mimeType !== RECORDS_UPLOAD_CONTENT_TYPE) return "unsupported_format";
|
|
708
|
+
const size = file.sizeBytes;
|
|
709
|
+
if (typeof size !== "number" || !Number.isFinite(size) || size <= 0 || size > MAX_UPLOAD_BYTES) {
|
|
710
|
+
return "oversize";
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
function reasonFromWire(error) {
|
|
715
|
+
const raw = error.wireMessage;
|
|
716
|
+
if (raw === "unsupported_format" || raw === "oversize" || raw === "corrupt") return raw;
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
function uploadFailed(wireMessage) {
|
|
720
|
+
return makeEverfurError("mediaUploadFailed", { retryable: false, wireMessage });
|
|
721
|
+
}
|
|
722
|
+
function internal() {
|
|
723
|
+
return makeEverfurError("internalError", { retryable: true });
|
|
724
|
+
}
|
|
725
|
+
var defaultTimers = {
|
|
726
|
+
setTimeout: (handler, ms) => setTimeout(handler, ms),
|
|
727
|
+
clearTimeout: (handle) => clearTimeout(handle)
|
|
728
|
+
};
|
|
729
|
+
function mergeSignals(a, b) {
|
|
730
|
+
if (!b) return a;
|
|
731
|
+
const ctrl = new AbortController();
|
|
732
|
+
if (a.aborted || b.aborted) {
|
|
733
|
+
ctrl.abort();
|
|
734
|
+
return ctrl.signal;
|
|
735
|
+
}
|
|
736
|
+
const onAbort = () => ctrl.abort();
|
|
737
|
+
a.addEventListener("abort", onAbort, { once: true });
|
|
738
|
+
b.addEventListener("abort", onAbort, { once: true });
|
|
739
|
+
return ctrl.signal;
|
|
740
|
+
}
|
|
741
|
+
function defaultIdempotencyFactory() {
|
|
742
|
+
let counter = 0;
|
|
743
|
+
return () => `records-${Date.now().toString(36)}-${(counter++).toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/client/protocol/frames.ts
|
|
747
|
+
function assertNever(x) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
`assertNever: unhandled discriminated-union member ${JSON.stringify(x)}`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
function errorFrame(fields) {
|
|
753
|
+
return Object.freeze({
|
|
754
|
+
kind: "error",
|
|
755
|
+
terminal: true,
|
|
756
|
+
code: fields.code,
|
|
757
|
+
retryable: fields.retryable,
|
|
758
|
+
wireMessage: fields.wireMessage,
|
|
759
|
+
synthesized: fields.synthesized,
|
|
760
|
+
lastEventId: fields.lastEventId,
|
|
761
|
+
serverRequestId: null
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/client/identity.ts
|
|
766
|
+
function authHeaders(auth) {
|
|
767
|
+
const headers = {};
|
|
768
|
+
if (auth.mode === "personalized") {
|
|
769
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
770
|
+
} else {
|
|
771
|
+
headers["x-everfur-partner-key"] = auth.publishableKey;
|
|
772
|
+
}
|
|
773
|
+
if (auth.userRef) headers["x-everfur-user-ref"] = auth.userRef;
|
|
774
|
+
if (auth.sessionId) headers["x-everfur-session-id"] = auth.sessionId;
|
|
775
|
+
return headers;
|
|
776
|
+
}
|
|
777
|
+
function withAuthHeaders(req, auth) {
|
|
778
|
+
return { ...req, headers: { ...stripAuthHeaders(req.headers), ...authHeaders(auth) } };
|
|
779
|
+
}
|
|
780
|
+
function stripAuthHeaders(headers) {
|
|
781
|
+
if (!headers) return {};
|
|
782
|
+
const out = {};
|
|
783
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
784
|
+
const lower = key.toLowerCase();
|
|
785
|
+
if (lower === "authorization" || lower === "x-everfur-partner-key") continue;
|
|
786
|
+
out[key] = value;
|
|
787
|
+
}
|
|
788
|
+
return out;
|
|
789
|
+
}
|
|
790
|
+
async function authedSend(req, ctx) {
|
|
791
|
+
if (ctx.mode === "anonymous") {
|
|
792
|
+
return ctx.transport.request(
|
|
793
|
+
withAuthHeaders(req, {
|
|
794
|
+
mode: "anonymous",
|
|
795
|
+
publishableKey: ctx.publishableKey,
|
|
796
|
+
userRef: ctx.userRef,
|
|
797
|
+
...ctx.sessionId ? { sessionId: ctx.sessionId } : {}
|
|
798
|
+
})
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
let token;
|
|
802
|
+
try {
|
|
803
|
+
token = await ctx.identity.currentToken();
|
|
804
|
+
} catch {
|
|
805
|
+
return err(authRejectedError());
|
|
806
|
+
}
|
|
807
|
+
const first = await ctx.transport.request(
|
|
808
|
+
withAuthHeaders(req, {
|
|
809
|
+
mode: "personalized",
|
|
810
|
+
token,
|
|
811
|
+
userRef: ctx.userRef,
|
|
812
|
+
...ctx.sessionId ? { sessionId: ctx.sessionId } : {}
|
|
813
|
+
})
|
|
814
|
+
);
|
|
815
|
+
if (first.ok || first.error.httpStatus !== 401) return first;
|
|
816
|
+
let refreshed;
|
|
817
|
+
try {
|
|
818
|
+
refreshed = await ctx.identity.currentToken({ forceRefresh: true });
|
|
819
|
+
} catch {
|
|
820
|
+
return err(authRejectedError());
|
|
821
|
+
}
|
|
822
|
+
const retry = await ctx.transport.request(
|
|
823
|
+
withAuthHeaders(req, {
|
|
824
|
+
mode: "personalized",
|
|
825
|
+
token: refreshed,
|
|
826
|
+
userRef: ctx.userRef,
|
|
827
|
+
...ctx.sessionId ? { sessionId: ctx.sessionId } : {}
|
|
828
|
+
})
|
|
829
|
+
);
|
|
830
|
+
if (!retry.ok && retry.error.httpStatus === 401) {
|
|
831
|
+
return err(authRejectedError(retry.error.httpStatus));
|
|
832
|
+
}
|
|
833
|
+
return retry;
|
|
834
|
+
}
|
|
835
|
+
function authRejectedError(httpStatus) {
|
|
836
|
+
return makeEverfurError("authRejected", {
|
|
837
|
+
retryable: false,
|
|
838
|
+
...httpStatus !== void 0 ? { httpStatus } : {}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/client/transport.ts
|
|
843
|
+
function networkError(cause) {
|
|
844
|
+
return makeEverfurError("internalError", { retryable: true, wireMessage: describeCause(cause) });
|
|
845
|
+
}
|
|
846
|
+
function describeCause(cause) {
|
|
847
|
+
if (cause instanceof Error) return cause.message;
|
|
848
|
+
return typeof cause === "string" ? cause : "network request failed";
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/client/funnel/requestFunnel.ts
|
|
852
|
+
var FUNNEL_UNARY_TIMEOUT_MS = 3e4;
|
|
853
|
+
var RETRY_OVERRIDE_MAX_MS = BACKOFF_CAP_MS * 2;
|
|
854
|
+
var IDEMPOTENCY_HEADER = "idempotency-key";
|
|
855
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
856
|
+
var NOOP_OBSERVER = Object.freeze({
|
|
857
|
+
onEvent() {
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
var defaultFunnelTimers = Object.freeze({
|
|
861
|
+
setTimeout: (handler, ms) => setTimeout(handler, ms),
|
|
862
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
863
|
+
now: () => Date.now()
|
|
864
|
+
});
|
|
865
|
+
function createRequestFunnel(deps) {
|
|
866
|
+
const observer = deps.observer ?? NOOP_OBSERVER;
|
|
867
|
+
const timers = deps.timers ?? defaultFunnelTimers;
|
|
868
|
+
const rng = deps.rng ?? Math.random;
|
|
869
|
+
const sleep = deps.sleep ?? ((ms, signal) => delayAbortable(ms, timers, signal));
|
|
870
|
+
const ceiling = deps.maxAttempts ?? MAX_RETRY_ATTEMPTS;
|
|
871
|
+
const timeoutMs = deps.unaryTimeoutMs ?? FUNNEL_UNARY_TIMEOUT_MS;
|
|
872
|
+
const mintKey = deps.newIdempotencyKey ?? defaultIdempotencyKeyFactory();
|
|
873
|
+
const emit = (event) => {
|
|
874
|
+
try {
|
|
875
|
+
observer.onEvent(event);
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
async function send(call, req, opts) {
|
|
880
|
+
const perCallMaxAttempts = opts?.maxAttempts;
|
|
881
|
+
const maxAttempts = Math.max(1, perCallMaxAttempts ?? ceiling);
|
|
882
|
+
const idempotent = opts?.idempotent ?? MUTATING_METHODS.has(req.method);
|
|
883
|
+
const idempotencyKey = idempotent ? mintKey() : null;
|
|
884
|
+
const base = assembleHeaders(req, idempotencyKey);
|
|
885
|
+
let attempt = 0;
|
|
886
|
+
for (; ; ) {
|
|
887
|
+
const stamp = (extra) => Object.freeze({
|
|
888
|
+
call,
|
|
889
|
+
method: req.method,
|
|
890
|
+
path: req.path,
|
|
891
|
+
attempt,
|
|
892
|
+
...idempotencyKey ? { idempotencyKey } : {},
|
|
893
|
+
...extra
|
|
894
|
+
});
|
|
895
|
+
emit(stamp({ phase: "attempt" }));
|
|
896
|
+
const deadline = boundedSignal(timeoutMs, timers);
|
|
897
|
+
const merged = mergeSignals2(deadline.signal, req.signal);
|
|
898
|
+
let settled;
|
|
899
|
+
try {
|
|
900
|
+
settled = await authedSend({ ...base, signal: merged.signal }, deps.auth);
|
|
901
|
+
} catch (cause) {
|
|
902
|
+
settled = err(networkError(cause));
|
|
903
|
+
} finally {
|
|
904
|
+
merged.dispose();
|
|
905
|
+
deadline.cancel();
|
|
906
|
+
}
|
|
907
|
+
if (settled.ok) {
|
|
908
|
+
emit(stamp({ phase: "settled", ok: true, reason: "ok" }));
|
|
909
|
+
return settled;
|
|
910
|
+
}
|
|
911
|
+
const error = settled.error;
|
|
912
|
+
const decision = decideRetry(error, attempt, {
|
|
913
|
+
maxAttempts,
|
|
914
|
+
rng,
|
|
915
|
+
now: timers.now(),
|
|
916
|
+
aborted: req.signal?.aborted === true,
|
|
917
|
+
...perCallMaxAttempts !== void 0 ? { perCallMaxAttempts } : {},
|
|
918
|
+
...deps.errorPolicy !== void 0 ? { policy: deps.errorPolicy } : {}
|
|
919
|
+
});
|
|
920
|
+
const settledEvent = {
|
|
921
|
+
ok: false,
|
|
922
|
+
code: error.code,
|
|
923
|
+
reason: decision.reason,
|
|
924
|
+
...error.httpStatus !== void 0 ? { httpStatus: error.httpStatus } : {},
|
|
925
|
+
...error.requestId !== void 0 ? { requestId: error.requestId } : {}
|
|
926
|
+
};
|
|
927
|
+
if (!decision.retry) {
|
|
928
|
+
emit(stamp({ phase: "settled", ...settledEvent }));
|
|
929
|
+
return err(error);
|
|
930
|
+
}
|
|
931
|
+
emit(stamp({ phase: "retry", ...settledEvent, delayMs: decision.delayMs }));
|
|
932
|
+
await sleep(decision.delayMs, req.signal);
|
|
933
|
+
if (req.signal?.aborted === true) {
|
|
934
|
+
emit(stamp({ phase: "settled", ...settledEvent, reason: "aborted" }));
|
|
935
|
+
return err(error);
|
|
936
|
+
}
|
|
937
|
+
attempt += 1;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async function authorize(req) {
|
|
941
|
+
return withAuthHeaders(req, await resolveDeviceAuth(deps.auth));
|
|
942
|
+
}
|
|
943
|
+
async function* stream(call, req, signal) {
|
|
944
|
+
emit(
|
|
945
|
+
Object.freeze({
|
|
946
|
+
call,
|
|
947
|
+
phase: "stream.open",
|
|
948
|
+
method: req.method,
|
|
949
|
+
path: req.path,
|
|
950
|
+
attempt: 0
|
|
951
|
+
})
|
|
952
|
+
);
|
|
953
|
+
try {
|
|
954
|
+
yield* deps.auth.transport.stream(req, signal);
|
|
955
|
+
} catch {
|
|
956
|
+
yield errorFrame({
|
|
957
|
+
// The same wire spelling sse.ts synthesizes, so `fromWire` folds it to the retryable `internalError`
|
|
958
|
+
// like any other transport failure. wireMessage stays null: it is the SERVER's text, and a thrown
|
|
959
|
+
// host exception's `.message` is not that.
|
|
960
|
+
code: "internal_error",
|
|
961
|
+
retryable: true,
|
|
962
|
+
wireMessage: null,
|
|
963
|
+
synthesized: true,
|
|
964
|
+
lastEventId: null});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return Object.freeze({ send, authorize, stream });
|
|
968
|
+
}
|
|
969
|
+
function assembleHeaders(req, idempotencyKey) {
|
|
970
|
+
const headers = {};
|
|
971
|
+
for (const [key, value] of Object.entries(req.headers ?? {})) {
|
|
972
|
+
if (key.toLowerCase() === IDEMPOTENCY_HEADER) continue;
|
|
973
|
+
headers[key] = value;
|
|
974
|
+
}
|
|
975
|
+
if (idempotencyKey !== null) headers["Idempotency-Key"] = idempotencyKey;
|
|
976
|
+
return { ...req, headers };
|
|
977
|
+
}
|
|
978
|
+
function defaultIdempotencyKeyFactory() {
|
|
979
|
+
let counter = 0;
|
|
980
|
+
return () => `efk_${Date.now().toString(36)}_${(counter++).toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
981
|
+
}
|
|
982
|
+
var POLICY_MAX_ATTEMPTS_CAP = 10;
|
|
983
|
+
function decideRetry(error, attempt, ctx) {
|
|
984
|
+
if (ctx.aborted) return { retry: false, reason: "aborted" };
|
|
985
|
+
const throttle = throttleKind(error);
|
|
986
|
+
if (throttle === "quota") {
|
|
987
|
+
return { retry: false, reason: "daily-quota-exhausted" };
|
|
988
|
+
}
|
|
989
|
+
if (throttle !== "burst" && !isAutoRetryable(error.code)) {
|
|
990
|
+
return { retry: false, reason: "terminal-code" };
|
|
991
|
+
}
|
|
992
|
+
const reason = throttle === "burst" ? "burst-throttle" : "retryable-code";
|
|
993
|
+
const policyVetoedRetry = () => {
|
|
994
|
+
if (ctx.policy === void 0) return false;
|
|
995
|
+
try {
|
|
996
|
+
return !ctx.policy.shouldRetry(error.code, attempt);
|
|
997
|
+
} catch {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
if (policyVetoedRetry()) return { retry: false, reason: "terminal-code" };
|
|
1002
|
+
const policyCeiling = () => {
|
|
1003
|
+
if (ctx.policy === void 0) return null;
|
|
1004
|
+
let raw;
|
|
1005
|
+
try {
|
|
1006
|
+
raw = ctx.policy.maxAttempts(error.code);
|
|
1007
|
+
} catch {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
|
|
1011
|
+
return Math.max(1, Math.min(POLICY_MAX_ATTEMPTS_CAP, Math.floor(raw)));
|
|
1012
|
+
};
|
|
1013
|
+
const ceiling = ctx.perCallMaxAttempts ?? policyCeiling() ?? ctx.maxAttempts;
|
|
1014
|
+
if (attempt + 1 >= ceiling) return { retry: false, reason: "attempts-exhausted" };
|
|
1015
|
+
const override = retryOverrideMs(error, ctx.now);
|
|
1016
|
+
if (override !== null) {
|
|
1017
|
+
if (override > RETRY_OVERRIDE_MAX_MS) {
|
|
1018
|
+
return { retry: false, reason: "server-reset-beyond-budget" };
|
|
1019
|
+
}
|
|
1020
|
+
return { retry: true, reason, delayMs: override };
|
|
1021
|
+
}
|
|
1022
|
+
return { retry: true, reason, delayMs: backoffDelayMs(attempt, ctx.rng) };
|
|
1023
|
+
}
|
|
1024
|
+
function throttleKind(error) {
|
|
1025
|
+
const code = error.code === "unknown" ? fromWire(rawWireCode(error.raw)) : error.code;
|
|
1026
|
+
if (code === "quotaExceeded") return "quota";
|
|
1027
|
+
if (code === "rateLimited") return "burst";
|
|
1028
|
+
if (error.httpStatus === 429) {
|
|
1029
|
+
return "burst";
|
|
1030
|
+
}
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
function retryOverrideMs(error, now) {
|
|
1034
|
+
const at = findResetsAt(error.raw);
|
|
1035
|
+
if (at === null) return null;
|
|
1036
|
+
const delta = at - now;
|
|
1037
|
+
return delta > 0 ? delta : 0;
|
|
1038
|
+
}
|
|
1039
|
+
function findResetsAt(raw) {
|
|
1040
|
+
for (const scope of candidateScopes(raw)) {
|
|
1041
|
+
const value = scope["resets_at"];
|
|
1042
|
+
const parsed = parseInstantMs(value);
|
|
1043
|
+
if (parsed !== null) return parsed;
|
|
1044
|
+
}
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
function candidateScopes(raw) {
|
|
1048
|
+
if (!isRecord(raw)) return [];
|
|
1049
|
+
const out = [raw];
|
|
1050
|
+
for (const nested of ["error", "detail"]) {
|
|
1051
|
+
const value = raw[nested];
|
|
1052
|
+
if (isRecord(value)) out.push(value);
|
|
1053
|
+
}
|
|
1054
|
+
return out;
|
|
1055
|
+
}
|
|
1056
|
+
function parseInstantMs(value) {
|
|
1057
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1058
|
+
return value < 1e11 ? Math.round(value * 1e3) : Math.round(value);
|
|
1059
|
+
}
|
|
1060
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1061
|
+
const parsed = Date.parse(value);
|
|
1062
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1063
|
+
}
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
function rawWireCode(raw) {
|
|
1067
|
+
for (const scope of candidateScopes(raw)) {
|
|
1068
|
+
const code = scope["code"];
|
|
1069
|
+
if (typeof code === "string" && code.length > 0) return code;
|
|
1070
|
+
}
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
function isRecord(value) {
|
|
1074
|
+
return typeof value === "object" && value !== null;
|
|
1075
|
+
}
|
|
1076
|
+
async function resolveDeviceAuth(auth) {
|
|
1077
|
+
if (auth.mode === "anonymous") {
|
|
1078
|
+
return {
|
|
1079
|
+
mode: "anonymous",
|
|
1080
|
+
publishableKey: auth.publishableKey,
|
|
1081
|
+
userRef: auth.userRef,
|
|
1082
|
+
...auth.sessionId ? { sessionId: auth.sessionId } : {}
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
mode: "personalized",
|
|
1087
|
+
token: await auth.identity.currentToken(),
|
|
1088
|
+
userRef: auth.userRef,
|
|
1089
|
+
...auth.sessionId ? { sessionId: auth.sessionId } : {}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function boundedSignal(ms, timers) {
|
|
1093
|
+
const ctrl = new AbortController();
|
|
1094
|
+
const handle = timers.setTimeout(() => ctrl.abort(), ms);
|
|
1095
|
+
return { signal: ctrl.signal, cancel: () => timers.clearTimeout(handle) };
|
|
1096
|
+
}
|
|
1097
|
+
function mergeSignals2(deadline, caller) {
|
|
1098
|
+
const noop = () => void 0;
|
|
1099
|
+
if (!caller) return { signal: deadline, dispose: noop };
|
|
1100
|
+
const ctrl = new AbortController();
|
|
1101
|
+
if (deadline.aborted || caller.aborted) {
|
|
1102
|
+
ctrl.abort();
|
|
1103
|
+
return { signal: ctrl.signal, dispose: noop };
|
|
1104
|
+
}
|
|
1105
|
+
const onAbort = () => ctrl.abort();
|
|
1106
|
+
deadline.addEventListener("abort", onAbort, { once: true });
|
|
1107
|
+
caller.addEventListener("abort", onAbort, { once: true });
|
|
1108
|
+
return {
|
|
1109
|
+
signal: ctrl.signal,
|
|
1110
|
+
dispose: () => {
|
|
1111
|
+
deadline.removeEventListener("abort", onAbort);
|
|
1112
|
+
caller.removeEventListener("abort", onAbort);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function delayAbortable(ms, timers, signal) {
|
|
1117
|
+
if (ms <= 0 || signal?.aborted === true) return Promise.resolve();
|
|
1118
|
+
return new Promise((resolve) => {
|
|
1119
|
+
const handle = timers.setTimeout(() => {
|
|
1120
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1121
|
+
resolve();
|
|
1122
|
+
}, ms);
|
|
1123
|
+
const onAbort = () => {
|
|
1124
|
+
timers.clearTimeout(handle);
|
|
1125
|
+
resolve();
|
|
1126
|
+
};
|
|
1127
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/client/text.ts
|
|
1132
|
+
function hasLoneSurrogate(value) {
|
|
1133
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1134
|
+
const code = value.charCodeAt(i);
|
|
1135
|
+
if (code >= 55296 && code <= 56319) {
|
|
1136
|
+
const low = value.charCodeAt(i + 1);
|
|
1137
|
+
if (!(low >= 56320 && low <= 57343)) return true;
|
|
1138
|
+
i += 1;
|
|
1139
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/core/records/repository.ts
|
|
1147
|
+
var BASE = "/widget/v1/records";
|
|
1148
|
+
function createHttpRecordsRepository(deps) {
|
|
1149
|
+
const { auth } = deps;
|
|
1150
|
+
const mode = auth.mode;
|
|
1151
|
+
const funnel = deps.funnel ?? createRequestFunnel({ auth, ...deps.errorPolicy ? { errorPolicy: deps.errorPolicy } : {} });
|
|
1152
|
+
const send = async (call, req, kind) => {
|
|
1153
|
+
const res = await funnel.send(call, req);
|
|
1154
|
+
if (res.ok) return res;
|
|
1155
|
+
return err(tagReason(res.error, mode, kind));
|
|
1156
|
+
};
|
|
1157
|
+
const segmentError = (field, value) => {
|
|
1158
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1159
|
+
return makeEverfurError("validationFailed", {
|
|
1160
|
+
retryable: false,
|
|
1161
|
+
wireMessage: `${field} is required and must be a non-empty string`
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
if (hasLoneSurrogate(value)) {
|
|
1165
|
+
return makeEverfurError("validationFailed", {
|
|
1166
|
+
retryable: false,
|
|
1167
|
+
wireMessage: `${field} contains an unpaired surrogate and cannot be encoded`
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
return null;
|
|
1171
|
+
};
|
|
1172
|
+
const boundsError = (field, value, max) => {
|
|
1173
|
+
const unusable = segmentError(field, value);
|
|
1174
|
+
if (unusable !== null) return unusable;
|
|
1175
|
+
const value_ = value;
|
|
1176
|
+
const length = [...value_].length;
|
|
1177
|
+
if (length > max) {
|
|
1178
|
+
return makeEverfurError("validationFailed", {
|
|
1179
|
+
retryable: false,
|
|
1180
|
+
wireMessage: `${field} is ${length} characters; the server accepts at most ${max}`
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
return null;
|
|
1184
|
+
};
|
|
1185
|
+
const firstError = (...checks) => checks.find((c) => c !== null) ?? null;
|
|
1186
|
+
return {
|
|
1187
|
+
createRequest: (body) => {
|
|
1188
|
+
const invalid = firstError(
|
|
1189
|
+
boundsError("idempotency_key", body.idempotency_key, IDEMPOTENCY_KEY_MAX),
|
|
1190
|
+
boundsError("pet_ref", body.pet_ref, PET_REF_MAX)
|
|
1191
|
+
);
|
|
1192
|
+
if (invalid) return Promise.resolve(err(invalid));
|
|
1193
|
+
return send("records.createRequest", post(`${BASE}/requests`, body), "request");
|
|
1194
|
+
},
|
|
1195
|
+
initiateUpload: (requestId, body) => {
|
|
1196
|
+
const invalid = segmentError("request_id", requestId);
|
|
1197
|
+
if (invalid) return Promise.resolve(err(invalid));
|
|
1198
|
+
return send(
|
|
1199
|
+
"records.initiateUpload",
|
|
1200
|
+
post(`${BASE}/requests/${encodeURIComponent(requestId)}/uploads/initiate`, body),
|
|
1201
|
+
"request"
|
|
1202
|
+
);
|
|
1203
|
+
},
|
|
1204
|
+
completeUpload: (requestId, body) => {
|
|
1205
|
+
const invalid = segmentError("request_id", requestId);
|
|
1206
|
+
if (invalid) return Promise.resolve(err(invalid));
|
|
1207
|
+
return send(
|
|
1208
|
+
"records.completeUpload",
|
|
1209
|
+
post(`${BASE}/requests/${encodeURIComponent(requestId)}/uploads/complete`, body),
|
|
1210
|
+
"request"
|
|
1211
|
+
);
|
|
1212
|
+
},
|
|
1213
|
+
getRequest: (requestId) => {
|
|
1214
|
+
const invalid = segmentError("request_id", requestId);
|
|
1215
|
+
if (invalid) return Promise.resolve(err(invalid));
|
|
1216
|
+
return send(
|
|
1217
|
+
"records.getRequest",
|
|
1218
|
+
get(`${BASE}/requests/${encodeURIComponent(requestId)}`),
|
|
1219
|
+
"request"
|
|
1220
|
+
);
|
|
1221
|
+
},
|
|
1222
|
+
listRequests: async () => {
|
|
1223
|
+
const res = await send(
|
|
1224
|
+
"records.listRequests",
|
|
1225
|
+
get(`${BASE}/requests`),
|
|
1226
|
+
"request"
|
|
1227
|
+
);
|
|
1228
|
+
if (!res.ok) return res;
|
|
1229
|
+
const value = res.value;
|
|
1230
|
+
const items = Array.isArray(value) ? value : value.items ?? [];
|
|
1231
|
+
return { ok: true, value: items };
|
|
1232
|
+
},
|
|
1233
|
+
getRecord: (petRef) => {
|
|
1234
|
+
const invalid = boundsError("pet_ref", petRef, PET_REF_MAX);
|
|
1235
|
+
if (invalid) return Promise.resolve(err(invalid));
|
|
1236
|
+
return send(
|
|
1237
|
+
"records.getRecord",
|
|
1238
|
+
get(`${BASE}/pets/${encodeURIComponent(petRef)}/record`),
|
|
1239
|
+
"content"
|
|
1240
|
+
);
|
|
1241
|
+
},
|
|
1242
|
+
getConsent: (petRef) => {
|
|
1243
|
+
const invalid = boundsError("pet_ref", petRef, PET_REF_MAX);
|
|
1244
|
+
if (invalid) return Promise.resolve(err(invalid));
|
|
1245
|
+
return send(
|
|
1246
|
+
"records.getConsent",
|
|
1247
|
+
get(`${BASE}/pets/${encodeURIComponent(petRef)}/consent`),
|
|
1248
|
+
"consent"
|
|
1249
|
+
);
|
|
1250
|
+
},
|
|
1251
|
+
putConsent: (petRef, body) => {
|
|
1252
|
+
const invalid = firstError(
|
|
1253
|
+
boundsError("consent_version", body.consent_version, CONSENT_VERSION_MAX),
|
|
1254
|
+
boundsError("idempotency_key", body.idempotency_key, IDEMPOTENCY_KEY_MAX),
|
|
1255
|
+
boundsError("pet_ref", petRef, PET_REF_MAX)
|
|
1256
|
+
);
|
|
1257
|
+
if (invalid) return Promise.resolve(err(invalid));
|
|
1258
|
+
return send(
|
|
1259
|
+
"records.putConsent",
|
|
1260
|
+
put(`${BASE}/pets/${encodeURIComponent(petRef)}/consent`, body),
|
|
1261
|
+
"consent"
|
|
1262
|
+
);
|
|
1263
|
+
},
|
|
1264
|
+
deleteConsent: (petRef) => {
|
|
1265
|
+
const invalid = boundsError("pet_ref", petRef, PET_REF_MAX);
|
|
1266
|
+
if (invalid) return Promise.resolve(err(invalid));
|
|
1267
|
+
return send(
|
|
1268
|
+
"records.deleteConsent",
|
|
1269
|
+
del(`${BASE}/pets/${encodeURIComponent(petRef)}/consent`),
|
|
1270
|
+
"consent"
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function withReason(error, reason) {
|
|
1276
|
+
const tagged = { ...error, recordsReason: reason };
|
|
1277
|
+
for (const key of ["wireMessage", "raw"]) {
|
|
1278
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, key);
|
|
1279
|
+
if (descriptor !== void 0) Object.defineProperty(tagged, key, descriptor);
|
|
1280
|
+
}
|
|
1281
|
+
return Object.freeze(tagged);
|
|
1282
|
+
}
|
|
1283
|
+
function tagReason(error, mode, kind) {
|
|
1284
|
+
if (error.httpStatus === 404) return withReason(error, "entitlement");
|
|
1285
|
+
if (error.code !== "accessDenied") return error;
|
|
1286
|
+
const reason = mode === "anonymous" ? "session_required" : kind === "content" ? "consent_required" : "entitlement";
|
|
1287
|
+
return withReason(error, reason);
|
|
1288
|
+
}
|
|
1289
|
+
function get(path) {
|
|
1290
|
+
return { method: "GET", path };
|
|
1291
|
+
}
|
|
1292
|
+
function post(path, body) {
|
|
1293
|
+
return { method: "POST", path, body };
|
|
1294
|
+
}
|
|
1295
|
+
function put(path, body) {
|
|
1296
|
+
return { method: "PUT", path, body };
|
|
1297
|
+
}
|
|
1298
|
+
function del(path) {
|
|
1299
|
+
return { method: "DELETE", path };
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/client/entitlements/resolve.ts
|
|
1303
|
+
var REQUIRES = Object.freeze({
|
|
1304
|
+
chat: Object.freeze(["chat.message.create"]),
|
|
1305
|
+
records: Object.freeze(["records.upload.create", "records.record.read"]),
|
|
1306
|
+
video: Object.freeze(["video.gait.execute"]),
|
|
1307
|
+
photo: Object.freeze(["photo.checkup.execute"]),
|
|
1308
|
+
animations: Object.freeze(["animations.state.read"])
|
|
1309
|
+
});
|
|
1310
|
+
function requires(name) {
|
|
1311
|
+
return REQUIRES[name];
|
|
1312
|
+
}
|
|
1313
|
+
function decideCapability(name, decisions, opts) {
|
|
1314
|
+
if (opts?.isPending === true) {
|
|
1315
|
+
return freezeDecision({ name, enabled: false, isPending: true, limits: {} });
|
|
1316
|
+
}
|
|
1317
|
+
const keys = requires(name);
|
|
1318
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
1319
|
+
for (const decision of decisions) byResource.set(decision.resource, decision);
|
|
1320
|
+
const limits = mergeLimits(keys, byResource);
|
|
1321
|
+
const missing = keys.filter((key) => byResource.get(key)?.granted !== true);
|
|
1322
|
+
if (missing.length === 0) {
|
|
1323
|
+
return freezeDecision({ name, enabled: true, isPending: false, limits });
|
|
1324
|
+
}
|
|
1325
|
+
return freezeDecision({
|
|
1326
|
+
name,
|
|
1327
|
+
enabled: false,
|
|
1328
|
+
isPending: false,
|
|
1329
|
+
limits,
|
|
1330
|
+
disabled: disabledFor(keys, byResource)
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
function mergeLimits(keys, byResource) {
|
|
1334
|
+
const out = {};
|
|
1335
|
+
for (const key of keys) {
|
|
1336
|
+
const decisionLimits = byResource.get(key)?.limits;
|
|
1337
|
+
if (!decisionLimits) continue;
|
|
1338
|
+
for (const [limitKey, limitValue] of Object.entries(decisionLimits)) {
|
|
1339
|
+
const existing = out[limitKey];
|
|
1340
|
+
out[limitKey] = existing === void 0 ? limitValue : Math.min(existing, limitValue);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return out;
|
|
1344
|
+
}
|
|
1345
|
+
function disabledFor(keys, byResource) {
|
|
1346
|
+
for (const key of keys) {
|
|
1347
|
+
const explicit = byResource.get(key)?.disabled;
|
|
1348
|
+
if (explicit) {
|
|
1349
|
+
return Object.freeze({
|
|
1350
|
+
reason: explicit.reason,
|
|
1351
|
+
level: explicit.level,
|
|
1352
|
+
recoverable: explicit.recoverable
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return Object.freeze({
|
|
1357
|
+
reason: userFacingError("accessDenied"),
|
|
1358
|
+
level: "user_entitlement",
|
|
1359
|
+
recoverable: false
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
function freezeDecision(d) {
|
|
1363
|
+
const base = {
|
|
1364
|
+
name: d.name,
|
|
1365
|
+
enabled: d.enabled,
|
|
1366
|
+
isPending: d.isPending,
|
|
1367
|
+
limits: Object.freeze({ ...d.limits })
|
|
1368
|
+
};
|
|
1369
|
+
if (d.disabled !== void 0) base.disabled = d.disabled;
|
|
1370
|
+
return Object.freeze(base);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/client/theme/tokens.ts
|
|
1374
|
+
function deepFreeze(value) {
|
|
1375
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
1376
|
+
Object.freeze(value);
|
|
1377
|
+
for (const key of Object.keys(value)) {
|
|
1378
|
+
deepFreeze(value[key]);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return value;
|
|
1382
|
+
}
|
|
1383
|
+
var SPACE = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24, xxl: 32 };
|
|
1384
|
+
var RADIUS = { tile: 5, xs: 6, sm: 10, md: 14, lg: 20, xl: 28, pill: 500 };
|
|
1385
|
+
var TYPE_SIZE = {
|
|
1386
|
+
nano: 8,
|
|
1387
|
+
small: 9,
|
|
1388
|
+
micro: 10,
|
|
1389
|
+
tiny: 11,
|
|
1390
|
+
caption: 12,
|
|
1391
|
+
bodyTight: 13,
|
|
1392
|
+
body: 14,
|
|
1393
|
+
subtitle: 16,
|
|
1394
|
+
heading: 18,
|
|
1395
|
+
verdict: 24
|
|
1396
|
+
};
|
|
1397
|
+
var LINE_HEIGHT = { tight: 1.15, snug: 1.3, normal: 1.4, relaxed: 1.55 };
|
|
1398
|
+
var WEIGHT = { regular: "400", medium: "500", bold: "700" };
|
|
1399
|
+
var LETTER_SPACING = {
|
|
1400
|
+
eyebrow: 1,
|
|
1401
|
+
monoWide: 0.9,
|
|
1402
|
+
headerBrand: -0.42,
|
|
1403
|
+
tight: -0.35
|
|
1404
|
+
};
|
|
1405
|
+
function baseType() {
|
|
1406
|
+
return {
|
|
1407
|
+
family: "System",
|
|
1408
|
+
mono: "monospace",
|
|
1409
|
+
fontUrl: null,
|
|
1410
|
+
size: TYPE_SIZE,
|
|
1411
|
+
lineHeight: LINE_HEIGHT,
|
|
1412
|
+
weight: WEIGHT,
|
|
1413
|
+
letterSpacing: LETTER_SPACING
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
var MOTION = {
|
|
1417
|
+
duration: { instant: 80, fast: 140, normal: 220, slow: 320 },
|
|
1418
|
+
spring: { gentle: { damping: 18, stiffness: 200 } },
|
|
1419
|
+
easing: { standard: "cubic-bezier(0.4, 0, 0.2, 1)" },
|
|
1420
|
+
reducedMotion: false
|
|
1421
|
+
};
|
|
1422
|
+
var LIGHT_COLOR = {
|
|
1423
|
+
brand: "#10462E",
|
|
1424
|
+
brandSoft: "rgba(16,70,46,0.10)",
|
|
1425
|
+
brandSofter: "rgba(16,70,46,0.15)",
|
|
1426
|
+
brandBorder: "rgba(16,70,46,0.18)",
|
|
1427
|
+
borderHairlineGreen: "rgba(16,70,46,0.25)",
|
|
1428
|
+
surface: "#FFFFFF",
|
|
1429
|
+
surfaceCream: "#FDFBF8",
|
|
1430
|
+
surfaceSubtle: "#EFE9E3",
|
|
1431
|
+
background: "#FFFFFF",
|
|
1432
|
+
borderRingCream: "#D8D2C8",
|
|
1433
|
+
text: {
|
|
1434
|
+
primary: "#111111",
|
|
1435
|
+
secondary: "#0E462E",
|
|
1436
|
+
muted: "#6E6660",
|
|
1437
|
+
placeholder: "rgba(0,0,0,0.30)",
|
|
1438
|
+
onBrand: "#FFFFFF",
|
|
1439
|
+
metaCool: "#3D3D3D"
|
|
1440
|
+
},
|
|
1441
|
+
primary: "#10462E",
|
|
1442
|
+
onPrimary: "#FFFFFF",
|
|
1443
|
+
accent: "#10462E",
|
|
1444
|
+
onAccent: "#FFFFFF",
|
|
1445
|
+
accentEdge: "#07C168",
|
|
1446
|
+
success: "#07C168",
|
|
1447
|
+
danger: "#B91C1C",
|
|
1448
|
+
errorRed: "#FF1B00",
|
|
1449
|
+
warningStrong: "#A45610",
|
|
1450
|
+
cautionAccent: "#D9821D",
|
|
1451
|
+
riskConcern: "#EFA700",
|
|
1452
|
+
badgeBgPositive: "rgba(7,193,104,0.12)",
|
|
1453
|
+
badgeBgCaution: "rgba(217,130,29,0.14)",
|
|
1454
|
+
badgeBgCritical: "rgba(185,28,28,0.12)",
|
|
1455
|
+
badgeBgNeutral: "rgba(110,102,96,0.12)",
|
|
1456
|
+
bubbleUser: "#10462E",
|
|
1457
|
+
bubbleAssistant: "#FFFFFF",
|
|
1458
|
+
skeleton: "#EFE9E3",
|
|
1459
|
+
scanButtonPink: "#FFC3BC",
|
|
1460
|
+
checkupCardLowRiskBg: "rgba(7,193,104,0.10)",
|
|
1461
|
+
checkupCardConcernBg: "rgba(239,167,0,0.10)"
|
|
1462
|
+
};
|
|
1463
|
+
var EVERFUR_LIGHT = deepFreeze({
|
|
1464
|
+
brightness: "light",
|
|
1465
|
+
meta: { displayName: "Everfur", logoUrl: null, placeholder: null, hidePoweredBy: false },
|
|
1466
|
+
color: LIGHT_COLOR,
|
|
1467
|
+
space: SPACE,
|
|
1468
|
+
radius: RADIUS,
|
|
1469
|
+
type: baseType(),
|
|
1470
|
+
motion: MOTION
|
|
1471
|
+
});
|
|
1472
|
+
var DARK_COLOR = {
|
|
1473
|
+
brand: "#6BD39A",
|
|
1474
|
+
brandSoft: "rgba(107,211,154,0.12)",
|
|
1475
|
+
brandSofter: "rgba(107,211,154,0.18)",
|
|
1476
|
+
brandBorder: "rgba(107,211,154,0.22)",
|
|
1477
|
+
borderHairlineGreen: "rgba(107,211,154,0.28)",
|
|
1478
|
+
surface: "#1C1F23",
|
|
1479
|
+
surfaceCream: "#23272C",
|
|
1480
|
+
surfaceSubtle: "#23272C",
|
|
1481
|
+
background: "#15171A",
|
|
1482
|
+
borderRingCream: "#3A3F45",
|
|
1483
|
+
text: {
|
|
1484
|
+
primary: "#F5F3F0",
|
|
1485
|
+
secondary: "#A9E4C4",
|
|
1486
|
+
muted: "#9A938C",
|
|
1487
|
+
placeholder: "rgba(255,255,255,0.35)",
|
|
1488
|
+
onBrand: "#0E1F17",
|
|
1489
|
+
metaCool: "#C9C9C9"
|
|
1490
|
+
},
|
|
1491
|
+
primary: "#6BD39A",
|
|
1492
|
+
onPrimary: "#0E1F17",
|
|
1493
|
+
accent: "#6BD39A",
|
|
1494
|
+
onAccent: "#0E1F17",
|
|
1495
|
+
accentEdge: "#34D888",
|
|
1496
|
+
success: "#07C168",
|
|
1497
|
+
danger: "#B91C1C",
|
|
1498
|
+
errorRed: "#FF1B00",
|
|
1499
|
+
warningStrong: "#A45610",
|
|
1500
|
+
cautionAccent: "#D9821D",
|
|
1501
|
+
riskConcern: "#EFA700",
|
|
1502
|
+
badgeBgPositive: "rgba(7,193,104,0.18)",
|
|
1503
|
+
badgeBgCaution: "rgba(217,130,29,0.20)",
|
|
1504
|
+
badgeBgCritical: "rgba(185,28,28,0.22)",
|
|
1505
|
+
badgeBgNeutral: "rgba(255,255,255,0.10)",
|
|
1506
|
+
bubbleUser: "#6BD39A",
|
|
1507
|
+
bubbleAssistant: "#1C1F23",
|
|
1508
|
+
skeleton: "#23272C",
|
|
1509
|
+
scanButtonPink: "#FFC3BC",
|
|
1510
|
+
checkupCardLowRiskBg: "rgba(7,193,104,0.16)",
|
|
1511
|
+
checkupCardConcernBg: "rgba(239,167,0,0.16)"
|
|
1512
|
+
};
|
|
1513
|
+
deepFreeze({
|
|
1514
|
+
brightness: "dark",
|
|
1515
|
+
meta: { displayName: "Everfur", logoUrl: null, placeholder: null, hidePoweredBy: false },
|
|
1516
|
+
color: DARK_COLOR,
|
|
1517
|
+
space: SPACE,
|
|
1518
|
+
radius: RADIUS,
|
|
1519
|
+
type: baseType(),
|
|
1520
|
+
motion: MOTION
|
|
1521
|
+
});
|
|
1522
|
+
var EVERFUR_THEME_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("@everfur/sdk#EverfurThemeContext@v1");
|
|
1523
|
+
var globalThemeHost = globalThis;
|
|
1524
|
+
var EverfurThemeContext = globalThemeHost[EVERFUR_THEME_CONTEXT_SYMBOL] ?? (globalThemeHost[EVERFUR_THEME_CONTEXT_SYMBOL] = createContext(EVERFUR_LIGHT));
|
|
1525
|
+
EverfurThemeContext.displayName ??= "EverfurThemeContext";
|
|
1526
|
+
function useEverfurTheme() {
|
|
1527
|
+
return useContext(EverfurThemeContext);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/rn/facade.ts
|
|
1531
|
+
var EVERFUR_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("@everfur/sdk#EverfurContext@v1");
|
|
1532
|
+
var globalContextHost = globalThis;
|
|
1533
|
+
var EverfurContext = globalContextHost[EVERFUR_CONTEXT_SYMBOL] ?? (globalContextHost[EVERFUR_CONTEXT_SYMBOL] = createContext(null));
|
|
1534
|
+
EverfurContext.displayName ??= "EverfurContext";
|
|
1535
|
+
function useEverfurContext() {
|
|
1536
|
+
const ctx = useContext(EverfurContext);
|
|
1537
|
+
if (ctx === null) {
|
|
1538
|
+
throw new EverfurConfigError(
|
|
1539
|
+
"not-initialized",
|
|
1540
|
+
"Everfur hooks must be called inside <EverfurProvider>. Wrap your tree once, high."
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
return ctx;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/rn/records/useRecords.ts
|
|
1547
|
+
var INITIAL = {
|
|
1548
|
+
record: null,
|
|
1549
|
+
requests: [],
|
|
1550
|
+
consent: null,
|
|
1551
|
+
error: null,
|
|
1552
|
+
denialReason: null,
|
|
1553
|
+
withheld: false,
|
|
1554
|
+
loading: true,
|
|
1555
|
+
isUploading: false,
|
|
1556
|
+
uploadProgress: null
|
|
1557
|
+
};
|
|
1558
|
+
var POLL_INTERVAL_MS = 4e3;
|
|
1559
|
+
var MAX_POLLS = 30;
|
|
1560
|
+
function useEverfurRecords(opts) {
|
|
1561
|
+
const { petRef } = opts;
|
|
1562
|
+
const { runtime, userRef } = useEverfurContext();
|
|
1563
|
+
const [controller, setController] = useState(opts.controller ?? null);
|
|
1564
|
+
const [snap, setSnap] = useState(INITIAL);
|
|
1565
|
+
const generation = useRef(0);
|
|
1566
|
+
useEffect(() => {
|
|
1567
|
+
if (opts.controller) {
|
|
1568
|
+
setController(opts.controller);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const { services } = runtime.registry;
|
|
1572
|
+
const telemetry = {
|
|
1573
|
+
record: (event, props) => services.telemetry.track(event, props)
|
|
1574
|
+
};
|
|
1575
|
+
const acquired = runtime.registry.acquire(
|
|
1576
|
+
"records",
|
|
1577
|
+
`${userRef ?? ""}|${petRef}`,
|
|
1578
|
+
() => createRecordsController({
|
|
1579
|
+
repository: createHttpRecordsRepository({
|
|
1580
|
+
auth: services.authFor(userRef),
|
|
1581
|
+
...services.errorPolicy ? { errorPolicy: services.errorPolicy } : {}
|
|
1582
|
+
}),
|
|
1583
|
+
uploads: services.uploadTransport,
|
|
1584
|
+
telemetry
|
|
1585
|
+
}),
|
|
1586
|
+
// The records controller holds only an in-memory cache and has no dispose; dropping the ref clears it.
|
|
1587
|
+
() => {
|
|
1588
|
+
}
|
|
1589
|
+
);
|
|
1590
|
+
setController(acquired);
|
|
1591
|
+
return () => {
|
|
1592
|
+
runtime.registry.release(acquired);
|
|
1593
|
+
setController((current) => current === acquired ? null : current);
|
|
1594
|
+
};
|
|
1595
|
+
}, [runtime, userRef, petRef, opts.controller]);
|
|
1596
|
+
const patch = useCallback((partial) => {
|
|
1597
|
+
setSnap((current) => ({ ...current, ...partial }));
|
|
1598
|
+
}, []);
|
|
1599
|
+
const load = useCallback(async () => {
|
|
1600
|
+
if (!controller) return;
|
|
1601
|
+
const gen = generation.current += 1;
|
|
1602
|
+
pollsRef.current = 0;
|
|
1603
|
+
rearmPolls();
|
|
1604
|
+
patch({ loading: true, error: null, denialReason: null, withheld: false });
|
|
1605
|
+
const [consentRes, listRes] = await Promise.all([
|
|
1606
|
+
controller.getConsent(petRef),
|
|
1607
|
+
controller.listRequests()
|
|
1608
|
+
]);
|
|
1609
|
+
if (gen !== generation.current) return;
|
|
1610
|
+
const consent = consentRes.ok ? consentRes.value : null;
|
|
1611
|
+
const requests = listRes.ok ? listRes.value : [];
|
|
1612
|
+
const listError = listRes.ok ? null : listRes.error;
|
|
1613
|
+
if (consent && consent.granted) {
|
|
1614
|
+
const recordRes = await controller.getRecord(petRef);
|
|
1615
|
+
if (gen !== generation.current) return;
|
|
1616
|
+
if (recordRes.ok) {
|
|
1617
|
+
patch({
|
|
1618
|
+
consent,
|
|
1619
|
+
requests,
|
|
1620
|
+
record: recordRes.value,
|
|
1621
|
+
withheld: recordRes.value.withheld,
|
|
1622
|
+
error: recordRes.value.withheld ? withheldSentinel() : listError,
|
|
1623
|
+
denialReason: null,
|
|
1624
|
+
loading: false
|
|
1625
|
+
});
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
patch({
|
|
1629
|
+
consent,
|
|
1630
|
+
requests,
|
|
1631
|
+
record: null,
|
|
1632
|
+
error: recordRes.error,
|
|
1633
|
+
denialReason: recordsDenialReasonOf(recordRes.error),
|
|
1634
|
+
withheld: false,
|
|
1635
|
+
loading: false
|
|
1636
|
+
});
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
patch({
|
|
1640
|
+
consent,
|
|
1641
|
+
requests,
|
|
1642
|
+
record: null,
|
|
1643
|
+
// When the CONSENT read itself failed, its error is the one that explains the state; `listError` is
|
|
1644
|
+
// incidental. This branch is now reachable with a real error because a 404 no longer folds into a
|
|
1645
|
+
// fake `granted:false`.
|
|
1646
|
+
error: consentRes.ok ? listError : consentRes.error,
|
|
1647
|
+
denialReason: consent ? "consent_required" : consentDenial(consentRes),
|
|
1648
|
+
withheld: false,
|
|
1649
|
+
loading: false
|
|
1650
|
+
});
|
|
1651
|
+
}, [controller, petRef, patch]);
|
|
1652
|
+
useEffect(() => {
|
|
1653
|
+
void load();
|
|
1654
|
+
}, [load]);
|
|
1655
|
+
const activeRequest = useMemo(() => newestRequest(snap.requests), [snap.requests]);
|
|
1656
|
+
const activeId = activeRequest?.id ?? null;
|
|
1657
|
+
const activeStatus = activeRequest?.status ?? null;
|
|
1658
|
+
const pollsRef = useRef(0);
|
|
1659
|
+
const polledIdRef = useRef(null);
|
|
1660
|
+
const [pollEpoch, rearmPolls] = useReducer((n) => n + 1, 0);
|
|
1661
|
+
useEffect(() => {
|
|
1662
|
+
if (!controller || activeId === null || activeStatus === null) return;
|
|
1663
|
+
if (isTerminalStatus(activeStatus) || activeStatus === "in_review") return;
|
|
1664
|
+
if (polledIdRef.current !== String(activeId)) {
|
|
1665
|
+
polledIdRef.current = String(activeId);
|
|
1666
|
+
pollsRef.current = 0;
|
|
1667
|
+
}
|
|
1668
|
+
let cancelled = false;
|
|
1669
|
+
const tick = async () => {
|
|
1670
|
+
if (cancelled || pollsRef.current >= MAX_POLLS) return;
|
|
1671
|
+
pollsRef.current += 1;
|
|
1672
|
+
const res = await controller.getRequest(activeId);
|
|
1673
|
+
if (cancelled) return;
|
|
1674
|
+
if (res.ok) {
|
|
1675
|
+
setSnap((current) => ({ ...current, requests: replaceRequest(current.requests, res.value) }));
|
|
1676
|
+
if (isTerminalStatus(res.value.status)) {
|
|
1677
|
+
if (res.value.status === "published") void load();
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (res.value.status === "in_review") return;
|
|
1681
|
+
}
|
|
1682
|
+
handle = setTimeout(() => void tick(), POLL_INTERVAL_MS);
|
|
1683
|
+
};
|
|
1684
|
+
let handle = setTimeout(() => void tick(), POLL_INTERVAL_MS);
|
|
1685
|
+
return () => {
|
|
1686
|
+
cancelled = true;
|
|
1687
|
+
clearTimeout(handle);
|
|
1688
|
+
};
|
|
1689
|
+
}, [controller, activeId, activeStatus, load, pollEpoch]);
|
|
1690
|
+
const createRequest = useCallback(
|
|
1691
|
+
async (idempotencyKey) => {
|
|
1692
|
+
if (!controller) return notReady();
|
|
1693
|
+
const res = await controller.createUploadRequest({ petRef, idempotencyKey });
|
|
1694
|
+
if (res.ok) setSnap((c) => ({ ...c, requests: replaceRequest(c.requests, res.value) }));
|
|
1695
|
+
return res;
|
|
1696
|
+
},
|
|
1697
|
+
[controller, petRef]
|
|
1698
|
+
);
|
|
1699
|
+
const uploadDocument = useCallback(
|
|
1700
|
+
async (requestId, file) => {
|
|
1701
|
+
if (!controller) return notReady();
|
|
1702
|
+
patch({ isUploading: true, uploadProgress: 0 });
|
|
1703
|
+
const res = await controller.uploadDocument({
|
|
1704
|
+
requestId,
|
|
1705
|
+
file,
|
|
1706
|
+
onProgress: (fraction) => patch({ uploadProgress: fraction })
|
|
1707
|
+
});
|
|
1708
|
+
patch({ isUploading: false, uploadProgress: null });
|
|
1709
|
+
if (res.ok) {
|
|
1710
|
+
const refreshed = await controller.getRequest(requestId);
|
|
1711
|
+
if (refreshed.ok) setSnap((c) => ({ ...c, requests: replaceRequest(c.requests, refreshed.value) }));
|
|
1712
|
+
}
|
|
1713
|
+
return res;
|
|
1714
|
+
},
|
|
1715
|
+
[controller, patch]
|
|
1716
|
+
);
|
|
1717
|
+
const grantConsent = useCallback(
|
|
1718
|
+
async (consentVersion, idempotencyKey) => {
|
|
1719
|
+
if (!controller) return notReady();
|
|
1720
|
+
const res = await controller.grantConsent({ petRef, consentVersion, idempotencyKey });
|
|
1721
|
+
if (res.ok) void load();
|
|
1722
|
+
return res;
|
|
1723
|
+
},
|
|
1724
|
+
[controller, petRef, load]
|
|
1725
|
+
);
|
|
1726
|
+
const revokeConsent = useCallback(async () => {
|
|
1727
|
+
if (!controller) return notReady();
|
|
1728
|
+
const res = await controller.revokeConsent(petRef);
|
|
1729
|
+
if (res.ok) {
|
|
1730
|
+
patch({ record: null, withheld: false, denialReason: "consent_required", consent: res.value });
|
|
1731
|
+
}
|
|
1732
|
+
return res;
|
|
1733
|
+
}, [controller, petRef, patch]);
|
|
1734
|
+
const refresh = useCallback(() => {
|
|
1735
|
+
void load();
|
|
1736
|
+
}, [load]);
|
|
1737
|
+
const status = selectStatus(snap, controller !== null);
|
|
1738
|
+
return useMemo(
|
|
1739
|
+
() => ({
|
|
1740
|
+
status,
|
|
1741
|
+
record: snap.record,
|
|
1742
|
+
requests: snap.requests,
|
|
1743
|
+
activeRequest,
|
|
1744
|
+
consent: snap.consent,
|
|
1745
|
+
error: snap.error,
|
|
1746
|
+
denialReason: snap.denialReason,
|
|
1747
|
+
withheld: snap.withheld,
|
|
1748
|
+
isUploading: snap.isUploading,
|
|
1749
|
+
uploadProgress: snap.uploadProgress,
|
|
1750
|
+
createRequest,
|
|
1751
|
+
uploadDocument,
|
|
1752
|
+
grantConsent,
|
|
1753
|
+
revokeConsent,
|
|
1754
|
+
refresh
|
|
1755
|
+
}),
|
|
1756
|
+
[status, snap, activeRequest, createRequest, uploadDocument, grantConsent, revokeConsent, refresh]
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
function selectStatus(snap, hasController) {
|
|
1760
|
+
if (snap.loading || !hasController) return "loading";
|
|
1761
|
+
if (snap.withheld || snap.denialReason !== null || snap.error !== null) return "error";
|
|
1762
|
+
if (snap.record !== null && !snap.record.isEmpty) return "populated";
|
|
1763
|
+
if (snap.requests.length > 0) return "populated";
|
|
1764
|
+
return "empty";
|
|
1765
|
+
}
|
|
1766
|
+
function newestRequest(requests) {
|
|
1767
|
+
if (requests.length === 0) return null;
|
|
1768
|
+
let newest = requests[0];
|
|
1769
|
+
for (const r of requests) if (r.updatedAt > newest.updatedAt) newest = r;
|
|
1770
|
+
return newest;
|
|
1771
|
+
}
|
|
1772
|
+
function replaceRequest(requests, next) {
|
|
1773
|
+
const filtered = requests.filter((r) => r.id !== next.id);
|
|
1774
|
+
return [next, ...filtered];
|
|
1775
|
+
}
|
|
1776
|
+
function consentDenial(res) {
|
|
1777
|
+
return res.ok ? "consent_required" : recordsDenialReasonOf(res.error) ?? "consent_required";
|
|
1778
|
+
}
|
|
1779
|
+
function withheldSentinel() {
|
|
1780
|
+
return makeEverfurError("internalError", { retryable: false });
|
|
1781
|
+
}
|
|
1782
|
+
function notReady() {
|
|
1783
|
+
return { ok: false, error: makeEverfurError("internalError", { retryable: true }) };
|
|
1784
|
+
}
|
|
1785
|
+
function useCapability(name) {
|
|
1786
|
+
const { runtime } = useEverfurContext();
|
|
1787
|
+
const store = runtime.entitlements;
|
|
1788
|
+
const snapshot = useSyncExternalStore(
|
|
1789
|
+
store.subscribe,
|
|
1790
|
+
store.getSnapshot,
|
|
1791
|
+
store.getSnapshot
|
|
1792
|
+
);
|
|
1793
|
+
return useMemo(
|
|
1794
|
+
() => decideCapability(name, snapshot.decisions, { isPending: snapshot.isPending }),
|
|
1795
|
+
[name, snapshot]
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
var MIN_OFFSTATE_HEIGHT = 48;
|
|
1799
|
+
function selectView(decision) {
|
|
1800
|
+
if (decision.isPending) return "pending";
|
|
1801
|
+
return decision.enabled ? "enabled" : "disabled";
|
|
1802
|
+
}
|
|
1803
|
+
function CapabilityGate(props) {
|
|
1804
|
+
const { capability, children, renderDisabled, renderPending, style } = props;
|
|
1805
|
+
const decision = useCapability(capability);
|
|
1806
|
+
const view = useMemo(() => selectView(decision), [decision]);
|
|
1807
|
+
switch (view) {
|
|
1808
|
+
case "enabled":
|
|
1809
|
+
return children;
|
|
1810
|
+
case "pending":
|
|
1811
|
+
return renderPending ? renderPending() : /* @__PURE__ */ jsx(PendingSkeleton, { capability, style });
|
|
1812
|
+
case "disabled":
|
|
1813
|
+
return renderDisabled ? renderDisabled(decision) : /* @__PURE__ */ jsx(OffState, { decision, style });
|
|
1814
|
+
default:
|
|
1815
|
+
return assertNever(view);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
function PendingSkeleton({
|
|
1819
|
+
capability,
|
|
1820
|
+
style
|
|
1821
|
+
}) {
|
|
1822
|
+
const theme = useEverfurTheme();
|
|
1823
|
+
const styles = useMemo(() => makeStyles(theme), [theme]);
|
|
1824
|
+
return /* @__PURE__ */ jsx(
|
|
1825
|
+
View,
|
|
1826
|
+
{
|
|
1827
|
+
style: [styles.offstate, style],
|
|
1828
|
+
accessible: true,
|
|
1829
|
+
accessibilityState: { busy: true },
|
|
1830
|
+
accessibilityLabel: `Loading ${capability}`,
|
|
1831
|
+
children: /* @__PURE__ */ jsx(View, { style: styles.skeletonBar })
|
|
1832
|
+
}
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
function OffState({
|
|
1836
|
+
decision,
|
|
1837
|
+
style
|
|
1838
|
+
}) {
|
|
1839
|
+
const theme = useEverfurTheme();
|
|
1840
|
+
const styles = useMemo(() => makeStyles(theme), [theme]);
|
|
1841
|
+
const message = decision.disabled?.reason ?? "";
|
|
1842
|
+
return /* @__PURE__ */ jsx(
|
|
1843
|
+
View,
|
|
1844
|
+
{
|
|
1845
|
+
style: [styles.offstate, style],
|
|
1846
|
+
accessible: true,
|
|
1847
|
+
accessibilityRole: "text",
|
|
1848
|
+
accessibilityLabel: message,
|
|
1849
|
+
children: message.length > 0 ? /* @__PURE__ */ jsx(Text, { style: styles.offstateText, children: message }) : null
|
|
1850
|
+
}
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
function makeStyles(theme) {
|
|
1854
|
+
const { color, space, radius, type } = theme;
|
|
1855
|
+
return StyleSheet.create({
|
|
1856
|
+
// A deliberate cream card with a soft brand hairline, never a bare grey slab.
|
|
1857
|
+
offstate: {
|
|
1858
|
+
minHeight: MIN_OFFSTATE_HEIGHT,
|
|
1859
|
+
justifyContent: "center",
|
|
1860
|
+
paddingHorizontal: space.lg,
|
|
1861
|
+
paddingVertical: space.md,
|
|
1862
|
+
backgroundColor: color.surfaceCream,
|
|
1863
|
+
borderRadius: radius.md,
|
|
1864
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
1865
|
+
borderColor: color.brandBorder
|
|
1866
|
+
},
|
|
1867
|
+
offstateText: {
|
|
1868
|
+
fontFamily: type.family,
|
|
1869
|
+
fontSize: type.size.body,
|
|
1870
|
+
lineHeight: Math.round(type.size.body * type.lineHeight.normal),
|
|
1871
|
+
color: color.text.muted
|
|
1872
|
+
},
|
|
1873
|
+
// A soft green shimmer bar (brand wash) that stays legible on the cream card in both light and dark,
|
|
1874
|
+
// where the neutral `skeleton` token would collide with the dark cream surface.
|
|
1875
|
+
skeletonBar: {
|
|
1876
|
+
height: space.md,
|
|
1877
|
+
borderRadius: radius.xs,
|
|
1878
|
+
backgroundColor: color.brandSofter
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
var CLINICAL_DISCLAIMER = Object.freeze({
|
|
1883
|
+
title: "For educational use only",
|
|
1884
|
+
body: "Everfur is for educational purposes only. It helps you track visible changes through AI checkups. It does not diagnose, treat, prescribe, or replace veterinary care. For urgent symptoms or emergencies, contact a veterinarian immediately."
|
|
1885
|
+
});
|
|
1886
|
+
function ClinicalDisclaimer(props) {
|
|
1887
|
+
const theme = useEverfurTheme();
|
|
1888
|
+
const styles = useMemo(() => makeStyles2(theme), [theme]);
|
|
1889
|
+
return /* @__PURE__ */ jsxs(
|
|
1890
|
+
View,
|
|
1891
|
+
{
|
|
1892
|
+
style: [styles.container, props.style],
|
|
1893
|
+
accessible: true,
|
|
1894
|
+
accessibilityRole: "text",
|
|
1895
|
+
accessibilityLabel: `${CLINICAL_DISCLAIMER.title}. ${CLINICAL_DISCLAIMER.body}`,
|
|
1896
|
+
children: [
|
|
1897
|
+
/* @__PURE__ */ jsx(Text, { style: styles.title, children: CLINICAL_DISCLAIMER.title }),
|
|
1898
|
+
/* @__PURE__ */ jsx(Text, { style: styles.body, children: CLINICAL_DISCLAIMER.body })
|
|
1899
|
+
]
|
|
1900
|
+
}
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
function makeStyles2(theme) {
|
|
1904
|
+
const { color, space, type } = theme;
|
|
1905
|
+
return StyleSheet.create({
|
|
1906
|
+
container: {
|
|
1907
|
+
paddingHorizontal: space.lg,
|
|
1908
|
+
paddingVertical: space.md,
|
|
1909
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
1910
|
+
borderTopColor: color.borderHairlineGreen,
|
|
1911
|
+
backgroundColor: color.surfaceCream,
|
|
1912
|
+
gap: space.xs
|
|
1913
|
+
},
|
|
1914
|
+
title: {
|
|
1915
|
+
fontFamily: type.family,
|
|
1916
|
+
fontSize: type.size.caption,
|
|
1917
|
+
lineHeight: Math.round(type.size.caption * type.lineHeight.snug),
|
|
1918
|
+
color: color.text.secondary,
|
|
1919
|
+
fontWeight: type.weight.bold,
|
|
1920
|
+
letterSpacing: type.letterSpacing.eyebrow
|
|
1921
|
+
},
|
|
1922
|
+
body: {
|
|
1923
|
+
fontFamily: type.family,
|
|
1924
|
+
fontSize: type.size.caption,
|
|
1925
|
+
lineHeight: Math.round(type.size.caption * type.lineHeight.snug),
|
|
1926
|
+
color: color.text.muted,
|
|
1927
|
+
fontWeight: type.weight.regular
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// src/rn/records/format.ts
|
|
1933
|
+
function formatEntityValue(value, unit) {
|
|
1934
|
+
if (value === null) return "";
|
|
1935
|
+
return unit ? `${value} ${unit}` : value;
|
|
1936
|
+
}
|
|
1937
|
+
function formatShortDate(at) {
|
|
1938
|
+
if (at === null || at.length === 0) return "";
|
|
1939
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})/.exec(at);
|
|
1940
|
+
if (iso !== null) {
|
|
1941
|
+
const year = iso[1] ?? "";
|
|
1942
|
+
const month = iso[2] ?? "";
|
|
1943
|
+
const day = iso[3] ?? "";
|
|
1944
|
+
return `${Number(month)}/${Number(day)}/${year.slice(2)}`;
|
|
1945
|
+
}
|
|
1946
|
+
const ms = Date.parse(at);
|
|
1947
|
+
if (Number.isNaN(ms)) return at;
|
|
1948
|
+
const date = new Date(ms);
|
|
1949
|
+
return `${date.getUTCMonth() + 1}/${date.getUTCDate()}/${String(date.getUTCFullYear()).slice(2)}`;
|
|
1950
|
+
}
|
|
1951
|
+
var TINT_KEYS = ["brandSoft", "badgeBgPositive", "badgeBgCaution", "badgeBgNeutral"];
|
|
1952
|
+
function buildRows(entities, forwardLooking, documents) {
|
|
1953
|
+
const rows = [];
|
|
1954
|
+
const order = [];
|
|
1955
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
1956
|
+
for (const entity of entities) {
|
|
1957
|
+
const category = entity.category.trim();
|
|
1958
|
+
if (category.length === 0) continue;
|
|
1959
|
+
const bucket = byCategory.get(category);
|
|
1960
|
+
if (bucket === void 0) {
|
|
1961
|
+
order.push(category);
|
|
1962
|
+
byCategory.set(category, [entity]);
|
|
1963
|
+
} else {
|
|
1964
|
+
bucket.push(entity);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
for (const category of order) {
|
|
1968
|
+
const group = byCategory.get(category) ?? [];
|
|
1969
|
+
rows.push({
|
|
1970
|
+
id: `cat:${category}`,
|
|
1971
|
+
title: category,
|
|
1972
|
+
count: group.length,
|
|
1973
|
+
dateLabel: null,
|
|
1974
|
+
entities: group,
|
|
1975
|
+
detail: null
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
forwardLooking.forEach((item, index) => {
|
|
1979
|
+
const title = item.title.trim();
|
|
1980
|
+
if (title.length === 0) return;
|
|
1981
|
+
const detail = item.detail !== null && item.detail.length > 0 ? item.detail : null;
|
|
1982
|
+
rows.push({
|
|
1983
|
+
id: `fwd:${index}`,
|
|
1984
|
+
title,
|
|
1985
|
+
count: null,
|
|
1986
|
+
dateLabel: formatShortDate(item.dueAt),
|
|
1987
|
+
entities: null,
|
|
1988
|
+
detail
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
1991
|
+
for (const doc of Object.values(documents)) {
|
|
1992
|
+
const title = doc.title !== null ? doc.title.trim() : "";
|
|
1993
|
+
if (title.length === 0) continue;
|
|
1994
|
+
rows.push({
|
|
1995
|
+
id: `doc:${doc.docId}`,
|
|
1996
|
+
title,
|
|
1997
|
+
count: null,
|
|
1998
|
+
dateLabel: formatShortDate(doc.receivedAt),
|
|
1999
|
+
entities: null,
|
|
2000
|
+
detail: null
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
return rows;
|
|
2004
|
+
}
|
|
2005
|
+
function RecordNavCards({
|
|
2006
|
+
entities,
|
|
2007
|
+
forwardLooking,
|
|
2008
|
+
documents,
|
|
2009
|
+
testID
|
|
2010
|
+
}) {
|
|
2011
|
+
const styles = useStyles();
|
|
2012
|
+
const rows = useMemo(
|
|
2013
|
+
() => buildRows(entities, forwardLooking, documents),
|
|
2014
|
+
[entities, forwardLooking, documents]
|
|
2015
|
+
);
|
|
2016
|
+
const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
|
|
2017
|
+
if (rows.length === 0) return null;
|
|
2018
|
+
const toggle = (id) => {
|
|
2019
|
+
setExpanded((current) => {
|
|
2020
|
+
const next = new Set(current);
|
|
2021
|
+
if (next.has(id)) next.delete(id);
|
|
2022
|
+
else next.add(id);
|
|
2023
|
+
return next;
|
|
2024
|
+
});
|
|
2025
|
+
};
|
|
2026
|
+
return /* @__PURE__ */ jsx(View, { style: styles.stack, testID, children: rows.map((row, index) => {
|
|
2027
|
+
const tintKey = TINT_KEYS[index % TINT_KEYS.length] ?? "brandSoft";
|
|
2028
|
+
const tint = styles.tints[tintKey];
|
|
2029
|
+
const body = row.entities ?? (row.detail !== null ? row.detail : null);
|
|
2030
|
+
const expandable = row.entities !== null || row.detail !== null;
|
|
2031
|
+
const isOpen = expanded.has(row.id);
|
|
2032
|
+
const trailing = row.count !== null ? `${row.count}` : row.dateLabel !== null ? row.dateLabel : null;
|
|
2033
|
+
const a11y = [row.title, trailing].filter((s) => s !== null && s.length > 0).join(", ");
|
|
2034
|
+
return /* @__PURE__ */ jsxs(View, { testID: testID ? `${testID}-row-${index}` : void 0, children: [
|
|
2035
|
+
/* @__PURE__ */ jsx(
|
|
2036
|
+
NavCard,
|
|
2037
|
+
{
|
|
2038
|
+
styles,
|
|
2039
|
+
tint,
|
|
2040
|
+
title: row.title,
|
|
2041
|
+
trailing,
|
|
2042
|
+
expandable,
|
|
2043
|
+
isOpen,
|
|
2044
|
+
a11y,
|
|
2045
|
+
onPress: expandable ? () => toggle(row.id) : void 0
|
|
2046
|
+
}
|
|
2047
|
+
),
|
|
2048
|
+
expandable && isOpen ? /* @__PURE__ */ jsx(NavCardBody, { styles, body }) : null
|
|
2049
|
+
] }, row.id);
|
|
2050
|
+
}) });
|
|
2051
|
+
}
|
|
2052
|
+
function NavCard({
|
|
2053
|
+
styles,
|
|
2054
|
+
tint,
|
|
2055
|
+
title,
|
|
2056
|
+
trailing,
|
|
2057
|
+
expandable,
|
|
2058
|
+
isOpen,
|
|
2059
|
+
a11y,
|
|
2060
|
+
onPress
|
|
2061
|
+
}) {
|
|
2062
|
+
return /* @__PURE__ */ jsxs(
|
|
2063
|
+
Pressable,
|
|
2064
|
+
{
|
|
2065
|
+
onPress,
|
|
2066
|
+
disabled: onPress === void 0,
|
|
2067
|
+
accessibilityRole: "button",
|
|
2068
|
+
accessibilityLabel: a11y,
|
|
2069
|
+
accessibilityState: { expanded: expandable ? isOpen : void 0, disabled: onPress === void 0 },
|
|
2070
|
+
style: [styles.card, tint],
|
|
2071
|
+
children: [
|
|
2072
|
+
/* @__PURE__ */ jsx(View, { style: styles.iconChip }),
|
|
2073
|
+
/* @__PURE__ */ jsx(Text, { style: styles.title, numberOfLines: 2, children: title }),
|
|
2074
|
+
/* @__PURE__ */ jsxs(View, { style: styles.trailing, children: [
|
|
2075
|
+
trailing !== null ? /* @__PURE__ */ jsx(View, { style: styles.pill, children: /* @__PURE__ */ jsx(Text, { style: styles.pillLabel, numberOfLines: 1, children: trailing }) }) : null,
|
|
2076
|
+
expandable ? /* @__PURE__ */ jsx(View, { style: isOpen ? styles.chevronOpen : styles.chevron }) : null
|
|
2077
|
+
] })
|
|
2078
|
+
]
|
|
2079
|
+
}
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
function NavCardBody({
|
|
2083
|
+
styles,
|
|
2084
|
+
body
|
|
2085
|
+
}) {
|
|
2086
|
+
if (body === null) return null;
|
|
2087
|
+
if (typeof body === "string") {
|
|
2088
|
+
return /* @__PURE__ */ jsx(View, { style: styles.body, children: /* @__PURE__ */ jsx(Text, { style: styles.detailText, children: body }) });
|
|
2089
|
+
}
|
|
2090
|
+
return /* @__PURE__ */ jsx(View, { style: styles.body, children: body.map((entity) => /* @__PURE__ */ jsxs(View, { style: styles.factRow, children: [
|
|
2091
|
+
/* @__PURE__ */ jsx(Text, { style: styles.factLabel, numberOfLines: 2, children: entity.label }),
|
|
2092
|
+
/* @__PURE__ */ jsx(Text, { style: styles.factValue, numberOfLines: 2, children: formatEntityValue(entity.value, entity.unit) })
|
|
2093
|
+
] }, entity.key)) });
|
|
2094
|
+
}
|
|
2095
|
+
function useStyles() {
|
|
2096
|
+
const theme = useEverfurTheme();
|
|
2097
|
+
return useMemo(() => makeStyles3(theme), [theme]);
|
|
2098
|
+
}
|
|
2099
|
+
function makeStyles3(theme) {
|
|
2100
|
+
const { color, space, radius, type } = theme;
|
|
2101
|
+
const { size, lineHeight, weight, letterSpacing, family } = type;
|
|
2102
|
+
const lh = (fontSize, multiplier) => Math.round(fontSize * multiplier);
|
|
2103
|
+
const hairline = StyleSheet.hairlineWidth;
|
|
2104
|
+
const boldWeight = weight.bold;
|
|
2105
|
+
const mediumWeight = weight.medium;
|
|
2106
|
+
const rowMinHeight = space.xxl + space.xl;
|
|
2107
|
+
const chipSize = space.xl;
|
|
2108
|
+
const chevronSize = space.sm;
|
|
2109
|
+
const chevronStroke = space.xs / 2;
|
|
2110
|
+
const softShadow = {
|
|
2111
|
+
shadowColor: color.brand,
|
|
2112
|
+
shadowOffset: { width: 0, height: space.xs },
|
|
2113
|
+
shadowRadius: space.sm,
|
|
2114
|
+
shadowOpacity: 0.06,
|
|
2115
|
+
elevation: space.xs
|
|
2116
|
+
};
|
|
2117
|
+
const chevronBase = {
|
|
2118
|
+
width: chevronSize,
|
|
2119
|
+
height: chevronSize,
|
|
2120
|
+
borderTopWidth: chevronStroke,
|
|
2121
|
+
borderRightWidth: chevronStroke,
|
|
2122
|
+
borderColor: color.brand
|
|
2123
|
+
};
|
|
2124
|
+
return {
|
|
2125
|
+
tints: {
|
|
2126
|
+
brandSoft: { backgroundColor: color.brandSoft },
|
|
2127
|
+
badgeBgPositive: { backgroundColor: color.badgeBgPositive },
|
|
2128
|
+
badgeBgCaution: { backgroundColor: color.badgeBgCaution },
|
|
2129
|
+
badgeBgNeutral: { backgroundColor: color.badgeBgNeutral }
|
|
2130
|
+
},
|
|
2131
|
+
...StyleSheet.create({
|
|
2132
|
+
stack: { gap: space.sm },
|
|
2133
|
+
card: {
|
|
2134
|
+
flexDirection: "row",
|
|
2135
|
+
alignItems: "center",
|
|
2136
|
+
gap: space.md,
|
|
2137
|
+
minHeight: rowMinHeight,
|
|
2138
|
+
paddingHorizontal: space.lg,
|
|
2139
|
+
paddingVertical: space.md,
|
|
2140
|
+
borderRadius: radius.md,
|
|
2141
|
+
borderWidth: hairline,
|
|
2142
|
+
borderColor: color.brandBorder,
|
|
2143
|
+
...softShadow
|
|
2144
|
+
},
|
|
2145
|
+
iconChip: {
|
|
2146
|
+
width: chipSize,
|
|
2147
|
+
height: chipSize,
|
|
2148
|
+
borderRadius: radius.sm,
|
|
2149
|
+
backgroundColor: color.brand
|
|
2150
|
+
},
|
|
2151
|
+
title: {
|
|
2152
|
+
flex: 1,
|
|
2153
|
+
fontFamily: family,
|
|
2154
|
+
fontSize: size.subtitle,
|
|
2155
|
+
lineHeight: lh(size.subtitle, lineHeight.snug),
|
|
2156
|
+
color: color.text.secondary,
|
|
2157
|
+
fontWeight: boldWeight,
|
|
2158
|
+
letterSpacing: letterSpacing.tight
|
|
2159
|
+
},
|
|
2160
|
+
trailing: {
|
|
2161
|
+
flexDirection: "row",
|
|
2162
|
+
alignItems: "center",
|
|
2163
|
+
gap: space.sm
|
|
2164
|
+
},
|
|
2165
|
+
pill: {
|
|
2166
|
+
minWidth: space.xl,
|
|
2167
|
+
alignItems: "center",
|
|
2168
|
+
justifyContent: "center",
|
|
2169
|
+
paddingHorizontal: space.sm,
|
|
2170
|
+
paddingVertical: space.xs,
|
|
2171
|
+
borderRadius: radius.pill,
|
|
2172
|
+
backgroundColor: color.surfaceCream,
|
|
2173
|
+
borderWidth: hairline,
|
|
2174
|
+
borderColor: color.brandBorder
|
|
2175
|
+
},
|
|
2176
|
+
pillLabel: {
|
|
2177
|
+
fontFamily: family,
|
|
2178
|
+
fontSize: size.caption,
|
|
2179
|
+
lineHeight: lh(size.caption, lineHeight.tight),
|
|
2180
|
+
color: color.text.secondary,
|
|
2181
|
+
fontWeight: boldWeight
|
|
2182
|
+
},
|
|
2183
|
+
chevron: { ...chevronBase, transform: [{ rotate: "45deg" }] },
|
|
2184
|
+
chevronOpen: { ...chevronBase, transform: [{ rotate: "135deg" }] },
|
|
2185
|
+
body: {
|
|
2186
|
+
gap: space.xs,
|
|
2187
|
+
marginTop: space.xs,
|
|
2188
|
+
paddingHorizontal: space.lg,
|
|
2189
|
+
paddingVertical: space.md,
|
|
2190
|
+
borderRadius: radius.sm,
|
|
2191
|
+
backgroundColor: color.surfaceCream,
|
|
2192
|
+
borderWidth: hairline,
|
|
2193
|
+
borderColor: color.brandBorder
|
|
2194
|
+
},
|
|
2195
|
+
factRow: {
|
|
2196
|
+
flexDirection: "row",
|
|
2197
|
+
justifyContent: "space-between",
|
|
2198
|
+
gap: space.md,
|
|
2199
|
+
paddingVertical: space.xs,
|
|
2200
|
+
borderBottomWidth: hairline,
|
|
2201
|
+
borderBottomColor: color.borderHairlineGreen
|
|
2202
|
+
},
|
|
2203
|
+
factLabel: {
|
|
2204
|
+
fontFamily: family,
|
|
2205
|
+
fontSize: size.body,
|
|
2206
|
+
lineHeight: lh(size.body, lineHeight.normal),
|
|
2207
|
+
color: color.text.muted,
|
|
2208
|
+
flexShrink: 1
|
|
2209
|
+
},
|
|
2210
|
+
factValue: {
|
|
2211
|
+
fontFamily: family,
|
|
2212
|
+
fontSize: size.body,
|
|
2213
|
+
lineHeight: lh(size.body, lineHeight.normal),
|
|
2214
|
+
color: color.text.primary,
|
|
2215
|
+
fontWeight: mediumWeight,
|
|
2216
|
+
textAlign: "right",
|
|
2217
|
+
flexShrink: 1
|
|
2218
|
+
},
|
|
2219
|
+
detailText: {
|
|
2220
|
+
fontFamily: family,
|
|
2221
|
+
fontSize: size.body,
|
|
2222
|
+
lineHeight: lh(size.body, lineHeight.normal),
|
|
2223
|
+
color: color.text.secondary
|
|
2224
|
+
}
|
|
2225
|
+
})
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
function pct(value) {
|
|
2229
|
+
return `${value}%`;
|
|
2230
|
+
}
|
|
2231
|
+
function isFinitePoint(point) {
|
|
2232
|
+
return point.value !== null && Number.isFinite(point.value);
|
|
2233
|
+
}
|
|
2234
|
+
function pointDisplay(point) {
|
|
2235
|
+
if (point.display !== null && point.display.length > 0) return point.display;
|
|
2236
|
+
return point.value !== null ? String(point.value) : "";
|
|
2237
|
+
}
|
|
2238
|
+
function buildTrendModel(series) {
|
|
2239
|
+
const primary = series.find((s) => s.points.some(isFinitePoint));
|
|
2240
|
+
if (primary === void 0) return null;
|
|
2241
|
+
const numeric = primary.points.filter(isFinitePoint);
|
|
2242
|
+
const first = numeric[0];
|
|
2243
|
+
if (first === void 0) return null;
|
|
2244
|
+
const values = numeric.map((p) => p.value);
|
|
2245
|
+
let lo = Math.min(...values);
|
|
2246
|
+
let hi = Math.max(...values);
|
|
2247
|
+
if (lo === hi) {
|
|
2248
|
+
lo -= 1;
|
|
2249
|
+
hi += 1;
|
|
2250
|
+
}
|
|
2251
|
+
const pad = (hi - lo) * 0.18;
|
|
2252
|
+
const yMin = lo - pad;
|
|
2253
|
+
const yMax = hi + pad;
|
|
2254
|
+
const span = yMax - yMin;
|
|
2255
|
+
const xInset = 6;
|
|
2256
|
+
const count = numeric.length;
|
|
2257
|
+
const points = numeric.map((p, index) => ({
|
|
2258
|
+
xPct: count <= 1 ? 50 : xInset + index / (count - 1) * (100 - 2 * xInset),
|
|
2259
|
+
// y in percent DOWN from the top: the max of the domain sits at 0%, the min at 100%.
|
|
2260
|
+
yPct: (yMax - p.value) / span * 100
|
|
2261
|
+
}));
|
|
2262
|
+
const bandTopPct = (yMax - hi) / span * 100;
|
|
2263
|
+
const bandBottomPct = (yMax - lo) / span * 100;
|
|
2264
|
+
const minPoint = numeric.reduce((acc, p) => p.value < acc.value ? p : acc, first);
|
|
2265
|
+
const maxPoint = numeric.reduce((acc, p) => p.value > acc.value ? p : acc, first);
|
|
2266
|
+
const lastNumeric = numeric[numeric.length - 1] ?? first;
|
|
2267
|
+
const latest = primary.points[primary.points.length - 1] ?? lastNumeric;
|
|
2268
|
+
const latestDisplay = pointDisplay(latest);
|
|
2269
|
+
const heroDisplay = latestDisplay.length > 0 ? latestDisplay : pointDisplay(lastNumeric);
|
|
2270
|
+
return {
|
|
2271
|
+
label: primary.label,
|
|
2272
|
+
heroDisplay,
|
|
2273
|
+
minDisplay: pointDisplay(minPoint),
|
|
2274
|
+
maxDisplay: pointDisplay(maxPoint),
|
|
2275
|
+
firstDate: formatShortDate(primary.points[0]?.at ?? null),
|
|
2276
|
+
lastDate: formatShortDate(latest.at),
|
|
2277
|
+
points,
|
|
2278
|
+
bandTopPct,
|
|
2279
|
+
bandBottomPct
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
function TrendRangeCard({ series, testID }) {
|
|
2283
|
+
const styles = useStyles2();
|
|
2284
|
+
const model = useMemo(() => buildTrendModel(series), [series]);
|
|
2285
|
+
if (model === null) return null;
|
|
2286
|
+
const summary = model.heroDisplay.length > 0 ? `${model.label}, ${model.heroDisplay}` : model.label;
|
|
2287
|
+
const bandHeightPct = Math.max(0, model.bandBottomPct - model.bandTopPct);
|
|
2288
|
+
return /* @__PURE__ */ jsxs(View, { style: styles.card, testID, children: [
|
|
2289
|
+
/* @__PURE__ */ jsxs(View, { accessible: true, accessibilityRole: "text", accessibilityLabel: summary, children: [
|
|
2290
|
+
/* @__PURE__ */ jsx(Text, { style: styles.eyebrow, numberOfLines: 1, children: model.label }),
|
|
2291
|
+
model.heroDisplay.length > 0 ? /* @__PURE__ */ jsx(Text, { style: styles.hero, numberOfLines: 1, children: model.heroDisplay }) : null
|
|
2292
|
+
] }),
|
|
2293
|
+
/* @__PURE__ */ jsxs(
|
|
2294
|
+
View,
|
|
2295
|
+
{
|
|
2296
|
+
style: styles.plot,
|
|
2297
|
+
accessibilityElementsHidden: true,
|
|
2298
|
+
importantForAccessibility: "no-hide-descendants",
|
|
2299
|
+
children: [
|
|
2300
|
+
model.points.map((p, index) => /* @__PURE__ */ jsx(View, { style: [styles.guide, { left: pct(p.xPct) }] }, `g-${index}`)),
|
|
2301
|
+
/* @__PURE__ */ jsx(View, { style: [styles.band, { top: pct(model.bandTopPct), height: pct(bandHeightPct) }] }),
|
|
2302
|
+
model.points.map((p, index) => {
|
|
2303
|
+
const latest = index === model.points.length - 1;
|
|
2304
|
+
return /* @__PURE__ */ jsx(
|
|
2305
|
+
View,
|
|
2306
|
+
{
|
|
2307
|
+
style: [styles.point, latest ? styles.pointLatest : null, { left: pct(p.xPct), top: pct(p.yPct) }]
|
|
2308
|
+
},
|
|
2309
|
+
`pt-${index}`
|
|
2310
|
+
);
|
|
2311
|
+
}),
|
|
2312
|
+
model.maxDisplay.length > 0 ? /* @__PURE__ */ jsx(Text, { style: [styles.edgeLabel, { top: pct(model.bandTopPct) }], numberOfLines: 1, children: model.maxDisplay }) : null,
|
|
2313
|
+
model.minDisplay.length > 0 ? /* @__PURE__ */ jsx(Text, { style: [styles.edgeLabel, { top: pct(model.bandBottomPct) }], numberOfLines: 1, children: model.minDisplay }) : null
|
|
2314
|
+
]
|
|
2315
|
+
}
|
|
2316
|
+
),
|
|
2317
|
+
model.firstDate.length > 0 || model.lastDate.length > 0 ? /* @__PURE__ */ jsxs(View, { style: styles.dates, children: [
|
|
2318
|
+
/* @__PURE__ */ jsx(Text, { style: styles.dateLabel, children: model.firstDate }),
|
|
2319
|
+
/* @__PURE__ */ jsx(Text, { style: styles.dateLabel, children: model.lastDate })
|
|
2320
|
+
] }) : null
|
|
2321
|
+
] });
|
|
2322
|
+
}
|
|
2323
|
+
function useStyles2() {
|
|
2324
|
+
const theme = useEverfurTheme();
|
|
2325
|
+
return useMemo(() => makeStyles4(theme), [theme]);
|
|
2326
|
+
}
|
|
2327
|
+
function makeStyles4(theme) {
|
|
2328
|
+
const { color, space, radius, type } = theme;
|
|
2329
|
+
const { size, lineHeight, weight, letterSpacing, family } = type;
|
|
2330
|
+
const lh = (fontSize, multiplier) => Math.round(fontSize * multiplier);
|
|
2331
|
+
const hairline = StyleSheet.hairlineWidth;
|
|
2332
|
+
const boldWeight = weight.bold;
|
|
2333
|
+
const mediumWeight = weight.medium;
|
|
2334
|
+
const plotHeight = space.xxl * 3;
|
|
2335
|
+
const dotSize = space.sm;
|
|
2336
|
+
const dotOffset = -(dotSize / 2);
|
|
2337
|
+
const bandInset = space.xs;
|
|
2338
|
+
const softShadow = {
|
|
2339
|
+
shadowColor: color.brand,
|
|
2340
|
+
shadowOffset: { width: 0, height: space.xs },
|
|
2341
|
+
shadowRadius: space.sm,
|
|
2342
|
+
shadowOpacity: 0.08,
|
|
2343
|
+
elevation: space.xs
|
|
2344
|
+
};
|
|
2345
|
+
return StyleSheet.create({
|
|
2346
|
+
card: {
|
|
2347
|
+
gap: space.sm,
|
|
2348
|
+
padding: space.lg,
|
|
2349
|
+
borderRadius: radius.md,
|
|
2350
|
+
backgroundColor: color.surfaceCream,
|
|
2351
|
+
borderWidth: hairline,
|
|
2352
|
+
borderColor: color.brandBorder,
|
|
2353
|
+
...softShadow
|
|
2354
|
+
},
|
|
2355
|
+
eyebrow: {
|
|
2356
|
+
fontFamily: family,
|
|
2357
|
+
fontSize: size.caption,
|
|
2358
|
+
lineHeight: lh(size.caption, lineHeight.normal),
|
|
2359
|
+
color: color.text.muted,
|
|
2360
|
+
letterSpacing: letterSpacing.eyebrow,
|
|
2361
|
+
textTransform: "uppercase"
|
|
2362
|
+
},
|
|
2363
|
+
hero: {
|
|
2364
|
+
fontFamily: family,
|
|
2365
|
+
fontSize: size.verdict,
|
|
2366
|
+
lineHeight: lh(size.verdict, lineHeight.tight),
|
|
2367
|
+
color: color.text.primary,
|
|
2368
|
+
fontWeight: boldWeight,
|
|
2369
|
+
letterSpacing: letterSpacing.tight
|
|
2370
|
+
},
|
|
2371
|
+
plot: {
|
|
2372
|
+
position: "relative",
|
|
2373
|
+
height: plotHeight,
|
|
2374
|
+
marginTop: space.xs
|
|
2375
|
+
},
|
|
2376
|
+
guide: {
|
|
2377
|
+
position: "absolute",
|
|
2378
|
+
top: 0,
|
|
2379
|
+
bottom: 0,
|
|
2380
|
+
width: hairline,
|
|
2381
|
+
backgroundColor: color.borderHairlineGreen
|
|
2382
|
+
},
|
|
2383
|
+
band: {
|
|
2384
|
+
position: "absolute",
|
|
2385
|
+
left: bandInset,
|
|
2386
|
+
right: space.xs,
|
|
2387
|
+
borderRadius: radius.sm,
|
|
2388
|
+
backgroundColor: color.brandSoft
|
|
2389
|
+
},
|
|
2390
|
+
point: {
|
|
2391
|
+
position: "absolute",
|
|
2392
|
+
width: dotSize,
|
|
2393
|
+
height: dotSize,
|
|
2394
|
+
marginLeft: dotOffset,
|
|
2395
|
+
marginTop: dotOffset,
|
|
2396
|
+
borderRadius: radius.pill,
|
|
2397
|
+
backgroundColor: color.brand,
|
|
2398
|
+
borderWidth: hairline,
|
|
2399
|
+
borderColor: color.surfaceCream
|
|
2400
|
+
},
|
|
2401
|
+
pointLatest: {
|
|
2402
|
+
backgroundColor: color.accent,
|
|
2403
|
+
borderColor: color.onAccent
|
|
2404
|
+
},
|
|
2405
|
+
edgeLabel: {
|
|
2406
|
+
position: "absolute",
|
|
2407
|
+
right: 0,
|
|
2408
|
+
marginTop: -space.sm,
|
|
2409
|
+
fontFamily: family,
|
|
2410
|
+
fontSize: size.tiny,
|
|
2411
|
+
lineHeight: lh(size.tiny, lineHeight.tight),
|
|
2412
|
+
color: color.text.muted,
|
|
2413
|
+
fontWeight: mediumWeight
|
|
2414
|
+
},
|
|
2415
|
+
dates: {
|
|
2416
|
+
flexDirection: "row",
|
|
2417
|
+
justifyContent: "space-between"
|
|
2418
|
+
},
|
|
2419
|
+
dateLabel: {
|
|
2420
|
+
fontFamily: family,
|
|
2421
|
+
fontSize: size.tiny,
|
|
2422
|
+
lineHeight: lh(size.tiny, lineHeight.normal),
|
|
2423
|
+
color: color.text.muted
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
var SPINNER_DELAY_MS = 300;
|
|
2428
|
+
function EverfurRecords(props) {
|
|
2429
|
+
const { petRef, controller, onPickDocument, consentVersion, style } = props;
|
|
2430
|
+
return /* @__PURE__ */ jsx(CapabilityGate, { capability: "records", style, children: /* @__PURE__ */ jsx(
|
|
2431
|
+
RecordsBody,
|
|
2432
|
+
{
|
|
2433
|
+
petRef,
|
|
2434
|
+
controller,
|
|
2435
|
+
onPickDocument,
|
|
2436
|
+
consentVersion,
|
|
2437
|
+
style
|
|
2438
|
+
}
|
|
2439
|
+
) });
|
|
2440
|
+
}
|
|
2441
|
+
function RecordsBody({
|
|
2442
|
+
petRef,
|
|
2443
|
+
controller,
|
|
2444
|
+
onPickDocument,
|
|
2445
|
+
consentVersion,
|
|
2446
|
+
style
|
|
2447
|
+
}) {
|
|
2448
|
+
const styles = useStyles3();
|
|
2449
|
+
const recordsOpts = useMemo(
|
|
2450
|
+
() => controller ? { petRef, controller } : { petRef },
|
|
2451
|
+
[petRef, controller]
|
|
2452
|
+
);
|
|
2453
|
+
const records = useEverfurRecords(recordsOpts);
|
|
2454
|
+
const view = records.status;
|
|
2455
|
+
const canUpload = onPickDocument !== void 0;
|
|
2456
|
+
const onUpload = useMemo(
|
|
2457
|
+
() => makeUploadHandler(records, onPickDocument),
|
|
2458
|
+
[records, onPickDocument]
|
|
2459
|
+
);
|
|
2460
|
+
const onGrant = useMemo(
|
|
2461
|
+
() => makeGrantHandler(records, consentVersion),
|
|
2462
|
+
[records, consentVersion]
|
|
2463
|
+
);
|
|
2464
|
+
const canGrant = useMemo(
|
|
2465
|
+
() => (consentVersion ?? records.consent?.version ?? "").length > 0,
|
|
2466
|
+
[consentVersion, records.consent]
|
|
2467
|
+
);
|
|
2468
|
+
return /* @__PURE__ */ jsxs(View, { style: [styles.container, style], children: [
|
|
2469
|
+
/* @__PURE__ */ jsx(View, { style: styles.body, children: renderView(view, records, onUpload, onGrant, canUpload, canGrant) }),
|
|
2470
|
+
/* @__PURE__ */ jsx(ClinicalDisclaimer, {})
|
|
2471
|
+
] });
|
|
2472
|
+
}
|
|
2473
|
+
function renderView(view, records, onUpload, onGrant, canUpload, canGrant) {
|
|
2474
|
+
switch (view) {
|
|
2475
|
+
case "loading":
|
|
2476
|
+
return /* @__PURE__ */ jsx(LoadingState, {});
|
|
2477
|
+
case "empty":
|
|
2478
|
+
return /* @__PURE__ */ jsx(EmptyState, { onUpload, canUpload });
|
|
2479
|
+
case "error":
|
|
2480
|
+
return renderErrorFamily(records, onGrant, canGrant);
|
|
2481
|
+
case "populated":
|
|
2482
|
+
return /* @__PURE__ */ jsx(PopulatedState, { record: records.record, activeRequest: records.activeRequest });
|
|
2483
|
+
default:
|
|
2484
|
+
return assertNever(view);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
function renderErrorFamily(records, onGrant, canGrant) {
|
|
2488
|
+
if (records.denialReason === "consent_required") {
|
|
2489
|
+
return canGrant ? /* @__PURE__ */ jsx(ConsentRequiredState, { onGrant }) : /* @__PURE__ */ jsx(ConsentUnavailableState, {});
|
|
2490
|
+
}
|
|
2491
|
+
if (records.denialReason === "entitlement") {
|
|
2492
|
+
return /* @__PURE__ */ jsx(ErrorState, { error: makeEverfurError("accessDenied"), onRetry: records.refresh });
|
|
2493
|
+
}
|
|
2494
|
+
if (records.denialReason === "session_required") {
|
|
2495
|
+
return /* @__PURE__ */ jsx(SessionRequiredState, {});
|
|
2496
|
+
}
|
|
2497
|
+
if (records.withheld) {
|
|
2498
|
+
return /* @__PURE__ */ jsx(WithheldState, {});
|
|
2499
|
+
}
|
|
2500
|
+
const error = records.error;
|
|
2501
|
+
if (error === null) return /* @__PURE__ */ jsx(WithheldState, {});
|
|
2502
|
+
return /* @__PURE__ */ jsx(ErrorState, { error, onRetry: records.refresh });
|
|
2503
|
+
}
|
|
2504
|
+
function makeUploadHandler(records, onPickDocument) {
|
|
2505
|
+
return () => {
|
|
2506
|
+
if (!onPickDocument) return;
|
|
2507
|
+
void (async () => {
|
|
2508
|
+
const file = await onPickDocument();
|
|
2509
|
+
if (!file) return;
|
|
2510
|
+
const key = `records-upload-${Date.now().toString(36)}`;
|
|
2511
|
+
const created = await records.createRequest(key);
|
|
2512
|
+
if (!created.ok) return;
|
|
2513
|
+
await records.uploadDocument(created.value.id, file);
|
|
2514
|
+
})();
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
function makeGrantHandler(records, consentVersion) {
|
|
2518
|
+
return () => {
|
|
2519
|
+
const version = consentVersion ?? records.consent?.version ?? "";
|
|
2520
|
+
if (version.length === 0) return;
|
|
2521
|
+
const key = `records-consent-${Date.now().toString(36)}`;
|
|
2522
|
+
void records.grantConsent(version, key);
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
function LoadingState() {
|
|
2526
|
+
const styles = useStyles3();
|
|
2527
|
+
const [visible, setVisible] = useState(false);
|
|
2528
|
+
useEffect(() => {
|
|
2529
|
+
const id = setTimeout(() => setVisible(true), SPINNER_DELAY_MS);
|
|
2530
|
+
return () => clearTimeout(id);
|
|
2531
|
+
}, []);
|
|
2532
|
+
return /* @__PURE__ */ jsx(View, { style: styles.centered, accessible: true, accessibilityState: { busy: true }, accessibilityLabel: "Loading", children: visible ? /* @__PURE__ */ jsx(ActivityIndicator, {}) : null });
|
|
2533
|
+
}
|
|
2534
|
+
function EmptyState({ onUpload, canUpload }) {
|
|
2535
|
+
const styles = useStyles3();
|
|
2536
|
+
return /* @__PURE__ */ jsxs(View, { style: styles.centered, accessible: true, accessibilityRole: "summary", accessibilityLabel: "No records yet", children: [
|
|
2537
|
+
/* @__PURE__ */ jsx(View, { style: styles.emptyGlyph }),
|
|
2538
|
+
/* @__PURE__ */ jsx(
|
|
2539
|
+
Pressable,
|
|
2540
|
+
{
|
|
2541
|
+
onPress: canUpload ? onUpload : void 0,
|
|
2542
|
+
disabled: !canUpload,
|
|
2543
|
+
accessibilityRole: "button",
|
|
2544
|
+
accessibilityLabel: "Upload records",
|
|
2545
|
+
accessibilityState: { disabled: !canUpload },
|
|
2546
|
+
style: canUpload ? styles.primaryButton : styles.disabledButton,
|
|
2547
|
+
children: /* @__PURE__ */ jsx(Text, { style: canUpload ? styles.primaryLabel : styles.disabledLabel, children: "Upload records" })
|
|
2548
|
+
}
|
|
2549
|
+
)
|
|
2550
|
+
] });
|
|
2551
|
+
}
|
|
2552
|
+
function ConsentRequiredState({ onGrant }) {
|
|
2553
|
+
const styles = useStyles3();
|
|
2554
|
+
return /* @__PURE__ */ jsx(View, { style: styles.centered, accessible: true, accessibilityRole: "alert", accessibilityLabel: "Consent required", children: /* @__PURE__ */ jsx(
|
|
2555
|
+
Pressable,
|
|
2556
|
+
{
|
|
2557
|
+
onPress: onGrant,
|
|
2558
|
+
accessibilityRole: "button",
|
|
2559
|
+
accessibilityLabel: "Allow",
|
|
2560
|
+
style: styles.primaryButton,
|
|
2561
|
+
children: /* @__PURE__ */ jsx(Text, { style: styles.primaryLabel, children: "Allow" })
|
|
2562
|
+
}
|
|
2563
|
+
) });
|
|
2564
|
+
}
|
|
2565
|
+
function AlertState({ label, glyph }) {
|
|
2566
|
+
const styles = useStyles3();
|
|
2567
|
+
useEffect(() => {
|
|
2568
|
+
AccessibilityInfo.announceForAccessibility?.(label);
|
|
2569
|
+
}, [label]);
|
|
2570
|
+
return /* @__PURE__ */ jsxs(
|
|
2571
|
+
View,
|
|
2572
|
+
{
|
|
2573
|
+
style: styles.centered,
|
|
2574
|
+
accessible: true,
|
|
2575
|
+
accessibilityRole: "alert",
|
|
2576
|
+
accessibilityLiveRegion: "assertive",
|
|
2577
|
+
accessibilityLabel: label,
|
|
2578
|
+
children: [
|
|
2579
|
+
/* @__PURE__ */ jsx(View, { style: glyph === "withheld" ? styles.withheldGlyph : styles.emptyGlyph }),
|
|
2580
|
+
/* @__PURE__ */ jsx(Text, { style: styles.errorText, children: label })
|
|
2581
|
+
]
|
|
2582
|
+
}
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
function ConsentUnavailableState() {
|
|
2586
|
+
return /* @__PURE__ */ jsx(AlertState, { label: "Consent required", glyph: "empty" });
|
|
2587
|
+
}
|
|
2588
|
+
function SessionRequiredState() {
|
|
2589
|
+
return /* @__PURE__ */ jsx(AlertState, { label: "Sign in required", glyph: "empty" });
|
|
2590
|
+
}
|
|
2591
|
+
function WithheldState() {
|
|
2592
|
+
return /* @__PURE__ */ jsx(AlertState, { label: "Record unavailable", glyph: "withheld" });
|
|
2593
|
+
}
|
|
2594
|
+
function ErrorState({ error, onRetry }) {
|
|
2595
|
+
const styles = useStyles3();
|
|
2596
|
+
useEffect(() => {
|
|
2597
|
+
AccessibilityInfo.announceForAccessibility?.(error.displayMessage);
|
|
2598
|
+
}, [error.displayMessage]);
|
|
2599
|
+
return /* @__PURE__ */ jsxs(View, { style: styles.centered, accessible: true, accessibilityRole: "alert", accessibilityLiveRegion: "assertive", children: [
|
|
2600
|
+
/* @__PURE__ */ jsx(Text, { style: styles.errorText, children: error.displayMessage }),
|
|
2601
|
+
error.retryable ? /* @__PURE__ */ jsx(Pressable, { onPress: onRetry, accessibilityRole: "button", accessibilityLabel: "Try again", style: styles.primaryButton, children: /* @__PURE__ */ jsx(Text, { style: styles.primaryLabel, children: "Try again" }) }) : null
|
|
2602
|
+
] });
|
|
2603
|
+
}
|
|
2604
|
+
function PopulatedState({
|
|
2605
|
+
record,
|
|
2606
|
+
activeRequest
|
|
2607
|
+
}) {
|
|
2608
|
+
const styles = useStyles3();
|
|
2609
|
+
const hasTimeline = activeRequest !== null && (activeRequest.statusLabel.length > 0 || activeRequest.heldItemCount > 0);
|
|
2610
|
+
const hasRecord = record !== null && !record.isEmpty;
|
|
2611
|
+
if (!hasTimeline && !hasRecord) return /* @__PURE__ */ jsx(LoadingState, {});
|
|
2612
|
+
return /* @__PURE__ */ jsxs(ScrollView, { style: styles.list, contentContainerStyle: styles.listContent, accessibilityLabel: "Records", children: [
|
|
2613
|
+
hasTimeline && activeRequest ? /* @__PURE__ */ jsx(RequestTimeline, { request: activeRequest }) : null,
|
|
2614
|
+
record && !record.isEmpty ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2615
|
+
/* @__PURE__ */ jsx(TrendRangeCard, { series: record.entitySeries, testID: "records-trend-card" }),
|
|
2616
|
+
/* @__PURE__ */ jsx(
|
|
2617
|
+
RecordNavCards,
|
|
2618
|
+
{
|
|
2619
|
+
entities: record.entities,
|
|
2620
|
+
forwardLooking: record.forwardLooking,
|
|
2621
|
+
documents: record.documents,
|
|
2622
|
+
testID: "records-nav-cards"
|
|
2623
|
+
}
|
|
2624
|
+
),
|
|
2625
|
+
/* @__PURE__ */ jsx(RecordDetails, { record })
|
|
2626
|
+
] }) : null
|
|
2627
|
+
] });
|
|
2628
|
+
}
|
|
2629
|
+
function RequestTimeline({ request }) {
|
|
2630
|
+
const styles = useStyles3();
|
|
2631
|
+
const hasLabel = request.statusLabel.length > 0;
|
|
2632
|
+
const hasHeld = request.heldItemCount > 0;
|
|
2633
|
+
if (!hasLabel && !hasHeld) return null;
|
|
2634
|
+
return /* @__PURE__ */ jsxs(
|
|
2635
|
+
View,
|
|
2636
|
+
{
|
|
2637
|
+
style: styles.timeline,
|
|
2638
|
+
accessible: true,
|
|
2639
|
+
accessibilityRole: "text",
|
|
2640
|
+
...hasLabel ? { accessibilityLabel: request.statusLabel } : {},
|
|
2641
|
+
children: [
|
|
2642
|
+
hasLabel ? /* @__PURE__ */ jsx(Text, { style: styles.timelineLabel, children: request.statusLabel }) : null,
|
|
2643
|
+
hasHeld ? /* @__PURE__ */ jsx(Text, { style: styles.timelineMeta, children: `${request.heldItemCount} being reviewed` }) : null
|
|
2644
|
+
]
|
|
2645
|
+
}
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
function RecordDetails({ record }) {
|
|
2649
|
+
const styles = useStyles3();
|
|
2650
|
+
const uncategorized = record.entities.filter((entity) => entity.category.trim().length === 0);
|
|
2651
|
+
const hasDetails = record.identity.length > 0 || uncategorized.length > 0;
|
|
2652
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2653
|
+
hasDetails ? /* @__PURE__ */ jsxs(View, { style: styles.recordBlock, children: [
|
|
2654
|
+
record.identity.map((field) => /* @__PURE__ */ jsxs(View, { style: styles.row, children: [
|
|
2655
|
+
/* @__PURE__ */ jsx(Text, { style: styles.rowLabel, children: field.label }),
|
|
2656
|
+
/* @__PURE__ */ jsx(Text, { style: styles.rowValue, children: field.value })
|
|
2657
|
+
] }, field.label)),
|
|
2658
|
+
uncategorized.map((entity) => /* @__PURE__ */ jsxs(View, { style: styles.row, children: [
|
|
2659
|
+
/* @__PURE__ */ jsx(Text, { style: styles.rowLabel, children: entity.label }),
|
|
2660
|
+
/* @__PURE__ */ jsx(Text, { style: styles.rowValue, children: formatEntityValue(entity.value, entity.unit) })
|
|
2661
|
+
] }, entity.key))
|
|
2662
|
+
] }) : null,
|
|
2663
|
+
record.disclaimer.length > 0 ? /* @__PURE__ */ jsx(Text, { style: styles.disclaimer, children: record.disclaimer }) : null
|
|
2664
|
+
] });
|
|
2665
|
+
}
|
|
2666
|
+
function useStyles3() {
|
|
2667
|
+
const theme = useEverfurTheme();
|
|
2668
|
+
return useMemo(() => makeStyles5(theme), [theme]);
|
|
2669
|
+
}
|
|
2670
|
+
function makeStyles5(theme) {
|
|
2671
|
+
const { color, space, radius, type } = theme;
|
|
2672
|
+
const { size, lineHeight, weight, letterSpacing, family } = type;
|
|
2673
|
+
const lh = (fontSize, multiplier) => Math.round(fontSize * multiplier);
|
|
2674
|
+
const hairline = StyleSheet.hairlineWidth;
|
|
2675
|
+
const boldWeight = weight.bold;
|
|
2676
|
+
const mediumWeight = weight.medium;
|
|
2677
|
+
const minSurface = space.xxl * 3 + space.xl;
|
|
2678
|
+
const touchTarget = space.xxl + space.md;
|
|
2679
|
+
const glyphSize = space.xxl + space.sm;
|
|
2680
|
+
const errorColor = theme.brightness === "dark" ? color.errorRed : color.danger;
|
|
2681
|
+
const softShadow = {
|
|
2682
|
+
shadowColor: color.brand,
|
|
2683
|
+
shadowOffset: { width: 0, height: space.xs },
|
|
2684
|
+
shadowRadius: space.sm,
|
|
2685
|
+
shadowOpacity: 0.08,
|
|
2686
|
+
elevation: space.xs
|
|
2687
|
+
};
|
|
2688
|
+
return StyleSheet.create({
|
|
2689
|
+
container: { flex: 1, minHeight: minSurface, backgroundColor: color.background },
|
|
2690
|
+
body: { flex: 1 },
|
|
2691
|
+
centered: {
|
|
2692
|
+
minHeight: minSurface,
|
|
2693
|
+
flexGrow: 1,
|
|
2694
|
+
alignItems: "center",
|
|
2695
|
+
justifyContent: "center",
|
|
2696
|
+
padding: space.xl,
|
|
2697
|
+
gap: space.lg
|
|
2698
|
+
},
|
|
2699
|
+
// Branded empty placeholder: a soft-brand disc ringed in the hairline brand border.
|
|
2700
|
+
emptyGlyph: {
|
|
2701
|
+
width: glyphSize,
|
|
2702
|
+
height: glyphSize,
|
|
2703
|
+
borderRadius: radius.lg,
|
|
2704
|
+
backgroundColor: color.brandSoft,
|
|
2705
|
+
borderWidth: hairline,
|
|
2706
|
+
borderColor: color.brandBorder
|
|
2707
|
+
},
|
|
2708
|
+
// Withheld / fail-closed glyph: a caution wash ringed in the caution accent.
|
|
2709
|
+
withheldGlyph: {
|
|
2710
|
+
width: glyphSize,
|
|
2711
|
+
height: glyphSize,
|
|
2712
|
+
borderRadius: radius.sm,
|
|
2713
|
+
backgroundColor: color.badgeBgCaution,
|
|
2714
|
+
borderWidth: hairline,
|
|
2715
|
+
borderColor: color.cautionAccent
|
|
2716
|
+
},
|
|
2717
|
+
errorText: {
|
|
2718
|
+
fontFamily: family,
|
|
2719
|
+
fontSize: size.subtitle,
|
|
2720
|
+
lineHeight: lh(size.subtitle, lineHeight.snug),
|
|
2721
|
+
color: errorColor,
|
|
2722
|
+
textAlign: "center",
|
|
2723
|
+
marginBottom: space.md
|
|
2724
|
+
},
|
|
2725
|
+
// Primary CTA: brand fill, WCAG onPrimary foreground, pill radius, soft brand shadow.
|
|
2726
|
+
primaryButton: {
|
|
2727
|
+
minHeight: touchTarget,
|
|
2728
|
+
minWidth: touchTarget,
|
|
2729
|
+
alignItems: "center",
|
|
2730
|
+
justifyContent: "center",
|
|
2731
|
+
paddingHorizontal: space.xl,
|
|
2732
|
+
paddingVertical: space.md,
|
|
2733
|
+
borderRadius: radius.pill,
|
|
2734
|
+
backgroundColor: color.primary,
|
|
2735
|
+
...softShadow
|
|
2736
|
+
},
|
|
2737
|
+
primaryLabel: {
|
|
2738
|
+
fontFamily: family,
|
|
2739
|
+
color: color.onPrimary,
|
|
2740
|
+
fontSize: size.body,
|
|
2741
|
+
fontWeight: boldWeight,
|
|
2742
|
+
letterSpacing: letterSpacing.tight
|
|
2743
|
+
},
|
|
2744
|
+
disabledButton: {
|
|
2745
|
+
minHeight: touchTarget,
|
|
2746
|
+
minWidth: touchTarget,
|
|
2747
|
+
alignItems: "center",
|
|
2748
|
+
justifyContent: "center",
|
|
2749
|
+
paddingHorizontal: space.xl,
|
|
2750
|
+
paddingVertical: space.md,
|
|
2751
|
+
borderRadius: radius.pill,
|
|
2752
|
+
backgroundColor: color.surfaceSubtle,
|
|
2753
|
+
borderWidth: hairline,
|
|
2754
|
+
borderColor: color.brandBorder
|
|
2755
|
+
},
|
|
2756
|
+
disabledLabel: {
|
|
2757
|
+
fontFamily: family,
|
|
2758
|
+
color: color.text.muted,
|
|
2759
|
+
fontSize: size.body,
|
|
2760
|
+
fontWeight: boldWeight,
|
|
2761
|
+
letterSpacing: letterSpacing.tight
|
|
2762
|
+
},
|
|
2763
|
+
list: { flex: 1 },
|
|
2764
|
+
listContent: { padding: space.lg, gap: space.md },
|
|
2765
|
+
// Request timeline: a cream card with the brand hairline border + soft depth.
|
|
2766
|
+
timeline: {
|
|
2767
|
+
paddingVertical: space.md,
|
|
2768
|
+
paddingHorizontal: space.lg,
|
|
2769
|
+
borderRadius: radius.md,
|
|
2770
|
+
backgroundColor: color.surfaceCream,
|
|
2771
|
+
borderWidth: hairline,
|
|
2772
|
+
borderColor: color.brandBorder,
|
|
2773
|
+
gap: space.xs,
|
|
2774
|
+
...softShadow
|
|
2775
|
+
},
|
|
2776
|
+
timelineLabel: {
|
|
2777
|
+
fontFamily: family,
|
|
2778
|
+
fontSize: size.subtitle,
|
|
2779
|
+
lineHeight: lh(size.subtitle, lineHeight.snug),
|
|
2780
|
+
color: color.text.secondary,
|
|
2781
|
+
fontWeight: boldWeight,
|
|
2782
|
+
letterSpacing: letterSpacing.tight
|
|
2783
|
+
},
|
|
2784
|
+
timelineMeta: {
|
|
2785
|
+
fontFamily: family,
|
|
2786
|
+
fontSize: size.bodyTight,
|
|
2787
|
+
lineHeight: lh(size.bodyTight, lineHeight.normal),
|
|
2788
|
+
color: color.text.muted
|
|
2789
|
+
},
|
|
2790
|
+
// Parsed record: a cream card of hairline-divided label/value rows.
|
|
2791
|
+
recordBlock: {
|
|
2792
|
+
gap: space.sm,
|
|
2793
|
+
padding: space.md,
|
|
2794
|
+
borderRadius: radius.md,
|
|
2795
|
+
backgroundColor: color.surfaceCream,
|
|
2796
|
+
borderWidth: hairline,
|
|
2797
|
+
borderColor: color.brandBorder,
|
|
2798
|
+
...softShadow
|
|
2799
|
+
},
|
|
2800
|
+
row: {
|
|
2801
|
+
flexDirection: "row",
|
|
2802
|
+
justifyContent: "space-between",
|
|
2803
|
+
gap: space.md,
|
|
2804
|
+
paddingVertical: space.xs,
|
|
2805
|
+
borderBottomWidth: hairline,
|
|
2806
|
+
borderBottomColor: color.borderHairlineGreen
|
|
2807
|
+
},
|
|
2808
|
+
rowLabel: {
|
|
2809
|
+
fontFamily: family,
|
|
2810
|
+
fontSize: size.body,
|
|
2811
|
+
lineHeight: lh(size.body, lineHeight.normal),
|
|
2812
|
+
color: color.text.muted,
|
|
2813
|
+
flexShrink: 1
|
|
2814
|
+
},
|
|
2815
|
+
rowValue: {
|
|
2816
|
+
fontFamily: family,
|
|
2817
|
+
fontSize: size.body,
|
|
2818
|
+
lineHeight: lh(size.body, lineHeight.normal),
|
|
2819
|
+
color: color.text.primary,
|
|
2820
|
+
fontWeight: mediumWeight,
|
|
2821
|
+
textAlign: "right",
|
|
2822
|
+
flexShrink: 1
|
|
2823
|
+
},
|
|
2824
|
+
disclaimer: {
|
|
2825
|
+
fontFamily: family,
|
|
2826
|
+
fontSize: size.caption,
|
|
2827
|
+
lineHeight: lh(size.caption, lineHeight.normal),
|
|
2828
|
+
color: color.text.muted,
|
|
2829
|
+
marginTop: space.md
|
|
2830
|
+
}
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
export { EverfurRecords, RECORDS_TERMINAL, isTerminalStatus, recordsDenialReasonOf, useEverfurRecords };
|