@teincfood/core 0.8.0 → 0.8.1

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.
@@ -16,5 +16,9 @@ export declare const CACHED_IMAGE_KEYS: {
16
16
  };
17
17
  /** Invalidate every cached-image query (e.g. after a bulk reference sync). */
18
18
  export declare function invalidateCachedImages(queryClient: ReturnType<typeof useQueryClient>): void;
19
- /** Resolve an image URL to a URI. Prefers local cache; null for null URL. */
19
+ /** Resolve an image URL to a URI safe for RN `<Image>`. Prefers local cache;
20
+ * null for null URL AND for URIs no native loader can handle (tfimg://
21
+ * leaks, bare S3 keys, blob:) — callers render placeholders instead of
22
+ * crashing the screen. resolveImageSource passthrough is preserved for
23
+ * non-render consumers; this gate applies at the render boundary only. */
20
24
  export declare function useCachedImageUrl(url: string | null | undefined): string | null;
@@ -8,7 +8,7 @@
8
8
  import { useQuery, useQueryClient } from "@tanstack/react-query";
9
9
  import { useSyncExternalStore } from "react";
10
10
  import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
11
- import { cacheRemoteImage, getCachedImageInfo, resolveImageSource, stableCacheUrl } from "../image-cache/service";
11
+ import { cacheRemoteImage, getCachedImageInfo, resolveImageSource, stableCacheUrl, toRenderableUri } from "../image-cache/service";
12
12
  export const IMAGE_CACHE_KEYS = {
13
13
  all: ["cachedImage"],
14
14
  url: (url) => ["cachedImage", url],
@@ -18,7 +18,11 @@ export const CACHED_IMAGE_KEYS = IMAGE_CACHE_KEYS;
18
18
  export function invalidateCachedImages(queryClient) {
19
19
  queryClient.invalidateQueries({ queryKey: IMAGE_CACHE_KEYS.all });
20
20
  }
21
- /** Resolve an image URL to a URI. Prefers local cache; null for null URL. */
21
+ /** Resolve an image URL to a URI safe for RN `<Image>`. Prefers local cache;
22
+ * null for null URL AND for URIs no native loader can handle (tfimg://
23
+ * leaks, bare S3 keys, blob:) — callers render placeholders instead of
24
+ * crashing the screen. resolveImageSource passthrough is preserved for
25
+ * non-render consumers; this gate applies at the render boundary only. */
22
26
  export function useCachedImageUrl(url) {
23
27
  const queryClient = useQueryClient();
24
28
  const connectivity = useSyncExternalStore(subscribeConnectivity, getConnectivityState, getConnectivityState);
@@ -58,5 +62,5 @@ export function useCachedImageUrl(url) {
58
62
  });
59
63
  if (!enabled || !data)
60
64
  return null;
61
- return data.uri;
65
+ return toRenderableUri(data.uri);
62
66
  }
@@ -22,6 +22,26 @@ export declare function getCachedImageInfo(url: string): Promise<ImageCacheEntry
22
22
  * hook's warm-then-invalidate cycle into an unbounded retry storm.
23
23
  */
24
24
  export declare function isDownloadableImageUrl(url: string): boolean;
