diff --git a/packages/shared-credit-service/package.json b/packages/shared-credit-service/package.json new file mode 100644 index 000000000..0154d2956 --- /dev/null +++ b/packages/shared-credit-service/package.json @@ -0,0 +1,25 @@ +{ + "name": "@manacore/shared-credit-service", + "version": "0.0.1", + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "types": "./src/index.ts", + "files": [ + "src" + ], + "scripts": { + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@manacore/shared-subscription-types": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/shared-credit-service/src/createCreditService.ts b/packages/shared-credit-service/src/createCreditService.ts new file mode 100644 index 000000000..9e64ac97b --- /dev/null +++ b/packages/shared-credit-service/src/createCreditService.ts @@ -0,0 +1,327 @@ +/** + * Credit Service Factory + * + * Creates a credit service instance configured for a specific app. + * Handles credit balance fetching, pricing, and consumption notifications. + * + * @example + * ```ts + * import { createCreditService } from '@manacore/shared-credit-service'; + * import { auth } from '$lib/stores/auth'; + * + * export const creditService = createCreditService({ + * apiUrl: 'https://api.example.com', + * pricingEndpoint: '/credits/pricing', + * balanceEndpoint: '/auth/credits', + * getAuthToken: () => auth.getToken(), + * fallbackPricing: { + * STORY_CREATION: 10, + * CHARACTER_CREATION: 20 + * } + * }); + * ``` + */ + +import type { + CreditServiceConfig, + CreditBalance, + CreditCheckResponse, + PricingResponse, + CreditUpdateCallback, + StandardOperationType +} from './types'; +import { DEFAULT_OPERATION_PRICING } from './types'; + +/** + * Create a credit service instance + */ +export function createCreditService(config: CreditServiceConfig) { + const { + apiUrl, + balanceEndpoint = '/auth/credits', + pricingEndpoint = '/credits/pricing', + cacheDuration = 30 * 60 * 1000, // 30 minutes default + fallbackPricing = {}, + getAuthToken + } = config; + + // Normalize API URL (remove trailing slash) + const baseUrl = apiUrl.replace(/\/$/, ''); + + // Internal state + let cachedPricing: PricingResponse | null = null; + let pricingLastFetched = 0; + const creditUpdateCallbacks: CreditUpdateCallback[] = []; + + // Merge fallback pricing with defaults + const mergedFallbackPricing = { + ...DEFAULT_OPERATION_PRICING, + ...fallbackPricing + }; + + /** + * Initialize the credit service by preloading pricing + */ + async function initialize(): Promise { + try { + await getPricing(); + console.log('[CreditService] Initialized with backend pricing'); + } catch (error) { + console.warn('[CreditService] Initialization failed, using fallback pricing:', error); + } + } + + /** + * Register a callback for credit consumption notifications + * @returns Unsubscribe function + */ + function onCreditUpdate(callback: CreditUpdateCallback): () => void { + creditUpdateCallbacks.push(callback); + + return () => { + const index = creditUpdateCallbacks.indexOf(callback); + if (index > -1) { + creditUpdateCallbacks.splice(index, 1); + } + }; + } + + /** + * Notify all callbacks about credit consumption + */ + function notifyCreditUpdate(creditsConsumed: number, operation?: string): void { + creditUpdateCallbacks.forEach((callback) => { + try { + callback(creditsConsumed, operation); + } catch (error) { + console.error('[CreditService] Error in credit update callback:', error); + } + }); + } + + /** + * Manually trigger credit update notifications + */ + function triggerCreditUpdate(creditsConsumed: number, operation?: string): void { + notifyCreditUpdate(creditsConsumed, operation); + } + + /** + * Fetch pricing information from backend with caching + */ + async function getPricing(): Promise { + const now = Date.now(); + + // Return cached pricing if still valid + if (cachedPricing && now - pricingLastFetched < cacheDuration) { + return cachedPricing; + } + + try { + const response = await fetch(`${baseUrl}${pricingEndpoint}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const pricing = await response.json(); + cachedPricing = pricing; + pricingLastFetched = now; + + return pricing; + } catch (error) { + console.error('[CreditService] Error fetching pricing:', error); + + // Return cached pricing if available + if (cachedPricing) { + return cachedPricing; + } + + // Return fallback pricing + return { + operationCosts: mergedFallbackPricing, + lastUpdated: new Date().toISOString() + }; + } + } + + /** + * Get user's credit balance + */ + async function getBalance(): Promise { + try { + const token = await getAuthToken(); + + if (!token) { + console.error('[CreditService] No authentication token available'); + return null; + } + + const response = await fetch(`${baseUrl}${balanceEndpoint}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || `HTTP ${response.status}`); + } + + const data = await response.json(); + + // Handle wrapped response structure + if (data.data && typeof data.data.credits === 'number') { + return { + credits: data.data.credits, + maxCreditLimit: data.data.maxCreditLimit ?? data.data.credits, + userId: data.data.userId ?? '', + lastUpdated: new Date().toISOString() + }; + } + + // Handle direct structure + if (typeof data.credits === 'number') { + return { + credits: data.credits, + maxCreditLimit: data.maxCreditLimit ?? data.credits, + userId: data.userId ?? '', + lastUpdated: new Date().toISOString() + }; + } + + return data; + } catch (error) { + console.error('[CreditService] Error fetching balance:', error); + return null; + } + } + + /** + * Get cost for a specific operation (async with backend fetch) + */ + async function getOperationCost(operation: StandardOperationType): Promise { + try { + const pricing = await getPricing(); + return pricing.operationCosts[operation] ?? mergedFallbackPricing[operation] ?? 0; + } catch (error) { + console.error('[CreditService] Error getting operation cost:', error); + return mergedFallbackPricing[operation] ?? 0; + } + } + + /** + * Get cost for a specific operation (sync, uses cached values) + */ + function getOperationCostSync(operation: StandardOperationType): number { + if (cachedPricing?.operationCosts[operation] !== undefined) { + return cachedPricing.operationCosts[operation]; + } + return mergedFallbackPricing[operation] ?? 0; + } + + /** + * Calculate cost for multiple units of an operation + */ + async function calculateCost(operation: StandardOperationType, quantity: number = 1): Promise { + const unitCost = await getOperationCost(operation); + return unitCost * quantity; + } + + /** + * Calculate cost synchronously (uses cached values) + */ + function calculateCostSync(operation: StandardOperationType, quantity: number = 1): number { + const unitCost = getOperationCostSync(operation); + return unitCost * quantity; + } + + /** + * Check if user has enough credits for an operation + */ + async function checkBalance( + requiredCredits: number, + operation?: string + ): Promise { + const balance = await getBalance(); + + if (!balance) { + return { + hasEnoughCredits: false, + currentCredits: 0, + requiredCredits, + deficit: requiredCredits + }; + } + + const hasEnough = balance.credits >= requiredCredits; + + return { + hasEnoughCredits: hasEnough, + currentCredits: balance.credits, + requiredCredits, + deficit: hasEnough ? undefined : requiredCredits - balance.credits, + context: operation ? { operation } : undefined + }; + } + + /** + * Check if user has enough credits for a specific operation + */ + async function checkOperationBalance(operation: StandardOperationType): Promise { + const cost = await getOperationCost(operation); + return checkBalance(cost, operation); + } + + /** + * Format credit amount for display + */ + function formatCredits(amount: number, locale: string = 'en-US'): string { + return new Intl.NumberFormat(locale).format(amount); + } + + /** + * Clear pricing cache (useful for testing or forced refresh) + */ + function clearCache(): void { + cachedPricing = null; + pricingLastFetched = 0; + } + + return { + // Initialization + initialize, + + // Balance operations + getBalance, + checkBalance, + checkOperationBalance, + + // Pricing operations + getPricing, + getOperationCost, + getOperationCostSync, + calculateCost, + calculateCostSync, + + // Notifications + onCreditUpdate, + triggerCreditUpdate, + + // Utilities + formatCredits, + clearCache + }; +} + +/** + * Type for the credit service instance + */ +export type CreditService = ReturnType; diff --git a/packages/shared-credit-service/src/index.ts b/packages/shared-credit-service/src/index.ts new file mode 100644 index 000000000..d93d87e1e --- /dev/null +++ b/packages/shared-credit-service/src/index.ts @@ -0,0 +1,75 @@ +/** + * @manacore/shared-credit-service + * + * Shared credit/mana service for the ManaCore monorepo. + * + * Provides: + * - Credit balance fetching and caching + * - Operation pricing with fallbacks + * - Credit check before operations + * - Credit consumption notifications + * + * @example Basic usage + * ```ts + * import { createCreditService } from '@manacore/shared-credit-service'; + * + * const creditService = createCreditService({ + * apiUrl: 'https://api.myapp.com', + * getAuthToken: async () => localStorage.getItem('token') + * }); + * + * // Initialize on app startup + * await creditService.initialize(); + * + * // Check balance before operation + * const check = await creditService.checkOperationBalance('STORY_CREATION'); + * if (!check.hasEnoughCredits) { + * showInsufficientCreditsModal(check.deficit); + * return; + * } + * + * // After successful operation, notify listeners + * creditService.triggerCreditUpdate(10, 'STORY_CREATION'); + * ``` + * + * @example With Svelte store integration + * ```ts + * // creditService.ts + * import { createCreditService } from '@manacore/shared-credit-service'; + * import { auth } from '$lib/stores/auth'; + * + * export const creditService = createCreditService({ + * apiUrl: import.meta.env.VITE_API_URL, + * pricingEndpoint: '/credits/pricing', + * getAuthToken: () => auth.getToken() + * }); + * + * // creditStore.svelte.ts + * import { creditService } from './creditService'; + * + * let balance = $state(0); + * + * // Listen for credit updates + * creditService.onCreditUpdate((consumed) => { + * balance -= consumed; + * }); + * ``` + */ + +// Factory function +export { createCreditService } from './createCreditService'; +export type { CreditService } from './createCreditService'; + +// Types +export type { + CreditServiceConfig, + CreditBalance, + CreditCheckResponse, + CreditConsumptionResponse, + PricingResponse, + CreditUpdateCallback, + StandardOperationType +} from './types'; + +// Constants +export { DEFAULT_OPERATION_PRICING } from './types'; diff --git a/packages/shared-credit-service/src/types.ts b/packages/shared-credit-service/src/types.ts new file mode 100644 index 000000000..3c6d99e30 --- /dev/null +++ b/packages/shared-credit-service/src/types.ts @@ -0,0 +1,154 @@ +/** + * Credit Service Types + * + * Types for credit/mana operations across all apps + */ + +import type { OperationPricing, ManaBalance } from '@manacore/shared-subscription-types'; + +/** + * Credit balance with additional metadata + */ +export interface CreditBalance { + /** Current credit/mana amount */ + credits: number; + /** Maximum credit limit */ + maxCreditLimit: number; + /** User ID */ + userId: string; + /** Currency identifier (default: 'mana') */ + currency?: string; + /** Last updated timestamp */ + lastUpdated?: string; +} + +/** + * Result of checking if user has enough credits + */ +export interface CreditCheckResponse { + /** Whether user has sufficient credits */ + hasEnoughCredits: boolean; + /** Current credit balance */ + currentCredits: number; + /** Credits required for operation */ + requiredCredits: number; + /** Deficit amount (if insufficient) */ + deficit?: number; + /** Credit source type */ + creditType?: 'user' | 'space'; + /** Additional context */ + context?: Record; +} + +/** + * Result of credit consumption + */ +export interface CreditConsumptionResponse { + /** Whether consumption succeeded */ + success: boolean; + /** Human-readable message */ + message: string; + /** Amount of credits consumed */ + creditsConsumed: number; + /** Credit source type */ + creditType: 'user' | 'space'; + /** Remaining balance after consumption */ + remainingCredits?: number; + /** Related operation identifier */ + operationId?: string; +} + +/** + * Pricing response from backend + */ +export interface PricingResponse { + /** Map of operation types to their costs */ + operationCosts: Record; + /** Cost per hour for time-based operations (e.g., transcription) */ + transcriptionPerHour?: number; + /** When pricing was last updated */ + lastUpdated: string; +} + +/** + * Configuration for creating a credit service instance + */ +export interface CreditServiceConfig { + /** Base URL for credit/billing API */ + apiUrl: string; + /** Endpoint for fetching balance (relative to apiUrl) */ + balanceEndpoint?: string; + /** Endpoint for fetching pricing (relative to apiUrl) */ + pricingEndpoint?: string; + /** How long to cache pricing (milliseconds, default: 30 minutes) */ + cacheDuration?: number; + /** Fallback pricing if backend unavailable */ + fallbackPricing?: Record; + /** Function to get current auth token */ + getAuthToken: () => Promise; +} + +/** + * Credit update callback type + */ +export type CreditUpdateCallback = (creditsConsumed: number, operation?: string) => void; + +/** + * Standard operation types across all apps + */ +export type StandardOperationType = + // Memoro operations + | 'TRANSCRIPTION_PER_HOUR' + | 'HEADLINE_GENERATION' + | 'MEMORY_CREATION' + | 'BLUEPRINT_PROCESSING' + | 'QUESTION_MEMO' + | 'NEW_MEMORY' + | 'MEMO_COMBINE' + | 'MEMO_SHARING' + | 'SPACE_OPERATION' + // Maerchenzauber operations + | 'CHARACTER_CREATION' + | 'CHARACTER_GENERATION_FROM_IMAGE' + | 'CHARACTER_IMPORT' + | 'STORY_CREATION' + | 'STORY_CONTINUATION' + // ManaDeck operations + | 'DECK_CREATION' + | 'CARD_GENERATION' + | 'AI_REVIEW' + // Generic operations + | 'AI_PROCESSING' + | 'EXPORT' + | 'IMPORT' + | string; // Allow custom operation types + +/** + * Default pricing for operations (fallback values) + */ +export const DEFAULT_OPERATION_PRICING: Record = { + // Memoro + TRANSCRIPTION_PER_HOUR: 120, + HEADLINE_GENERATION: 10, + MEMORY_CREATION: 10, + BLUEPRINT_PROCESSING: 5, + QUESTION_MEMO: 5, + NEW_MEMORY: 5, + MEMO_COMBINE: 5, + MEMO_SHARING: 1, + SPACE_OPERATION: 2, + // Maerchenzauber + CHARACTER_CREATION: 20, + CHARACTER_GENERATION_FROM_IMAGE: 20, + CHARACTER_IMPORT: 10, + STORY_CREATION: 10, + STORY_CONTINUATION: 5, + // ManaDeck + DECK_CREATION: 5, + CARD_GENERATION: 2, + AI_REVIEW: 10, + // Generic + AI_PROCESSING: 10, + EXPORT: 1, + IMPORT: 1 +}; diff --git a/packages/shared-credit-service/tsconfig.json b/packages/shared-credit-service/tsconfig.json new file mode 100644 index 000000000..882e14638 --- /dev/null +++ b/packages/shared-credit-service/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "noEmit": true + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a21ddd34..36a31a820 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1630,6 +1630,16 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/shared-credit-service: + dependencies: + '@manacore/shared-subscription-types': + specifier: workspace:* + version: link:../shared-subscription-types + devDependencies: + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages/shared-i18n: devDependencies: svelte: @@ -14868,7 +14878,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(evxcyavfmgswt4zg3ii4wlqsdm) + expo-router: 6.0.15(nbbplg4zewzlp5oy3zff3m2jw4) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -20699,7 +20709,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) @@ -20773,7 +20783,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -20798,14 +20808,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -20877,7 +20887,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -21472,7 +21482,7 @@ snapshots: expo-device@8.0.9(expo@54.0.25): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) ua-parser-js: 0.7.41 expo-document-picker@14.0.7(expo@54.0.25): @@ -21527,7 +21537,7 @@ snapshots: expo-image-loader@6.0.0(expo@54.0.25): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-image-picker@17.0.8(expo@54.0.13): dependencies: @@ -21536,7 +21546,7 @@ snapshots: expo-image-picker@17.0.8(expo@54.0.25): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-image-loader: 6.0.0(expo@54.0.25) expo-image@3.0.10(expo@54.0.25)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): @@ -21603,7 +21613,7 @@ snapshots: expo-localization@17.0.7(expo@54.0.25)(react@19.1.0): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react: 19.1.0 rtl-detect: 1.1.2 @@ -21865,7 +21875,7 @@ snapshots: expo-secure-store@15.0.7(expo@54.0.25): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-server@1.0.4: {} @@ -21876,7 +21886,7 @@ snapshots: expo-splash-screen@31.0.11(expo@54.0.25): dependencies: '@expo/prebuild-config': 54.0.6(expo@54.0.25) - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - supports-color