feat: add shared credit service package (Tier 5)

Create @manacore/shared-credit-service with:
- createCreditService() factory function
- Credit balance fetching with auth token support
- Operation pricing with caching (30min default)
- Fallback pricing for all standard operations
- Credit check before operations
- Credit consumption notification system
- Sync and async cost calculation methods

Standard operations supported:
- Memoro: transcription, headline, memory, blueprint, etc.
- Maerchenzauber: character creation, story creation
- ManaDeck: deck creation, card generation, AI review
- Generic: AI processing, export, import

Apps can use this service with their own configuration:
```ts
const creditService = createCreditService({
  apiUrl: 'https://api.myapp.com',
  getAuthToken: () => auth.getToken()
});
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-25 00:16:43 +01:00
parent 294d1fc5c5
commit c87641f91b
6 changed files with 619 additions and 12 deletions

View file

@ -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"
}
}

View file

@ -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<void> {
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<PricingResponse> {
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<CreditBalance | null> {
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<number> {
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<number> {
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<CreditCheckResponse> {
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<CreditCheckResponse> {
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<typeof createCreditService>;

View file

@ -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<number>(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';

View file

@ -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<string, unknown>;
}
/**
* 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<string, number>;
/** 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<string, number>;
/** Function to get current auth token */
getAuthToken: () => Promise<string | null>;
}
/**
* 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<string, number> = {
// 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
};

View file

@ -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/**/*"]
}

34
pnpm-lock.yaml generated
View file

@ -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