25
+ /**
26
+ * URI schemes React Native's `<Image>` can actually load on device. Anything
27
+ * else has no RCTImageURLLoader on iOS ("No suitable image URL loader
28
+ * found") and crashes the screen — notably `tfimg://` disk URIs leaked from
29
+ * desktop rows, bare S3 keys (`menus/biz/img.jpg`), and `blob:` preview
30
+ * URLs. The hook maps these to null so callers render their placeholder
31
+ * branch instead of feeding a fatal URI to the native loader.
32
+ */
33
+ export declare function isLoadableImageUri(uri: string): boolean;
34
+ /** Map a resolved URI to one RN `<Image>` can load, else null (placeholder). */
35
+ export declare function toRenderableUri(uri: string | null | undefined): string | null;
36
+ /**
37
+ * Values safe to persist as a menu image reference: S3 keys, http(s) URLs,
38
+ * null. Preview URIs (tfimg:, blob:, file:, data:) must never be stored —
39
+ * the backend keeps non-http values verbatim and they poison every other
40
+ * device (mobile has no native loader for tfimg:// and crashes). Returns
41
+ * null for un-storable values, so re-saving a poisoned row self-heals it
42
+ * (caller re-uploads to restore the image).
43
+ */
44
+ export declare function toStorableImageUrl(value: string | null | undefined): string | null;
25
45
  export declare function cacheRemoteImage(url: string | null | undefined, opts?: {
26
46
  skipIfCached?: boolean;
27
47
  removeOnFailure?: boolean;
@@ -60,6 +60,42 @@ const DOWNLOADABLE_RE = /^https?:\/\//i;
60
60
  export function isDownloadableImageUrl(url) {
61
61
  return DOWNLOADABLE_RE.test(url);
62
62
  }
63
+ const LOADABLE_RE = /^(https?|file|data|asset|content):/i;
64
+ /**
65
+ * URI schemes React Native's `<Image>` can actually load on device. Anything
66
+ * else has no RCTImageURLLoader on iOS ("No suitable image URL loader
67
+ * found") and crashes the screen — notably `tfimg://` disk URIs leaked from
68
+ * desktop rows, bare S3 keys (`menus/biz/img.jpg`), and `blob:` preview
69
+ * URLs. The hook maps these to null so callers render their placeholder
70
+ * branch instead of feeding a fatal URI to the native loader.
71
+ */
72
+ export function isLoadableImageUri(uri) {
73
+ return LOADABLE_RE.test(uri);
74
+ }
75
+ /** Map a resolved URI to one RN `<Image>` can load, else null (placeholder). */
76
+ export function toRenderableUri(uri) {
77
+ if (!uri)
78
+ return null;
79
+ return isLoadableImageUri(uri) ? uri : null;
80
+ }
81
+ const URI_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
82
+ /**
83
+ * Values safe to persist as a menu image reference: S3 keys, http(s) URLs,
84
+ * null. Preview URIs (tfimg:, blob:, file:, data:) must never be stored —
85
+ * the backend keeps non-http values verbatim and they poison every other
86
+ * device (mobile has no native loader for tfimg:// and crashes). Returns
87
+ * null for un-storable values, so re-saving a poisoned row self-heals it
88
+ * (caller re-uploads to restore the image).
89
+ */
90
+ export function toStorableImageUrl(value) {
91
+ if (!value)
92
+ return null;
93
+ if (URI_SCHEME_RE.test(value) && !DOWNLOADABLE_RE.test(value)) {
94
+ syncLogger.warn("ImageCache", "Dropping un-storable image URL", { value: value.slice(0, 32) });
95
+ return null;
96
+ }
97
+ return value;
98
+ }
63
99
  export async function cacheRemoteImage(url, opts = {}) {
64
100
  if (!url)
65
101
  return null;
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { generateUUID } from "../utils/uuid";
11
11
  import { CATALOG_KEYS } from "../catalog/hooks";
12
+ import { toStorableImageUrl } from "../image-cache/service";
12
13
  export function buildMenuItemRecord(input) {
13
14
  const currencyCode = input.currencyCode || "GHS";
14
15
  const now = input.now ?? new Date().toISOString();
@@ -17,7 +18,9 @@ export function buildMenuItemRecord(input) {
17
18
  name: input.name,
18
19
  description: input.description ?? null,
19
20
  basePrice: { amountMinor: input.priceMinor, currencyCode },
20
- image_url: input.imageUrl ?? null,
21
+ // Never persist preview URIs (tfimg:/blob:/file:): the backend stores
22
+ // non-http values verbatim and they poison every other device.
23
+ image_url: toStorableImageUrl(input.imageUrl),
21
24
  is_available: input.available ?? true,
22
25
  dietary_tags: input.dietaryTags ?? [],
23
26
  category_id: input.categoryId,
@@ -152,7 +155,9 @@ export function buildMenuItemEnvelope(input) {
152
155
  : { name: e.name, price_minor: e.price_minor ?? 0 };
153
156
  }),
154
157
  available: input.available ?? true,
155
- image_url: input.imageUrl ?? null,
158
+ // Same storable rule as the cache record: preview URIs must not reach
159
+ // the backend (it stores them verbatim).
160
+ image_url: toStorableImageUrl(input.imageUrl),
156
161
  ...(input.wireExtra ?? {}),
157
162
  };
158
163
  return { wire, cacheRecord };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "TeincFood shared offline-first core — types, sync engine, local DB, reference data, and repositories for mobile + desktop",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",