feat(api): route all image uploads through mana-media for CAS, thumbnails & Photos gallery

Picture, Contacts, Planta, Storage, and NutriPhi image uploads now go
through mana-media instead of directly to S3. This enables SHA-256
deduplication, automatic thumbnail generation, EXIF extraction, and
makes all images visible in the Photos gallery. Non-image files (PDFs,
audio, docs) continue to use shared-storage directly. SVG avatars in
Contacts also stay on shared-storage since Sharp can't process SVGs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-04 10:38:30 +02:00
parent 46dae20fa3
commit 502813f49c
10 changed files with 238 additions and 213 deletions

View file

@ -25,6 +25,8 @@ PUBLIC_GLITCHTIP_DSN=
MANA_CORE_AUTH_URL=http://localhost:3001
# Mana Credits Service
MANA_CREDITS_URL=http://localhost:3061
# Mana Media Service (CAS, thumbnails, Photos gallery)
MANA_MEDIA_URL=http://localhost:3015
# Service key for service-to-service communication
MANA_CORE_SERVICE_KEY=dev-service-key-for-bot-sso-2024

View file

@ -10,6 +10,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@manacore/media-client": "workspace:*",
"@manacore/shared-hono": "workspace:*",
"@manacore/shared-storage": "workspace:*",
"@mozilla/readability": "^0.5.0",

40
apps/api/src/lib/media.ts Normal file
View file

@ -0,0 +1,40 @@
/**
* Shared media helper routes image uploads through mana-media
* for CAS deduplication, thumbnail generation, and Photos gallery visibility.
*/
import { MediaClient, type MediaResult } from '@manacore/media-client';
const MEDIA_URL = process.env.MANA_MEDIA_URL || 'http://localhost:3015';
let client: MediaClient | null = null;
function getMediaClient(): MediaClient {
if (!client) client = new MediaClient(MEDIA_URL);
return client;
}
export async function uploadImageToMedia(
buffer: ArrayBuffer,
filename: string,
options: { app: string; userId: string }
): Promise<MediaResult> {
return getMediaClient().upload(buffer, {
app: options.app,
userId: options.userId,
filename,
});
}
export function getMediaUrls(mediaId: string) {
const c = getMediaClient();
return {
original: c.getOriginalUrl(mediaId),
thumbnail: c.getThumbnailUrl(mediaId),
medium: c.getMediumUrl(mediaId),
large: c.getLargeUrl(mediaId),
};
}
export function isImageMimeType(mimeType: string): boolean {
return mimeType.startsWith('image/') && mimeType !== 'image/svg+xml';
}

View file

@ -29,22 +29,37 @@ routes.post('/:id/avatar', async (c) => {
}
try {
const { createContactsStorage, generateUserFileKey, getContentType } = await import(
'@manacore/shared-storage'
);
const storage = createContactsStorage();
const key = generateUserFileKey(
userId,
`avatar-${c.req.param('id')}.${file.name.split('.').pop()}`
);
const buffer = Buffer.from(await file.arrayBuffer());
const buffer = await file.arrayBuffer();
const result = await storage.upload(key, buffer, {
contentType: getContentType(file.name),
public: true,
});
if (file.type === 'image/svg+xml') {
// SVGs stay on shared-storage (Sharp can't process SVG)
const { createContactsStorage, generateUserFileKey } = await import(
'@manacore/shared-storage'
);
const storage = createContactsStorage();
const key = generateUserFileKey(userId, `avatar-${c.req.param('id')}.svg`);
const result = await storage.upload(key, Buffer.from(buffer), {
contentType: 'image/svg+xml',
public: true,
});
return c.json({ avatarUrl: result.url }, 201);
}
return c.json({ avatarUrl: result.url }, 201);
// Raster images -> mana-media for dedup, thumbnails & Photos gallery
const { uploadImageToMedia } = await import('../../lib/media');
const result = await uploadImageToMedia(
buffer,
`avatar-${c.req.param('id')}.${file.name.split('.').pop()}`,
{ app: 'contacts', userId }
);
return c.json(
{
avatarUrl: result.urls.thumbnail || result.urls.original,
mediaId: result.id,
},
201
);
} catch {
return c.json({ error: 'Upload failed' }, 500);
}

View file

@ -25,11 +25,15 @@ const routes = new Hono();
// ─── Photo Analysis (server-only: Gemini Vision) ────────────
routes.post('/analysis/photo', async (c) => {
const userId = c.get('userId');
const { imageBase64, mimeType } = await c.req.json();
if (!imageBase64) return c.json({ error: 'imageBase64 required' }, 400);
const mime = mimeType || 'image/jpeg';
try {
const res = await fetch(`${LLM_URL}/api/v1/chat/completions`, {
// Run AI analysis and mana-media upload in parallel
const analysisPromise = fetch(`${LLM_URL}/api/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@ -41,7 +45,7 @@ routes.post('/analysis/photo', async (c) => {
{ type: 'text', text: 'Analysiere diese Mahlzeit.' },
{
type: 'image_url',
image_url: { url: `data:${mimeType || 'image/jpeg'};base64,${imageBase64}` },
image_url: { url: `data:${mime};base64,${imageBase64}` },
},
],
},
@ -52,12 +56,29 @@ routes.post('/analysis/photo', async (c) => {
}),
});
// Store meal photo in mana-media for Photos gallery & persistence
const ext = mime.split('/')[1] || 'jpg';
const { uploadImageToMedia } = await import('../../lib/media');
const buffer = Uint8Array.from(atob(imageBase64), (ch) => ch.charCodeAt(0));
const mediaPromise = uploadImageToMedia(buffer.buffer, `meal-${Date.now()}.${ext}`, {
app: 'nutriphi',
userId,
}).catch(() => null); // Don't fail analysis if media upload fails
const [res, mediaResult] = await Promise.all([analysisPromise, mediaPromise]);
if (!res.ok) return c.json({ error: 'AI analysis failed' }, 502);
const data = await res.json();
const content = data.choices?.[0]?.message?.content;
const analysis = typeof content === 'string' ? JSON.parse(content) : content;
// Attach media info so the frontend can store photoMediaId on the meal
if (mediaResult) {
analysis.mediaId = mediaResult.id;
analysis.photoUrl = mediaResult.urls.thumbnail || mediaResult.urls.original;
}
return c.json(analysis);
} catch (err) {
console.error('Photo analysis failed:', err);

View file

@ -98,7 +98,27 @@ routes.post('/generate', async (c) => {
await consumeCredits(userId, 'AI_IMAGE_GENERATION', cost, `Image: ${prompt.slice(0, 50)}`);
return c.json({ imageUrl, prompt, model: model || 'flux-schnell' });
// Store generated image in mana-media for dedup, thumbnails & Photos gallery
try {
const { uploadImageToMedia } = await import('../../lib/media');
const imgRes = await fetch(imageUrl);
const imgBuffer = await imgRes.arrayBuffer();
const media = await uploadImageToMedia(imgBuffer, `generated-${Date.now()}.png`, {
app: 'picture',
userId,
});
return c.json({
imageUrl: media.urls.original,
mediaId: media.id,
thumbnailUrl: media.urls.thumbnail,
prompt,
model: model || 'flux-schnell',
});
} catch {
// Fallback: return raw imageUrl if mana-media is unavailable
return c.json({ imageUrl, prompt, model: model || 'flux-schnell' });
}
} catch (_err) {
return c.json({ error: 'Generation failed' }, 500);
}
@ -115,19 +135,19 @@ routes.post('/upload', async (c) => {
if (file.size > 10 * 1024 * 1024) return c.json({ error: 'Max 10MB' }, 400);
try {
const { createPictureStorage, generateUserFileKey, getContentType } = await import(
'@manacore/shared-storage'
const { uploadImageToMedia } = await import('../../lib/media');
const buffer = await file.arrayBuffer();
const result = await uploadImageToMedia(buffer, file.name, { app: 'picture', userId });
return c.json(
{
storagePath: result.id,
publicUrl: result.urls.original,
mediaId: result.id,
thumbnailUrl: result.urls.thumbnail,
},
201
);
const storage = createPictureStorage();
const key = generateUserFileKey(userId, file.name);
const buffer = Buffer.from(await file.arrayBuffer());
const result = await storage.upload(key, buffer, {
contentType: getContentType(file.name),
public: true,
});
return c.json({ storagePath: key, publicUrl: result.url }, 201);
} catch (_err) {
return c.json({ error: 'Upload failed' }, 500);
}

View file

@ -24,19 +24,19 @@ routes.post('/photos/upload', async (c) => {
if (file.size > 10 * 1024 * 1024) return c.json({ error: 'File too large (max 10MB)' }, 400);
try {
const { createPlantaStorage, generateUserFileKey, getContentType } = await import(
'@manacore/shared-storage'
const { uploadImageToMedia } = await import('../../lib/media');
const buffer = await file.arrayBuffer();
const result = await uploadImageToMedia(buffer, file.name, { app: 'planta', userId });
return c.json(
{
storagePath: result.id,
publicUrl: result.urls.original,
mediaId: result.id,
plantId,
},
201
);
const storage = createPlantaStorage();
const key = generateUserFileKey(userId, file.name);
const buffer = Buffer.from(await file.arrayBuffer());
const result = await storage.upload(key, buffer, {
contentType: getContentType(file.name),
public: true,
});
return c.json({ storagePath: key, publicUrl: result.url, plantId }, 201);
} catch (err) {
console.error('Upload failed:', err);
return c.json({ error: 'Upload failed' }, 500);

View file

@ -22,14 +22,37 @@ routes.post('/files/upload', async (c) => {
if (file.size > 100 * 1024 * 1024) return c.json({ error: 'Max 100MB' }, 400);
try {
const buffer = await file.arrayBuffer();
const { isImageMimeType } = await import('../../lib/media');
// Images -> mana-media for dedup, thumbnails & Photos gallery
if (isImageMimeType(file.type)) {
const { uploadImageToMedia } = await import('../../lib/media');
const result = await uploadImageToMedia(buffer, file.name, { app: 'storage', userId });
return c.json(
{
id: crypto.randomUUID(),
name: file.name,
storagePath: result.id,
storageKey: result.id,
mimeType: file.type,
size: file.size,
parentFolderId: folderId,
mediaId: result.id,
},
201
);
}
// Non-images -> shared-storage as before
const { createStorageStorage, generateUserFileKey, getContentType } = await import(
'@manacore/shared-storage'
);
const storage = createStorageStorage();
const key = generateUserFileKey(userId, file.name);
const buffer = Buffer.from(await file.arrayBuffer());
await storage.upload(key, buffer, {
await storage.upload(key, Buffer.from(buffer), {
contentType: getContentType(file.name),
public: false,
});

View file

@ -24,6 +24,8 @@ export interface LocalMeal extends BaseRecord {
portionSize?: string | null;
confidence: number;
nutrition?: NutritionData | null;
photoMediaId?: string | null;
photoUrl?: string | null;
}
export interface LocalGoal extends BaseRecord {
@ -65,5 +67,7 @@ export interface MealWithNutrition {
portionSize?: string | null;
confidence: number;
nutrition: NutritionData | null;
photoMediaId?: string | null;
photoUrl?: string | null;
createdAt: string;
}

239
pnpm-lock.yaml generated
View file

@ -64,6 +64,9 @@ importers:
apps/api:
dependencies:
'@manacore/media-client':
specifier: workspace:*
version: link:../../services/mana-media/packages/client
'@manacore/shared-hono':
specifier: workspace:*
version: link:../../packages/shared-hono
@ -26455,7 +26458,7 @@ snapshots:
ws: 8.18.3
zod: 3.25.76
optionalDependencies:
expo-router: 55.0.5(7734cwvz7fkv6rui37yzuzjxt4)
expo-router: 55.0.5(qfbmyq6c2bmncbs6qduwm3ekeu)
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
transitivePeerDependencies:
- '@expo/dom-webview'
@ -27204,7 +27207,7 @@ snapshots:
react: 19.2.4
optionalDependencies:
'@expo/metro-runtime': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
expo-router: 55.0.5(7734cwvz7fkv6rui37yzuzjxt4)
expo-router: 55.0.5(qfbmyq6c2bmncbs6qduwm3ekeu)
react-dom: 19.2.4(react@19.2.4)
transitivePeerDependencies:
- supports-color
@ -27661,42 +27664,6 @@ snapshots:
- supports-color
- ts-node
'@jest/core@30.3.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))':
dependencies:
'@jest/console': 30.3.0
'@jest/pattern': 30.0.1
'@jest/reporters': 30.3.0
'@jest/test-result': 30.3.0
'@jest/transform': 30.3.0
'@jest/types': 30.3.0
'@types/node': 22.19.1
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 4.3.1
exit-x: 0.2.2
graceful-fs: 4.2.11
jest-changed-files: 30.3.0
jest-config: 30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
jest-haste-map: 30.3.0
jest-message-util: 30.3.0
jest-regex-util: 30.0.1
jest-resolve: 30.3.0
jest-resolve-dependencies: 30.3.0
jest-runner: 30.3.0
jest-runtime: 30.3.0
jest-snapshot: 30.3.0
jest-util: 30.3.0
jest-validate: 30.3.0
jest-watcher: 30.3.0
pretty-format: 30.3.0
slash: 3.0.0
transitivePeerDependencies:
- babel-plugin-macros
- esbuild-register
- supports-color
- ts-node
optional: true
'@jest/core@30.3.0(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))':
dependencies:
'@jest/console': 30.3.0
@ -32225,19 +32192,6 @@ snapshots:
jest: 30.3.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))
optional: true
'@testing-library/react-native@13.3.3(jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react-test-renderer@19.1.0(react@19.2.4))(react@19.2.4)':
dependencies:
jest-matcher-utils: 30.3.0
picocolors: 1.1.1
pretty-format: 30.3.0
react: 19.2.4
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
react-test-renderer: 19.1.0(react@19.2.4)
redent: 3.0.0
optionalDependencies:
jest: 30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
optional: true
'@testing-library/react-native@13.3.3(jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3)))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react-test-renderer@19.1.0(react@19.2.0))(react@19.2.0)':
dependencies:
jest-matcher-utils: 30.3.0
@ -32303,6 +32257,19 @@ snapshots:
jest: 30.3.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
optional: true
'@testing-library/react-native@13.3.3(jest@30.3.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react-test-renderer@19.1.0(react@19.2.4))(react@19.2.4)':
dependencies:
jest-matcher-utils: 30.3.0
picocolors: 1.1.1
pretty-format: 30.3.0
react: 19.2.4
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
react-test-renderer: 19.1.0(react@19.2.4)
redent: 3.0.0
optionalDependencies:
jest: 30.3.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
optional: true
'@testing-library/svelte-core@1.0.0(svelte@5.44.0)':
dependencies:
svelte: 5.44.0
@ -38632,57 +38599,6 @@ snapshots:
- expo-font
- supports-color
expo-router@55.0.5(7734cwvz7fkv6rui37yzuzjxt4):
dependencies:
'@expo/log-box': 55.0.8(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@expo/metro-runtime': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@expo/schema-utils': 55.0.2
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@react-navigation/bottom-tabs': 7.15.5(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@react-navigation/native': 7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@react-navigation/native-stack': 7.14.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
client-only: 0.0.1
debug: 4.4.3
escape-string-regexp: 4.0.0
expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.7)(expo-router@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native-webview@13.12.2(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
expo-constants: 55.0.7(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(typescript@5.9.3)
expo-glass-effect: 55.0.8(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
expo-image: 55.0.6(expo@55.0.5)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
expo-linking: 55.0.7(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
expo-server: 55.0.6
expo-symbols: 55.0.5(expo-font@55.0.4)(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
fast-deep-equal: 3.1.3
invariant: 2.2.4
nanoid: 3.3.11
query-string: 7.1.3
react: 19.2.4
react-fast-compare: 3.2.2
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
react-native-is-edge-to-edge: 1.2.1(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
react-native-safe-area-context: 5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
react-native-screens: 4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
semver: 7.6.3
server-only: 0.0.1
sf-symbols-typescript: 2.2.0
shallowequal: 1.1.0
use-latest-callback: 0.2.6(react@19.2.4)
vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
optionalDependencies:
'@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-gesture-handler@2.30.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-reanimated@4.2.2(react-native-worklets@0.7.4(@babel/core@7.28.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@testing-library/react-native': 13.3.3(jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react-test-renderer@19.1.0(react@19.2.4))(react@19.2.4)
react-dom: 19.2.4(react@19.2.4)
react-native-gesture-handler: 2.30.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
react-native-reanimated: 4.2.2(react-native-worklets@0.7.4(@babel/core@7.28.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- '@react-native-masked-view/masked-view'
- '@types/react'
- '@types/react-dom'
- expo-font
- supports-color
optional: true
expo-router@55.0.5(alplmueeiabdlyo62r6crpdhli):
dependencies:
'@expo/log-box': 55.0.8(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)
@ -38884,6 +38800,57 @@ snapshots:
- supports-color
optional: true
expo-router@55.0.5(qfbmyq6c2bmncbs6qduwm3ekeu):
dependencies:
'@expo/log-box': 55.0.8(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@expo/metro-runtime': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@expo/schema-utils': 55.0.2
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@react-navigation/bottom-tabs': 7.15.5(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@react-navigation/native': 7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@react-navigation/native-stack': 7.14.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
client-only: 0.0.1
debug: 4.4.3
escape-string-regexp: 4.0.0
expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.7)(expo-router@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native-webview@13.12.2(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
expo-constants: 55.0.7(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(typescript@5.9.3)
expo-glass-effect: 55.0.8(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
expo-image: 55.0.6(expo@55.0.5)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
expo-linking: 55.0.7(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
expo-server: 55.0.6
expo-symbols: 55.0.5(expo-font@55.0.4)(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
fast-deep-equal: 3.1.3
invariant: 2.2.4
nanoid: 3.3.11
query-string: 7.1.3
react: 19.2.4
react-fast-compare: 3.2.2
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
react-native-is-edge-to-edge: 1.2.1(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
react-native-safe-area-context: 5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
react-native-screens: 4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
semver: 7.6.3
server-only: 0.0.1
sf-symbols-typescript: 2.2.0
shallowequal: 1.1.0
use-latest-callback: 0.2.6(react@19.2.4)
vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
optionalDependencies:
'@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-gesture-handler@2.30.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-reanimated@4.2.2(react-native-worklets@0.7.4(@babel/core@7.28.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@testing-library/react-native': 13.3.3(jest@30.3.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react-test-renderer@19.1.0(react@19.2.4))(react@19.2.4)
react-dom: 19.2.4(react@19.2.4)
react-native-gesture-handler: 2.30.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
react-native-reanimated: 4.2.2(react-native-worklets@0.7.4(@babel/core@7.28.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- '@react-native-masked-view/masked-view'
- '@types/react'
- '@types/react-dom'
- expo-font
- supports-color
optional: true
expo-router@55.0.5(x6ppsusqzuln52ezuk2uhtcwta):
dependencies:
'@expo/log-box': 55.0.8(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@ -40916,26 +40883,6 @@ snapshots:
- ts-node
optional: true
jest-cli@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
dependencies:
'@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
'@jest/test-result': 30.3.0
'@jest/types': 30.3.0
chalk: 4.1.2
exit-x: 0.2.2
import-local: 3.2.0
jest-config: 30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
jest-util: 30.3.0
jest-validate: 30.3.0
yargs: 17.7.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- esbuild-register
- supports-color
- ts-node
optional: true
jest-cli@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3)):
dependencies:
'@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3))
@ -41112,40 +41059,6 @@ snapshots:
- supports-color
optional: true
jest-config@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
dependencies:
'@babel/core': 7.28.5
'@jest/get-type': 30.1.0
'@jest/pattern': 30.0.1
'@jest/test-sequencer': 30.3.0
'@jest/types': 30.3.0
babel-jest: 30.3.0(@babel/core@7.28.5)
chalk: 4.1.2
ci-info: 4.3.1
deepmerge: 4.3.1
glob: 10.5.0
graceful-fs: 4.2.11
jest-circus: 30.3.0
jest-docblock: 30.2.0
jest-environment-node: 30.3.0
jest-regex-util: 30.0.1
jest-resolve: 30.3.0
jest-runner: 30.3.0
jest-util: 30.3.0
jest-validate: 30.3.0
parse-json: 5.2.0
pretty-format: 30.3.0
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 22.19.1
esbuild-register: 3.6.0(esbuild@0.19.12)
ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
optional: true
jest-config@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)):
dependencies:
'@babel/core': 7.28.5
@ -41900,20 +41813,6 @@ snapshots:
- ts-node
optional: true
jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
dependencies:
'@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
'@jest/types': 30.3.0
import-local: 3.2.0
jest-cli: 30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- esbuild-register
- supports-color
- ts-node
optional: true
jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3)):
dependencies:
'@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3))