diff --git a/android/app/build.gradle b/android/app/build.gradle
index 6fb253cd..4b2ed948 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -92,8 +92,8 @@ android {
applicationId 'com.brewtracker.android'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 191
- versionName "3.2.6"
+ versionCode 207
+ versionName "3.3.15"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index a7bd023e..71527157 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,7 +1,7 @@
BrewTracker
automatic
- 3.2.6
+ 3.3.15
contain
false
\ No newline at end of file
diff --git a/app.json b/app.json
index 9d3b4108..1ff3df9d 100644
--- a/app.json
+++ b/app.json
@@ -3,7 +3,7 @@
"name": "BrewTracker",
"slug": "brewtracker-android",
"orientation": "portrait",
- "version": "3.2.6",
+ "version": "3.3.15",
"icon": "./assets/images/BrewTrackerAndroidLogo.png",
"scheme": "brewtracker",
"userInterfaceStyle": "automatic",
@@ -16,7 +16,7 @@
},
"edgeToEdgeEnabled": true,
"package": "com.brewtracker.android",
- "versionCode": 191,
+ "versionCode": 207,
"permissions": [
"CAMERA",
"VIBRATE",
@@ -58,7 +58,7 @@
}
},
"owner": "jackmisner",
- "runtimeVersion": "3.2.6",
+ "runtimeVersion": "3.3.15",
"updates": {
"url": "https://u.expo.dev/edf222a8-b532-4d12-9b69-4e7fbb1d41c2"
}
diff --git a/app/(auth)/resetPassword.tsx b/app/(auth)/resetPassword.tsx
index 4546b65b..3199116d 100644
--- a/app/(auth)/resetPassword.tsx
+++ b/app/(auth)/resetPassword.tsx
@@ -47,6 +47,7 @@ import { useAuth } from "@contexts/AuthContext";
import { useTheme } from "@contexts/ThemeContext";
import { loginStyles } from "@styles/auth/loginStyles";
import { TEST_IDS } from "@src/constants/testIDs";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
type Strength = "" | "weak" | "medium" | "strong";
@@ -188,7 +189,11 @@ const ResetPasswordScreen: React.FC = () => {
setSuccess(true);
} catch (err: unknown) {
// Error is handled by the context and displayed through error state
- console.error("Password reset failed:", err);
+ void UnifiedLogger.error(
+ "resetPassword.handleResetPassword",
+ "Password reset failed:",
+ err
+ );
// Optionally show a fallback alert if context error handling fails
if (!error) {
Alert.alert("Error", "Failed to reset password. Please try again.");
diff --git a/app/(modals)/(beerxml)/importBeerXML.tsx b/app/(modals)/(beerxml)/importBeerXML.tsx
index d0ec4c47..409fa13a 100644
--- a/app/(modals)/(beerxml)/importBeerXML.tsx
+++ b/app/(modals)/(beerxml)/importBeerXML.tsx
@@ -25,38 +25,34 @@ import {
import { MaterialIcons } from "@expo/vector-icons";
import { router } from "expo-router";
import { useTheme } from "@contexts/ThemeContext";
+import { useUnits } from "@contexts/UnitContext";
import { createRecipeStyles } from "@styles/modals/createRecipeStyles";
-import BeerXMLService from "@services/beerxml/BeerXMLService";
+import BeerXMLService, {
+ type BeerXMLRecipe,
+} from "@services/beerxml/BeerXMLService";
import { TEST_IDS } from "@src/constants/testIDs";
import { ModalHeader } from "@src/components/ui/ModalHeader";
-
-interface Recipe {
- name?: string;
- style?: string;
- batch_size?: number;
- batch_size_unit?: "l" | "gal";
- ingredients?: {
- type: "grain" | "hop" | "yeast" | "other";
- [key: string]: any;
- }[];
- [key: string]: any;
-}
+import { UnitConversionChoiceModal } from "@src/components/beerxml/UnitConversionChoiceModal";
+import { UnitSystem } from "@src/types";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
interface ImportState {
- step: "file_selection" | "parsing" | "recipe_selection";
+ step: "file_selection" | "parsing" | "unit_choice" | "recipe_selection";
isLoading: boolean;
error: string | null;
selectedFile: {
content: string;
filename: string;
} | null;
- parsedRecipes: Recipe[];
- selectedRecipe: Recipe | null;
+ parsedRecipes: BeerXMLRecipe[];
+ selectedRecipe: BeerXMLRecipe | null;
+ convertingTarget: UnitSystem | null;
}
export default function ImportBeerXMLScreen() {
const theme = useTheme();
const styles = createRecipeStyles(theme);
+ const { unitSystem } = useUnits();
const [importState, setImportState] = useState({
step: "file_selection",
@@ -65,6 +61,7 @@ export default function ImportBeerXMLScreen() {
selectedFile: null,
parsedRecipes: [],
selectedRecipe: null,
+ convertingTarget: null,
});
/**
@@ -99,7 +96,11 @@ export default function ImportBeerXMLScreen() {
// Automatically proceed to parsing
await parseBeerXML(result.content, result.filename);
} catch (error) {
- console.error("🍺 BeerXML Import - File selection error:", error);
+ void UnifiedLogger.error(
+ "beerxml",
+ "🍺 BeerXML Import - File selection error:",
+ error
+ );
setImportState(prev => ({
...prev,
isLoading: false,
@@ -110,24 +111,33 @@ export default function ImportBeerXMLScreen() {
/**
* Parse the selected BeerXML content
+ * BeerXML files are always in metric per spec
*/
const parseBeerXML = async (content: string, _filename: string) => {
try {
- // Parse using backend service
+ // Parse using backend service (returns metric recipes per BeerXML spec)
const recipes = await BeerXMLService.parseBeerXML(content);
if (recipes.length === 0) {
throw new Error("No recipes found in the BeerXML file");
}
+
+ // Select first recipe by default
+ const firstRecipe = recipes[0];
+
setImportState(prev => ({
...prev,
isLoading: false,
parsedRecipes: recipes,
- selectedRecipe: recipes[0],
- step: "recipe_selection",
+ selectedRecipe: firstRecipe,
+ step: "unit_choice", // Always show unit choice after parsing
}));
} catch (error) {
- console.error("🍺 BeerXML Import - Parsing error:", error);
+ void UnifiedLogger.error(
+ "beerxml",
+ "🍺 BeerXML Import - Parsing error:",
+ error
+ );
setImportState(prev => ({
...prev,
isLoading: false,
@@ -137,11 +147,74 @@ export default function ImportBeerXMLScreen() {
}
};
+ /**
+ * Handle unit system choice and conversion
+ * Applies normalization to both metric and imperial imports
+ */
+ const handleUnitSystemChoice = async (targetSystem: UnitSystem) => {
+ const recipe = importState.selectedRecipe;
+ if (!recipe) {
+ return;
+ }
+
+ setImportState(prev => ({ ...prev, convertingTarget: targetSystem }));
+
+ try {
+ // Convert recipe to target system with normalization
+ // Even metric imports need normalization (e.g., 28.3g -> 30g)
+ const { recipe: convertedRecipe, warnings } =
+ await BeerXMLService.convertRecipeUnits(
+ recipe,
+ targetSystem,
+ true // Always normalize to brewing-friendly increments
+ );
+
+ // Log warnings if any
+ if (warnings && warnings.length > 0) {
+ void UnifiedLogger.warn(
+ "beerxml",
+ "🍺 BeerXML Conversion warnings:",
+ warnings
+ );
+ }
+
+ setImportState(prev => ({
+ ...prev,
+ selectedRecipe: convertedRecipe,
+ step: "recipe_selection",
+ convertingTarget: null,
+ }));
+ } catch (error) {
+ void UnifiedLogger.error(
+ "beerxml",
+ "🍺 BeerXML Import - Conversion error:",
+ error
+ );
+ setImportState(prev => ({ ...prev, convertingTarget: null }));
+ Alert.alert(
+ "Conversion Error",
+ "Failed to convert recipe units. Please try again or select a different file.",
+ [
+ {
+ text: "OK",
+ onPress: () =>
+ setImportState(prev => ({
+ ...prev,
+ step: "file_selection",
+ })),
+ },
+ ]
+ );
+ }
+ };
+
/**
* Proceed to ingredient matching workflow
*/
- const proceedToIngredientMatching = () => {
- if (!importState.selectedRecipe) {
+ const proceedToIngredientMatching = (recipe?: BeerXMLRecipe) => {
+ const recipeToImport = recipe || importState.selectedRecipe;
+
+ if (!recipeToImport) {
Alert.alert("Error", "Please select a recipe to import");
return;
}
@@ -150,7 +223,7 @@ export default function ImportBeerXMLScreen() {
router.push({
pathname: "/(modals)/(beerxml)/ingredientMatching" as any,
params: {
- recipeData: JSON.stringify(importState.selectedRecipe),
+ recipeData: JSON.stringify(recipeToImport),
filename: importState.selectedFile?.filename || "imported_recipe",
},
});
@@ -167,6 +240,7 @@ export default function ImportBeerXMLScreen() {
selectedFile: null,
parsedRecipes: [],
selectedRecipe: null,
+ convertingTarget: null,
});
};
@@ -319,7 +393,7 @@ export default function ImportBeerXMLScreen() {
proceedToIngredientMatching()}
testID={TEST_IDS.patterns.touchableOpacityAction(
"proceed-to-matching"
)}
@@ -403,6 +477,22 @@ export default function ImportBeerXMLScreen() {
importState.step === "recipe_selection" &&
renderRecipeSelection()}
+
+ {/* Unit Conversion Choice Modal */}
+ handleUnitSystemChoice("metric")}
+ onChooseImperial={() => handleUnitSystemChoice("imperial")}
+ onCancel={() =>
+ setImportState(prev => ({
+ ...prev,
+ step: "file_selection",
+ }))
+ }
+ />
);
}
diff --git a/app/(modals)/(beerxml)/importReview.tsx b/app/(modals)/(beerxml)/importReview.tsx
index c27f1fbb..00e76c44 100644
--- a/app/(modals)/(beerxml)/importReview.tsx
+++ b/app/(modals)/(beerxml)/importReview.tsx
@@ -12,7 +12,7 @@
* - Import statistics and metadata display
*/
-import React, { useState } from "react";
+import React, { useState, useMemo } from "react";
import {
View,
Text,
@@ -26,13 +26,74 @@ import { router, useLocalSearchParams } from "expo-router";
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
import { useTheme } from "@contexts/ThemeContext";
import { createRecipeStyles } from "@styles/modals/createRecipeStyles";
-import ApiService from "@services/api/apiService";
-import { IngredientInput } from "@src/types";
+import {
+ Recipe,
+ RecipeIngredient,
+ RecipeMetricsInput,
+ TemperatureUnit,
+ UnitSystem,
+ BatchSizeUnit,
+} from "@src/types";
import { TEST_IDS } from "@src/constants/testIDs";
import { generateUniqueId } from "@utils/keyUtils";
import { QUERY_KEYS } from "@services/api/queryClient";
import { ModalHeader } from "@src/components/ui/ModalHeader";
+import { useRecipes } from "@src/hooks/offlineV2";
+import { OfflineMetricsCalculator } from "@services/brewing/OfflineMetricsCalculator";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
+
+/**
+ * Derive unit system from batch_size_unit with fallback to metric
+ * Centralizes the "l" => metric logic used throughout import
+ */
+function deriveUnitSystem(
+ batchSizeUnit: string | undefined,
+ explicitUnitSystem?: string
+): UnitSystem {
+ if (explicitUnitSystem === "metric" || explicitUnitSystem === "imperial") {
+ return explicitUnitSystem;
+ }
+ if (explicitUnitSystem !== undefined) {
+ void UnifiedLogger.warn(
+ "import-review",
+ `Invalid explicit unit_system "${explicitUnitSystem}", deriving from batch_size_unit`
+ );
+ }
+ // Default based on batch size unit
+ const lowerCasedUnit = batchSizeUnit?.toLowerCase();
+ return lowerCasedUnit === "gal" || lowerCasedUnit === "gallons"
+ ? "imperial"
+ : "metric";
+}
+
+/**
+ * Derive mash temperature unit with normalization and validation
+ * Only accepts valid "C" or "F" values, falls back to unit system default
+ */
+function deriveMashTempUnit(
+ mashTempUnit: string | undefined,
+ unitSystem: UnitSystem
+): TemperatureUnit {
+ // Normalize and validate provided unit
+ if (mashTempUnit) {
+ const normalized = mashTempUnit.toUpperCase();
+ if (normalized === "C" || normalized === "F") {
+ return normalized as TemperatureUnit;
+ }
+ // Invalid unit provided, log warning and fall through to default
+ void UnifiedLogger.warn(
+ "import-review",
+ `Invalid mash_temp_unit "${mashTempUnit}", using default for ${unitSystem}`
+ );
+ }
+ // Fall back to unit system default
+ return unitSystem === "metric" ? "C" : "F";
+}
+
+/**
+ * Coerce ingredient time values to valid numbers or undefined
+ */
function coerceIngredientTime(input: unknown): number | undefined {
if (input == null) {
return undefined;
@@ -47,6 +108,173 @@ function coerceIngredientTime(input: unknown): number | undefined {
return Number.isFinite(n) && n >= 0 ? n : undefined; // reject NaN/±Inf/negatives
}
+/**
+ * Type for imported recipe data from BeerXML
+ */
+interface ImportedRecipeData {
+ name: string;
+ style?: string;
+ description?: string;
+ notes?: string;
+ batch_size: number | string;
+ batch_size_unit?: string;
+ boil_time?: number | string;
+ efficiency?: number;
+ unit_system?: string;
+ mash_temp_unit?: string;
+ mash_temperature?: number;
+ ingredients?: any[];
+}
+
+/**
+ * Normalized recipe values for consistent use across metrics, creation, and preview
+ */
+interface NormalizedRecipeValues {
+ unitSystem: UnitSystem;
+ batchSize: number;
+ batchSizeUnit: BatchSizeUnit;
+ boilTime: number;
+ displayBatchSize: string;
+ displayBoilTime: string;
+}
+
+/**
+ * Normalize batch size and boil time values with consistent defaults
+ * Returns values ready for metrics calculation, recipe creation, and preview display
+ */
+function normalizeRecipeValues(
+ recipeData: ImportedRecipeData
+): NormalizedRecipeValues {
+ // Derive unit system using centralized logic
+ const unitSystem = deriveUnitSystem(
+ recipeData.batch_size_unit,
+ recipeData.unit_system
+ );
+
+ // Normalize batch size with fallback to 19.0
+ const rawBatchSize =
+ typeof recipeData.batch_size === "number"
+ ? recipeData.batch_size
+ : Number(recipeData.batch_size);
+ const batchSize =
+ Number.isFinite(rawBatchSize) && rawBatchSize > 0 ? rawBatchSize : 19.0;
+
+ // Normalize batch size unit
+ const batchSizeUnit =
+ recipeData.batch_size_unit || (unitSystem === "metric" ? "l" : "gal");
+
+ // Normalize boil time with fallback to 60
+ const boilTime = coerceIngredientTime(recipeData.boil_time) ?? 60;
+
+ // Generate display strings (consistent with what's shown and saved)
+ const displayBatchSize = batchSize.toFixed(1);
+ const displayBoilTime = String(boilTime);
+
+ // Determine display unit label
+ const rawUnit = String(batchSizeUnit).toLowerCase();
+ const unitLabel =
+ rawUnit === "l"
+ ? "L"
+ : rawUnit === "gal" || rawUnit === "gallons"
+ ? "gal"
+ : unitSystem === "metric"
+ ? "L"
+ : "gal";
+
+ return {
+ unitSystem,
+ batchSize,
+ batchSizeUnit: unitLabel === "L" ? "l" : "gal", // Normalized for API
+ boilTime,
+ displayBatchSize: `${displayBatchSize} ${unitLabel}`,
+ displayBoilTime: `${displayBoilTime} minutes`,
+ };
+}
+
+/**
+ * Normalize and validate imported ingredients
+ * Filters out invalid ingredients and logs errors
+ * Returns array ready for both metrics calculation and recipe creation
+ */
+function normalizeImportedIngredients(
+ ingredients: any[] | undefined
+): RecipeIngredient[] {
+ if (!ingredients || !Array.isArray(ingredients)) {
+ return [];
+ }
+
+ return ingredients
+ .filter((ing: any) => {
+ // Validate required fields before mapping
+ if (!ing.ingredient_id) {
+ void UnifiedLogger.warn(
+ "import-review",
+ "Ingredient missing ingredient_id",
+ ing
+ );
+ return false;
+ }
+ if (!ing.name || !ing.type || !ing.unit) {
+ void UnifiedLogger.error(
+ "import-review",
+ "Ingredient missing required fields",
+ ing
+ );
+ return false;
+ }
+ if (ing.amount === "" || ing.amount == null) {
+ void UnifiedLogger.error(
+ "import-review",
+ "Ingredient has missing amount",
+ ing
+ );
+ return false;
+ }
+ const amountNumber =
+ typeof ing.amount === "number" ? ing.amount : Number(ing.amount);
+ if (!Number.isFinite(amountNumber) || amountNumber <= 0) {
+ void UnifiedLogger.error(
+ "import-review",
+ "Ingredient has invalid amount",
+ ing
+ );
+ return false;
+ }
+ return true;
+ })
+ .map((ing: any): RecipeIngredient => {
+ const type = String(ing.type).toLowerCase();
+ return {
+ // No id - backend generates on creation
+ ingredient_id: ing.ingredient_id,
+ name: ing.name,
+ type: type,
+ amount:
+ typeof ing.amount === "number" ? ing.amount : Number(ing.amount),
+ unit: ing.unit,
+ use: ing.use,
+ time: coerceIngredientTime(ing.time),
+ instance_id: generateUniqueId("ing"),
+ // Include type-specific fields for proper ingredient matching and metrics
+ ...(type === "grain" && {
+ potential: ing.potential,
+ color: ing.color,
+ grain_type: ing.grain_type,
+ }),
+ ...(type === "hop" && {
+ alpha_acid: ing.alpha_acid,
+ }),
+ ...(type === "yeast" && {
+ attenuation: ing.attenuation,
+ }),
+ // Preserve BeerXML metadata if available
+ ...(ing.beerxml_data && {
+ beerxml_data: ing.beerxml_data,
+ }),
+ };
+ });
+}
+
export default function ImportReviewScreen() {
const theme = useTheme();
const styles = createRecipeStyles(theme);
@@ -57,66 +285,126 @@ export default function ImportReviewScreen() {
}>();
const queryClient = useQueryClient();
- const [recipeData] = useState(() => {
+ const { create: createRecipe } = useRecipes();
+ const [recipeData] = useState(() => {
try {
return JSON.parse(params.recipeData);
} catch (error) {
- console.error("Failed to parse recipe data:", error);
+ void UnifiedLogger.error(
+ "import-review",
+ "Failed to parse recipe data:",
+ error
+ );
return null;
}
});
/**
- * Calculate recipe metrics before creation
+ * Normalize recipe values once - use this for metrics, creation, and preview
+ * This ensures "what you see is what you get" across all three contexts
+ */
+ const normalizedValues = useMemo(() => {
+ if (!recipeData) {
+ return null;
+ }
+ return normalizeRecipeValues(recipeData);
+ }, [recipeData]);
+
+ /**
+ * Normalize ingredients once - use this for both preview and creation
+ * This ensures "what you see is what you get"
+ */
+ const normalizedIngredients = useMemo(() => {
+ return normalizeImportedIngredients(recipeData?.ingredients);
+ }, [recipeData?.ingredients]);
+
+ /**
+ * Count of ingredients that were filtered out during normalization
+ */
+ const filteredOutCount = useMemo(() => {
+ const originalCount = recipeData?.ingredients?.length || 0;
+ const normalizedCount = normalizedIngredients.length;
+ return originalCount - normalizedCount;
+ }, [recipeData?.ingredients, normalizedIngredients.length]);
+
+ /**
+ * Calculate recipe metrics before creation using offline-first approach
*/
const {
data: calculatedMetrics,
isLoading: metricsLoading,
error: metricsError,
} = useQuery({
- queryKey: ["recipeMetrics", "beerxml-import", recipeData],
+ queryKey: [
+ "recipeMetrics",
+ "beerxml-import-offline",
+ recipeData?.batch_size,
+ recipeData?.batch_size_unit,
+ recipeData?.efficiency,
+ recipeData?.boil_time,
+ recipeData?.mash_temperature,
+ recipeData?.mash_temp_unit,
+ // Include ingredient fingerprint for cache invalidation
+ normalizedIngredients.length,
+ normalizedIngredients
+ .map(i => `${i.ingredient_id}:${i.amount}:${i.unit}`)
+ .join("|"),
+ ],
queryFn: async () => {
- if (!recipeData || !recipeData.ingredients) {
+ if (
+ !recipeData ||
+ !normalizedValues ||
+ normalizedIngredients.length === 0
+ ) {
return null;
}
- const metricsPayload = {
- batch_size: recipeData.batch_size || 5,
- batch_size_unit: recipeData.batch_size_unit || "gal",
+ // Use pre-normalized values for consistent metrics calculation
+ const recipeFormData: RecipeMetricsInput = {
+ batch_size: normalizedValues.batchSize,
+ batch_size_unit: normalizedValues.batchSizeUnit,
efficiency: recipeData.efficiency || 75,
- boil_time: recipeData.boil_time || 60,
- // Respect provided unit when present; default sensibly per system.
- mash_temp_unit: ((recipeData.mash_temp_unit as "C" | "F") ??
- ((String(recipeData.batch_size_unit).toLowerCase() === "l"
- ? "C"
- : "F") as "C" | "F")) as "C" | "F",
- mash_temperature:
- typeof recipeData.mash_temperature === "number"
- ? recipeData.mash_temperature
- : String(recipeData.batch_size_unit).toLowerCase() === "l"
- ? 67
- : 152,
- ingredients: recipeData.ingredients.filter(
- (ing: any) => ing.ingredient_id
+ boil_time: normalizedValues.boilTime,
+ mash_temp_unit: deriveMashTempUnit(
+ recipeData.mash_temp_unit,
+ normalizedValues.unitSystem
),
+ mash_temperature:
+ recipeData.mash_temperature ??
+ (normalizedValues.unitSystem === "metric" ? 67 : 152),
+ ingredients: normalizedIngredients,
};
- const response =
- await ApiService.recipes.calculateMetricsPreview(metricsPayload);
+ // Calculate metrics offline (always, no network dependency)
+ // Validation failures return null, but internal errors throw to set metricsError
+ const validation =
+ OfflineMetricsCalculator.validateRecipeData(recipeFormData);
+ if (!validation.isValid) {
+ void UnifiedLogger.warn(
+ "import-review",
+ "Invalid recipe data for metrics calculation",
+ validation.errors
+ );
+ return null; // Validation failure - no error state, just no metrics
+ }
- return response.data;
- },
- enabled:
- !!recipeData &&
- !!recipeData.ingredients &&
- recipeData.ingredients.length > 0,
- staleTime: 30000,
- retry: (failureCount, error: any) => {
- if (error?.response?.status === 400) {
- return false;
+ try {
+ const metrics =
+ OfflineMetricsCalculator.calculateMetrics(recipeFormData);
+ return metrics;
+ } catch (error) {
+ // Internal calculator error - throw to set metricsError state
+ void UnifiedLogger.error(
+ "import-review",
+ "Unexpected metrics calculation failure",
+ error
+ );
+ throw error; // Re-throw to trigger error state
}
- return failureCount < 2;
},
+ enabled: !!recipeData && normalizedIngredients.length > 0,
+ staleTime: Infinity, // Deterministic calculation, never stale
+ retry: false, // Local calculation doesn't need retries
});
/**
@@ -124,30 +412,29 @@ export default function ImportReviewScreen() {
*/
const createRecipeMutation = useMutation({
mutationFn: async () => {
+ if (!normalizedValues || !recipeData) {
+ throw new Error("Recipe values not normalized");
+ }
+
+ // Use pre-normalized values (same as used for metrics and preview)
// Prepare recipe data for creation
- const recipePayload = {
+ const recipePayload: Partial = {
name: recipeData.name,
style: recipeData.style || "",
description: recipeData.description || "",
notes: recipeData.notes || "",
- batch_size: recipeData.batch_size,
- batch_size_unit: recipeData.batch_size_unit || "gal",
- boil_time: recipeData.boil_time || 60,
+ batch_size: normalizedValues.batchSize,
+ batch_size_unit: normalizedValues.batchSizeUnit,
+ boil_time: normalizedValues.boilTime,
efficiency: recipeData.efficiency || 75,
- unit_system: (recipeData.batch_size_unit === "l"
- ? "metric"
- : "imperial") as "metric" | "imperial",
- // Respect provided unit when present; default sensibly per system.
- mash_temp_unit: ((recipeData.mash_temp_unit as "C" | "F") ??
- ((String(recipeData.batch_size_unit).toLowerCase() === "l"
- ? "C"
- : "F") as "C" | "F")) as "C" | "F",
+ unit_system: normalizedValues.unitSystem,
+ mash_temp_unit: deriveMashTempUnit(
+ recipeData.mash_temp_unit,
+ normalizedValues.unitSystem
+ ),
mash_temperature:
- typeof recipeData.mash_temperature === "number"
- ? recipeData.mash_temperature
- : String(recipeData.batch_size_unit).toLowerCase() === "l"
- ? 67
- : 152,
+ recipeData.mash_temperature ??
+ (normalizedValues.unitSystem === "metric" ? 67 : 152),
is_public: false, // Import as private by default
// Include calculated metrics if available
...(calculatedMetrics && {
@@ -157,56 +444,39 @@ export default function ImportReviewScreen() {
estimated_ibu: calculatedMetrics.ibu,
estimated_srm: calculatedMetrics.srm,
}),
- ingredients: (recipeData.ingredients || [])
- .filter((ing: any) => {
- // Validate required fields before mapping
- if (!ing.ingredient_id) {
- console.error("Ingredient missing ingredient_id:", ing);
- return false;
- }
- if (!ing.name || !ing.type || !ing.unit) {
- console.error("Ingredient missing required fields:", ing);
- return false;
- }
- if (isNaN(Number(ing.amount))) {
- console.error("Ingredient has invalid amount:", ing);
- return false;
- }
- return true;
- })
- .map(
- (ing: any): IngredientInput => ({
- ingredient_id: ing.ingredient_id,
- name: ing.name,
- type: ing.type,
- amount: Number(ing.amount) || 0,
- unit: ing.unit,
- use: ing.use,
- time: coerceIngredientTime(ing.time),
- instance_id: generateUniqueId("ing"), // Generate unique instance ID for each imported ingredient
- })
- ),
+ ingredients: normalizedIngredients,
};
- const response = await ApiService.recipes.create(recipePayload);
- return response.data;
+ // Use offline V2 createRecipe which creates temp recipe first, then syncs to server
+ // This ensures immediate display with temp ID, then updates with server ID after sync
+ return await createRecipe(recipePayload);
},
onSuccess: createdRecipe => {
// Invalidate queries to refresh recipe lists
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.RECIPES });
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.USER_RECIPES });
+ queryClient.invalidateQueries({
+ queryKey: [...QUERY_KEYS.RECIPES, "offline"],
+ });
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.DASHBOARD });
+ // Prime the detail cache for immediate access
+ queryClient.setQueryData(
+ QUERY_KEYS.RECIPE(createdRecipe.id),
+ createdRecipe
+ );
+
// Show success message and navigate to recipe
Alert.alert(
"Import Successful",
- `"${recipeData.name}" has been imported successfully!`,
+ `"${recipeData?.name || "Recipe"}" has been imported successfully!`,
[
{
text: "View Recipe",
onPress: () => {
- // Navigate to the newly created recipe
+ // Replace current modal with ViewRecipe modal (like createRecipe does)
router.dismissAll();
- router.push({
+ router.replace({
pathname: "/(modals)/(recipes)/viewRecipe",
params: { recipe_id: createdRecipe.id },
});
@@ -224,10 +494,10 @@ export default function ImportReviewScreen() {
);
},
onError: (error: any) => {
- console.error("🍺 Import Review - Recipe creation error:", error);
+ void UnifiedLogger.error("import-review", "Recipe creation error", error);
Alert.alert(
"Import Failed",
- `Failed to create recipe "${recipeData.name}". Please try again.`,
+ `Failed to create recipe "${recipeData?.name || "Recipe"}". Please try again.`,
[{ text: "OK" }]
);
},
@@ -239,7 +509,7 @@ export default function ImportReviewScreen() {
const handleCreateRecipe = () => {
Alert.alert(
"Create Recipe",
- `Create "${recipeData.name}" in your recipe collection?`,
+ `Create "${recipeData?.name || "Recipe"}" in your recipe collection?`,
[
{ text: "Cancel", style: "cancel" },
{
@@ -360,7 +630,13 @@ export default function ImportReviewScreen() {
Ingredients
- {recipeData.ingredients?.length || 0} ingredients
+ {normalizedIngredients.length} ingredients
+ {filteredOutCount > 0 && (
+
+ {" "}
+ ({filteredOutCount} invalid filtered out)
+
+ )}
@@ -411,20 +687,14 @@ export default function ImportReviewScreen() {
Batch Size:
- {(() => {
- const n = Number(recipeData.batch_size);
- return Number.isFinite(n) ? n.toFixed(1) : "N/A";
- })()}{" "}
- {String(recipeData.batch_size_unit).toLowerCase() === "l"
- ? "L"
- : "gal"}
+ {normalizedValues?.displayBatchSize || "N/A"}
Boil Time:
- {coerceIngredientTime(recipeData.boil_time) ?? 60} minutes
+ {normalizedValues?.displayBoilTime || "N/A"}
@@ -508,7 +778,7 @@ export default function ImportReviewScreen() {
) : null}
- {calculatedMetrics.ibu ? (
+ {calculatedMetrics.ibu != null ? (
IBU:
@@ -516,7 +786,7 @@ export default function ImportReviewScreen() {
) : null}
- {calculatedMetrics.srm ? (
+ {calculatedMetrics.srm != null ? (
SRM:
@@ -536,10 +806,9 @@ export default function ImportReviewScreen() {
{["grain", "hop", "yeast", "other"].map(type => {
- const ingredients =
- recipeData.ingredients?.filter(
- (ing: any) => ing.type === type
- ) || [];
+ const ingredients = normalizedIngredients.filter(
+ ing => ing.type === type
+ );
if (ingredients.length === 0) {
return null;
@@ -551,17 +820,23 @@ export default function ImportReviewScreen() {
{type.charAt(0).toUpperCase() + type.slice(1)}s (
{ingredients.length})
- {ingredients.map((ingredient: any, index: number) => (
-
-
- {ingredient.name}
-
-
- {ingredient.amount || 0} {ingredient.unit || ""}
- {ingredient.use && ` • ${ingredient.use}`}
- {ingredient.time > 0 &&
- ` • ${coerceIngredientTime(ingredient.time)} min`}
-
+ {ingredients.map(ingredient => (
+
+
+
+ {ingredient.name}
+
+
+ {ingredient.amount || 0} {ingredient.unit || ""}
+ {ingredient.use && ` • ${ingredient.use}`}
+ {ingredient.time !== undefined && ingredient.time > 0
+ ? ` • ${ingredient.time} min`
+ : ""}
+
+
))}
diff --git a/app/(modals)/(brewSessions)/createBrewSession.tsx b/app/(modals)/(brewSessions)/createBrewSession.tsx
index c227e928..0df91102 100644
--- a/app/(modals)/(brewSessions)/createBrewSession.tsx
+++ b/app/(modals)/(brewSessions)/createBrewSession.tsx
@@ -59,6 +59,7 @@ import {
} from "@utils/formatUtils";
import DateTimePicker from "@react-native-community/datetimepicker";
import { TEST_IDS } from "@src/constants/testIDs";
+import { TemperatureUnit } from "@/src/types";
function toLocalISODateString(d: Date) {
const year = d.getFullYear();
@@ -102,9 +103,8 @@ export default function CreateBrewSessionScreen() {
const { create: createBrewSession } = useBrewSessions();
const { getById: getRecipeById } = useRecipes();
const [showDatePicker, setShowDatePicker] = useState(false);
- const [selectedTemperatureUnit, setSelectedTemperatureUnit] = useState<
- "F" | "C" | null
- >(null);
+ const [selectedTemperatureUnit, setSelectedTemperatureUnit] =
+ useState(null);
const [showUnitPrompt, setShowUnitPrompt] = useState(false);
const [recipe, setRecipe] = useState(null);
const [isLoadingRecipe, setIsLoadingRecipe] = useState(true);
diff --git a/app/(modals)/(brewSessions)/viewBrewSession.tsx b/app/(modals)/(brewSessions)/viewBrewSession.tsx
index 2349c49b..af9f2f77 100644
--- a/app/(modals)/(brewSessions)/viewBrewSession.tsx
+++ b/app/(modals)/(brewSessions)/viewBrewSession.tsx
@@ -23,7 +23,7 @@ import { DryHopTracker } from "@src/components/brewSessions/DryHopTracker";
import { useBrewSessions } from "@hooks/offlineV2/useUserData";
import { ModalHeader } from "@src/components/ui/ModalHeader";
import { QUERY_KEYS } from "@services/api/queryClient";
-import UnifiedLogger from "@services/logger/UnifiedLogger";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
import { DevIdDebugger } from "@src/components/debug/DevIdDebugger";
export default function ViewBrewSession() {
@@ -164,17 +164,8 @@ export default function ViewBrewSession() {
const currentRecipeId = brewSessionData?.recipe_id;
try {
// First try to refresh the overall brew sessions data
- await UnifiedLogger.debug(
- "ViewBrewSession.onRefresh",
- "Calling brewSessionsHook.refresh() to fetch from server"
- );
await brewSessionsHook.refresh();
- await UnifiedLogger.debug(
- "ViewBrewSession.onRefresh",
- "Server refresh complete, now getting specific session from cache"
- );
-
// Then reload the specific session
const session = await brewSessionsHook.getById(brewSessionId);
@@ -801,19 +792,6 @@ export default function ViewBrewSession() {
const updatedSession = await getById(brewSession.id);
if (updatedSession) {
setBrewSessionData(updatedSession);
- await UnifiedLogger.debug(
- "viewBrewSession.onAddDryHop",
- `Session reloaded with ${updatedSession.dry_hop_additions?.length || 0} dry-hops`,
- {
- dryHopAdditions: updatedSession.dry_hop_additions?.map(
- dh => ({
- hop_name: dh.hop_name,
- recipe_instance_id: dh.recipe_instance_id,
- hasInstanceId: !!dh.recipe_instance_id,
- })
- ),
- }
- );
}
}}
onRemoveDryHop={async dryHopIndex => {
diff --git a/app/(modals)/(calculators)/hydrometerCorrection.tsx b/app/(modals)/(calculators)/hydrometerCorrection.tsx
index d962ae03..09ab84c4 100644
--- a/app/(modals)/(calculators)/hydrometerCorrection.tsx
+++ b/app/(modals)/(calculators)/hydrometerCorrection.tsx
@@ -44,8 +44,8 @@ import { calculatorScreenStyles } from "@styles/modals/calculators/calculatorScr
* Temperature unit options for the calculator
*/
const TEMP_UNIT_OPTIONS = [
- { label: "°F", value: "f" as const, description: "Fahrenheit" },
- { label: "°C", value: "c" as const, description: "Celsius" },
+ { label: "°F", value: "F" as const, description: "Fahrenheit" },
+ { label: "°C", value: "C" as const, description: "Celsius" },
];
export default function HydrometerCorrectionCalculatorScreen() {
@@ -165,10 +165,10 @@ export default function HydrometerCorrectionCalculatorScreen() {
}
let converted: number;
- if (fromUnit === "f" && toUnit === "c") {
+ if (fromUnit === "F" && toUnit === "C") {
// °F to °C: (°F - 32) * 5/9
converted = (numValue - 32) * (5 / 9);
- } else if (fromUnit === "c" && toUnit === "f") {
+ } else if (fromUnit === "C" && toUnit === "F") {
// °C to °F: °C * 9/5 + 32
converted = numValue * (9 / 5) + 32;
} else {
@@ -206,7 +206,7 @@ export default function HydrometerCorrectionCalculatorScreen() {
};
const getTempPlaceholder = (isCalibration: boolean = false) => {
- if (hydrometerCorrection.tempUnit === "f") {
+ if (hydrometerCorrection.tempUnit === "F") {
return isCalibration ? "68" : "75";
} else {
return isCalibration ? "20" : "24";
@@ -258,8 +258,8 @@ export default function HydrometerCorrectionCalculatorScreen() {
value={hydrometerCorrection.wortTemp}
onChangeText={handleMeasuredTempChange}
placeholder={getTempPlaceholder()}
- min={hydrometerCorrection.tempUnit === "c" ? 0 : 32}
- max={hydrometerCorrection.tempUnit === "c" ? 100 : 212}
+ min={hydrometerCorrection.tempUnit === "C" ? 0 : 32}
+ max={hydrometerCorrection.tempUnit === "C" ? 100 : 212}
unit={`°${tempUnit}`}
testID="hydrometer-sample-temp"
/>
@@ -271,8 +271,8 @@ export default function HydrometerCorrectionCalculatorScreen() {
value={hydrometerCorrection.calibrationTemp}
onChangeText={handleCalibrationTempChange}
placeholder={getTempPlaceholder(true)}
- min={hydrometerCorrection.tempUnit === "c" ? 0 : 32}
- max={hydrometerCorrection.tempUnit === "c" ? 100 : 212}
+ min={hydrometerCorrection.tempUnit === "C" ? 0 : 32}
+ max={hydrometerCorrection.tempUnit === "C" ? 100 : 212}
unit={`°${tempUnit}`}
helperText="Temperature your hydrometer was calibrated at"
testID="hydrometer-calibration-temp"
diff --git a/app/(modals)/(calculators)/strikeWater.tsx b/app/(modals)/(calculators)/strikeWater.tsx
index a8fb33f2..7a76046c 100644
--- a/app/(modals)/(calculators)/strikeWater.tsx
+++ b/app/(modals)/(calculators)/strikeWater.tsx
@@ -46,13 +46,14 @@ import {
import { useTheme } from "@contexts/ThemeContext";
import { UnitConverter } from "@/src/services/calculators/UnitConverter";
import { calculatorScreenStyles } from "@styles/modals/calculators/calculatorScreenStyles";
+import { TemperatureUnit } from "@/src/types/common";
/**
* Temperature unit options for the calculator
*/
const TEMP_UNIT_OPTIONS = [
- { label: "°F", value: "f" as const, description: "Fahrenheit" },
- { label: "°C", value: "c" as const, description: "Celsius" },
+ { label: "°F", value: "F" as const, description: "Fahrenheit" },
+ { label: "°C", value: "C" as const, description: "Celsius" },
];
/**
@@ -213,8 +214,8 @@ export default function StrikeWaterCalculatorScreen() {
};
const handleTempUnitChange = (tempUnit: string) => {
- const fromUnit = strikeWater.tempUnit as "f" | "c";
- const toUnit = tempUnit as "f" | "c";
+ const fromUnit = strikeWater.tempUnit as TemperatureUnit;
+ const toUnit = tempUnit as TemperatureUnit;
const convert = (v?: string) => {
const n = parseFloat(v ?? "");
@@ -255,7 +256,7 @@ export default function StrikeWaterCalculatorScreen() {
};
const getTempPlaceholder = (isTarget: boolean = false) => {
- if (strikeWater.tempUnit === "f") {
+ if (strikeWater.tempUnit === "F") {
return isTarget ? "152" : "70";
} else {
return isTarget ? "67" : "21";
@@ -263,7 +264,7 @@ export default function StrikeWaterCalculatorScreen() {
};
const getTempRange = () => {
- if (strikeWater.tempUnit === "f") {
+ if (strikeWater.tempUnit === "F") {
return { grain: "32-120°F", mash: "140-170°F" };
} else {
return { grain: "0-50°C", mash: "60-77°C" };
diff --git a/app/(modals)/(calculators)/unitConverter.tsx b/app/(modals)/(calculators)/unitConverter.tsx
index 3df159bd..1b905bd1 100644
--- a/app/(modals)/(calculators)/unitConverter.tsx
+++ b/app/(modals)/(calculators)/unitConverter.tsx
@@ -76,8 +76,8 @@ const VOLUME_UNITS = [
* Temperature unit options for conversion
*/
const TEMPERATURE_UNITS = [
- { label: "°F", value: "f", description: "Fahrenheit" },
- { label: "°C", value: "c", description: "Celsius" },
+ { label: "°F", value: "F", description: "Fahrenheit" },
+ { label: "°C", value: "C", description: "Celsius" },
{ label: "K", value: "k", description: "Kelvin" },
];
diff --git a/app/(modals)/(recipes)/createRecipe.tsx b/app/(modals)/(recipes)/createRecipe.tsx
index 356a77fd..664b1d52 100644
--- a/app/(modals)/(recipes)/createRecipe.tsx
+++ b/app/(modals)/(recipes)/createRecipe.tsx
@@ -44,6 +44,7 @@ import {
RecipeIngredient,
Recipe,
AIAnalysisResponse,
+ UnitSystem,
} from "@src/types";
import { createRecipeStyles } from "@styles/modals/createRecipeStyles";
import { BasicInfoForm } from "@src/components/recipes/RecipeForm/BasicInfoForm";
@@ -84,9 +85,7 @@ const STEP_TITLES = ["Basic Info", "Parameters", "Ingredients", "Review"];
* @param unitSystem - User's preferred unit system ('imperial' or 'metric')
* @returns Initial recipe form data with appropriate units and defaults
*/
-const createInitialRecipeState = (
- unitSystem: "imperial" | "metric"
-): RecipeFormData => ({
+const createInitialRecipeState = (unitSystem: UnitSystem): RecipeFormData => ({
name: "",
style: "",
description: "",
@@ -109,7 +108,7 @@ type RecipeBuilderAction =
| { type: "ADD_INGREDIENT"; ingredient: RecipeIngredient }
| { type: "REMOVE_INGREDIENT"; index: number }
| { type: "UPDATE_INGREDIENT"; index: number; ingredient: RecipeIngredient }
- | { type: "RESET"; unitSystem: "imperial" | "metric" };
+ | { type: "RESET"; unitSystem: UnitSystem };
/**
* Updates the recipe form state based on the specified action.
@@ -277,7 +276,7 @@ export default function CreateRecipeScreen() {
},
onSuccess: response => {
// Invalidate relevant recipe caches to ensure fresh data
- queryClient.invalidateQueries({ queryKey: ["userRecipes"] });
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.USER_RECIPES });
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.RECIPES }); // AllRecipes cache
queryClient.invalidateQueries({
queryKey: [...QUERY_KEYS.RECIPES, "offline"],
diff --git a/app/(modals)/(recipes)/editRecipe.tsx b/app/(modals)/(recipes)/editRecipe.tsx
index b7a9960a..5645fe7e 100644
--- a/app/(modals)/(recipes)/editRecipe.tsx
+++ b/app/(modals)/(recipes)/editRecipe.tsx
@@ -16,7 +16,13 @@ import { useTheme } from "@contexts/ThemeContext";
import { useUnits } from "@contexts/UnitContext";
import { useRecipes } from "@src/hooks/offlineV2";
import { useAuth } from "@contexts/AuthContext";
-import { RecipeFormData, RecipeIngredient, Recipe } from "@src/types";
+import {
+ RecipeFormData,
+ RecipeIngredient,
+ Recipe,
+ RecipeMetrics,
+ UnitSystem,
+} from "@src/types";
import { createRecipeStyles } from "@styles/modals/createRecipeStyles";
import { BasicInfoForm } from "@src/components/recipes/RecipeForm/BasicInfoForm";
import { ParametersForm } from "@src/components/recipes/RecipeForm/ParametersForm";
@@ -141,7 +147,7 @@ const toOptionalNumber = (v: any): number | undefined => {
// Create unit-aware initial recipe state from existing recipe
const createRecipeStateFromExisting = (
existingRecipe: Recipe,
- unitSystem: "imperial" | "metric"
+ unitSystem: UnitSystem
): RecipeFormData => ({
name: existingRecipe.name ?? "",
style: existingRecipe.style ?? "",
@@ -380,6 +386,24 @@ export default function EditRecipeScreen() {
return sanitized;
});
+ const getMetricOrFallback = (
+ metricKey: keyof RecipeMetrics,
+ estimatedKey: keyof Pick<
+ Recipe,
+ | "estimated_og"
+ | "estimated_fg"
+ | "estimated_abv"
+ | "estimated_ibu"
+ | "estimated_srm"
+ >
+ ): number | undefined => {
+ const metricValue = metricsData?.[metricKey];
+ if (metricValue !== undefined && Number.isFinite(metricValue)) {
+ return metricValue as number;
+ }
+ return existingRecipe?.[estimatedKey] as number | undefined;
+ };
+
const updateData = {
name: formData.name || "",
style: formData.style || "",
@@ -416,27 +440,13 @@ export default function EditRecipeScreen() {
is_public: Boolean(formData.is_public),
notes: formData.notes || "",
ingredients: sanitizedIngredients,
- // Include estimated metrics only when finite
- ...(metricsData &&
- Number.isFinite(metricsData.og) && {
- estimated_og: metricsData.og,
- }),
- ...(metricsData &&
- Number.isFinite(metricsData.fg) && {
- estimated_fg: metricsData.fg,
- }),
- ...(metricsData &&
- Number.isFinite(metricsData.abv) && {
- estimated_abv: metricsData.abv,
- }),
- ...(metricsData &&
- Number.isFinite(metricsData.ibu) && {
- estimated_ibu: metricsData.ibu,
- }),
- ...(metricsData &&
- Number.isFinite(metricsData.srm) && {
- estimated_srm: metricsData.srm,
- }),
+ // Include estimated metrics - use newly calculated metrics if available and finite,
+ // otherwise preserve existing recipe metrics to avoid resetting them
+ estimated_og: getMetricOrFallback("og", "estimated_og"),
+ estimated_fg: getMetricOrFallback("fg", "estimated_fg"),
+ estimated_abv: getMetricOrFallback("abv", "estimated_abv"),
+ estimated_ibu: getMetricOrFallback("ibu", "estimated_ibu"),
+ estimated_srm: getMetricOrFallback("srm", "estimated_srm"),
};
const updatedRecipe = await updateRecipeV2(recipe_id, updateData);
diff --git a/app/(modals)/(recipes)/ingredientPicker.tsx b/app/(modals)/(recipes)/ingredientPicker.tsx
index 78feb06a..a80042f6 100644
--- a/app/(modals)/(recipes)/ingredientPicker.tsx
+++ b/app/(modals)/(recipes)/ingredientPicker.tsx
@@ -48,6 +48,7 @@ import {
IngredientUnit,
Ingredient,
HopFormat,
+ UnitSystem,
} from "@src/types";
import { ingredientPickerStyles } from "@styles/modals/ingredientPickerStyles";
import { IngredientDetailEditor } from "@src/components/recipes/IngredientEditor/IngredientDetailEditor";
@@ -154,7 +155,7 @@ const convertIngredientToRecipeIngredient = (
const createRecipeIngredientWithDefaults = (
baseIngredient: RecipeIngredient,
ingredientType: IngredientType,
- unitSystem: "imperial" | "metric"
+ unitSystem: UnitSystem
): RecipeIngredient => {
// Default amounts by type and unit system
const getDefaultAmount = (type: IngredientType): number => {
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 4d679a2e..3c464ac6 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -104,6 +104,7 @@ export default function DashboardScreen() {
data: brewSessions,
isLoading: brewSessionsLoading,
refresh: refreshBrewSessions,
+ delete: deleteBrewSession,
} = useBrewSessions();
// Separate query for public recipes (online-only feature)
@@ -215,7 +216,10 @@ export default function DashboardScreen() {
await ApiService.recipes.delete(recipeId);
},
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: [...QUERY_KEYS.RECIPES] });
+ // Invalidate both recipes and dashboard queries to immediately update UI
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.RECIPES });
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.DASHBOARD });
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.USER_RECIPES });
},
onError: (error: unknown) => {
console.error("Failed to delete recipe:", error);
@@ -241,8 +245,8 @@ export default function DashboardScreen() {
}
},
onSuccess: (response, recipe) => {
- queryClient.invalidateQueries({ queryKey: [...QUERY_KEYS.RECIPES] });
- queryClient.invalidateQueries({ queryKey: [...QUERY_KEYS.DASHBOARD] });
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.RECIPES });
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.DASHBOARD });
// Ensure offline lists reflect the new clone
queryClient.invalidateQueries({
queryKey: [...QUERY_KEYS.RECIPES, "offline"],
@@ -410,9 +414,34 @@ export default function DashboardScreen() {
);
},
onDelete: (brewSession: BrewSession) => {
+ // Close the context menu before prompting
+ brewSessionContextMenu.hideMenu();
Alert.alert(
"Delete Session",
- `Deleting "${brewSession.name}" - Feature coming soon!`
+ `Are you sure you want to delete "${brewSession.name}"? This action cannot be undone.`,
+ [
+ {
+ text: "Cancel",
+ style: "cancel",
+ },
+ {
+ text: "Delete",
+ style: "destructive",
+ onPress: async () => {
+ try {
+ await deleteBrewSession(brewSession.id);
+ Alert.alert("Success", "Brew session deleted successfully");
+ } catch (error) {
+ console.error("Failed to delete brew session:", error);
+ Alert.alert(
+ "Delete Failed",
+ "Failed to delete brew session. Please try again.",
+ [{ text: "OK" }]
+ );
+ }
+ },
+ },
+ ]
);
},
});
diff --git a/package-lock.json b/package-lock.json
index db462527..a4f4de1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "brewtracker",
- "version": "3.2.6",
+ "version": "3.3.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "brewtracker",
- "version": "3.2.6",
+ "version": "3.3.15",
"license": "GPL-3.0-or-later",
"dependencies": {
"@expo/metro-runtime": "~6.1.2",
@@ -24,31 +24,31 @@
"ajv": "^8.17.1",
"axios": "^1.12.2",
"expo": "~54.0.25",
- "expo-blur": "~15.0.7",
- "expo-build-properties": "~1.0.7",
+ "expo-blur": "~15.0.8",
+ "expo-build-properties": "~1.0.10",
"expo-constants": "~18.0.9",
- "expo-crypto": "~15.0.7",
- "expo-dev-client": "~6.0.18",
- "expo-device": "~8.0.9",
- "expo-document-picker": "~14.0.7",
+ "expo-crypto": "~15.0.8",
+ "expo-dev-client": "~6.0.20",
+ "expo-device": "~8.0.10",
+ "expo-document-picker": "~14.0.8",
"expo-file-system": "~19.0.14",
"expo-font": "~14.0.8",
- "expo-haptics": "~15.0.7",
- "expo-image": "~3.0.10",
- "expo-linear-gradient": "~15.0.7",
- "expo-linking": "~8.0.9",
- "expo-local-authentication": "~17.0.7",
- "expo-media-library": "~18.2.0",
- "expo-notifications": "~0.32.13",
- "expo-router": "~6.0.15",
- "expo-secure-store": "~15.0.7",
- "expo-sharing": "~14.0.7",
- "expo-splash-screen": "~31.0.11",
- "expo-status-bar": "~3.0.8",
- "expo-symbols": "~1.0.7",
- "expo-system-ui": "~6.0.8",
- "expo-updates": "~29.0.13",
- "expo-web-browser": "~15.0.9",
+ "expo-haptics": "~15.0.8",
+ "expo-image": "~3.0.11",
+ "expo-linear-gradient": "~15.0.8",
+ "expo-linking": "~8.0.10",
+ "expo-local-authentication": "~17.0.8",
+ "expo-media-library": "~18.2.1",
+ "expo-notifications": "~0.32.14",
+ "expo-router": "~6.0.17",
+ "expo-secure-store": "~15.0.8",
+ "expo-sharing": "~14.0.8",
+ "expo-splash-screen": "~31.0.12",
+ "expo-status-bar": "~3.0.9",
+ "expo-symbols": "~1.0.8",
+ "expo-system-ui": "~6.0.9",
+ "expo-updates": "~29.0.14",
+ "expo-web-browser": "~15.0.10",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
@@ -84,7 +84,7 @@
"eslint-config-expo": "~10.0.0",
"express": "^5.1.0",
"jest": "~29.7.0",
- "jest-expo": "~54.0.13",
+ "jest-expo": "~54.0.14",
"jsdom": "^26.1.0",
"oxlint": "^1.14.0",
"prettier": "^3.4.2",
@@ -2185,7 +2185,7 @@
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/@csstools/color-helpers": {
@@ -2545,40 +2545,40 @@
}
},
"node_modules/@expo/config": {
- "version": "12.0.10",
- "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.10.tgz",
- "integrity": "sha512-lJMof5Nqakq1DxGYlghYB/ogSBjmv4Fxn1ovyDmcjlRsQdFCXgu06gEUogkhPtc9wBt9WlTTfqENln5HHyLW6w==",
+ "version": "12.0.11",
+ "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.11.tgz",
+ "integrity": "sha512-bGKNCbHirwgFlcOJHXpsAStQvM0nU3cmiobK0o07UkTfcUxl9q9lOQQh2eoMGqpm6Vs1IcwBpYye6thC3Nri/w==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "~7.10.4",
- "@expo/config-plugins": "~54.0.2",
- "@expo/config-types": "^54.0.8",
+ "@expo/config-plugins": "~54.0.3",
+ "@expo/config-types": "^54.0.9",
"@expo/json-file": "^10.0.7",
"deepmerge": "^4.3.1",
"getenv": "^2.0.0",
- "glob": "^10.4.2",
+ "glob": "^13.0.0",
"require-from-string": "^2.0.2",
"resolve-from": "^5.0.0",
"resolve-workspace-root": "^2.0.0",
"semver": "^7.6.0",
"slugify": "^1.3.4",
- "sucrase": "3.35.0"
+ "sucrase": "~3.35.1"
}
},
"node_modules/@expo/config-plugins": {
- "version": "54.0.2",
- "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.2.tgz",
- "integrity": "sha512-jD4qxFcURQUVsUFGMcbo63a/AnviK8WUGard+yrdQE3ZrB/aurn68SlApjirQQLEizhjI5Ar2ufqflOBlNpyPg==",
+ "version": "54.0.3",
+ "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.3.tgz",
+ "integrity": "sha512-tBIUZIxLQfCu5jmqTO+UOeeDUGIB0BbK6xTMkPRObAXRQeTLPPfokZRCo818d2owd+Bcmq1wBaDz0VY3g+glfw==",
"license": "MIT",
"dependencies": {
- "@expo/config-types": "^54.0.8",
+ "@expo/config-types": "^54.0.9",
"@expo/json-file": "~10.0.7",
"@expo/plist": "^0.4.7",
"@expo/sdk-runtime-versions": "^1.0.0",
"chalk": "^4.1.2",
"debug": "^4.3.5",
"getenv": "^2.0.0",
- "glob": "^10.4.2",
+ "glob": "^13.0.0",
"resolve-from": "^5.0.0",
"semver": "^7.5.4",
"slash": "^3.0.0",
@@ -2587,45 +2587,42 @@
"xml2js": "0.6.0"
}
},
- "node_modules/@expo/config-plugins/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
"node_modules/@expo/config-plugins/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "license": "ISC",
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
+ "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
+ "minimatch": "^10.1.1",
"minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "path-scurry": "^2.0.0"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/@expo/config-plugins/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/@expo/config-plugins/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -2640,6 +2637,22 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/@expo/config-plugins/node_modules/path-scurry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
+ "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@expo/config-plugins/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -2653,9 +2666,9 @@
}
},
"node_modules/@expo/config-types": {
- "version": "54.0.8",
- "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.8.tgz",
- "integrity": "sha512-lyIn/x/Yz0SgHL7IGWtgTLg6TJWC9vL7489++0hzCHZ4iGjVcfZmPTUfiragZ3HycFFj899qN0jlhl49IHa94A==",
+ "version": "54.0.9",
+ "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.9.tgz",
+ "integrity": "sha512-Llf4jwcrAnrxgE5WCdAOxtMf8FGwS4Sk0SSgI0NnIaSyCnmOCAm80GPFvsK778Oj19Ub4tSyzdqufPyeQPksWw==",
"license": "MIT"
},
"node_modules/@expo/config/node_modules/@babel/code-frame": {
@@ -2667,45 +2680,42 @@
"@babel/highlight": "^7.10.4"
}
},
- "node_modules/@expo/config/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
"node_modules/@expo/config/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "license": "ISC",
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
+ "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
+ "minimatch": "^10.1.1",
"minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "path-scurry": "^2.0.0"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/@expo/config/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/@expo/config/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -2720,6 +2730,22 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/@expo/config/node_modules/path-scurry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
+ "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@expo/config/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -2733,23 +2759,13 @@
}
},
"node_modules/@expo/devcert": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.0.tgz",
- "integrity": "sha512-Uilcv3xGELD5t/b0eM4cxBFEKQRIivB3v7i+VhWLV/gL98aw810unLKKJbGAxAIhY6Ipyz8ChWibFsKFXYwstA==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz",
+ "integrity": "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==",
"license": "MIT",
"dependencies": {
"@expo/sudo-prompt": "^9.3.1",
- "debug": "^3.1.0",
- "glob": "^10.4.2"
- }
- },
- "node_modules/@expo/devcert/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
+ "debug": "^3.1.0"
}
},
"node_modules/@expo/devcert/node_modules/debug": {
@@ -2761,54 +2777,10 @@
"ms": "^2.1.1"
}
},
- "node_modules/@expo/devcert/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@expo/devcert/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@expo/devcert/node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
"node_modules/@expo/devtools": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-0.1.7.tgz",
- "integrity": "sha512-dfIa9qMyXN+0RfU6SN4rKeXZyzKWsnz6xBSDccjL4IRiE+fQ0t84zg0yxgN4t/WK2JU5v6v4fby7W7Crv9gJvA==",
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-0.1.8.tgz",
+ "integrity": "sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2"
@@ -2827,9 +2799,9 @@
}
},
"node_modules/@expo/env": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.7.tgz",
- "integrity": "sha512-BNETbLEohk3HQ2LxwwezpG8pq+h7Fs7/vAMP3eAtFT1BCpprLYoBBFZH7gW4aqGfqOcVP4Lc91j014verrYNGg==",
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.8.tgz",
+ "integrity": "sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA==",
"license": "MIT",
"dependencies": {
"chalk": "^4.0.0",
@@ -2852,9 +2824,9 @@
}
},
"node_modules/@expo/fingerprint": {
- "version": "0.15.3",
- "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.15.3.tgz",
- "integrity": "sha512-8YPJpEYlmV171fi+t+cSLMX1nC5ngY9j2FiN70dHldLpd6Ct6ouGhk96svJ4BQZwsqwII2pokwzrDAwqo4Z0FQ==",
+ "version": "0.15.4",
+ "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.15.4.tgz",
+ "integrity": "sha512-eYlxcrGdR2/j2M6pEDXo9zU9KXXF1vhP+V+Tl+lyY+bU8lnzrN6c637mz6Ye3em2ANy8hhUR03Raf8VsT9Ogng==",
"license": "MIT",
"dependencies": {
"@expo/spawn-async": "^1.7.2",
@@ -2862,7 +2834,7 @@
"chalk": "^4.1.2",
"debug": "^4.3.4",
"getenv": "^2.0.0",
- "glob": "^10.4.2",
+ "glob": "^13.0.0",
"ignore": "^5.3.1",
"minimatch": "^9.0.0",
"p-limit": "^3.1.0",
@@ -2883,25 +2855,46 @@
}
},
"node_modules/@expo/fingerprint/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "license": "ISC",
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
+ "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
+ "minimatch": "^10.1.1",
"minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "path-scurry": "^2.0.0"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@expo/fingerprint/node_modules/glob/node_modules/minimatch": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/brace-expansion": "^5.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/@expo/fingerprint/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/@expo/fingerprint/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -2926,6 +2919,22 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/@expo/fingerprint/node_modules/path-scurry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
+ "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@expo/fingerprint/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -2939,9 +2948,9 @@
}
},
"node_modules/@expo/image-utils": {
- "version": "0.8.7",
- "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.7.tgz",
- "integrity": "sha512-SXOww4Wq3RVXLyOaXiCCuQFguCDh8mmaHBv54h/R29wGl4jRY8GEyQEx8SypV/iHt1FbzsU/X3Qbcd9afm2W2w==",
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.8.tgz",
+ "integrity": "sha512-HHHaG4J4nKjTtVa1GG9PCh763xlETScfEyNxxOvfTRr8IKPJckjTyqSLEtdJoFNJ1vqiABEjW7tqGhqGibZLeA==",
"license": "MIT",
"dependencies": {
"@expo/spawn-async": "^1.7.2",
@@ -2969,9 +2978,9 @@
}
},
"node_modules/@expo/json-file": {
- "version": "10.0.7",
- "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.7.tgz",
- "integrity": "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw==",
+ "version": "10.0.8",
+ "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.8.tgz",
+ "integrity": "sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "~7.10.4",
@@ -2987,25 +2996,6 @@
"@babel/highlight": "^7.10.4"
}
},
- "node_modules/@expo/mcp-tunnel": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/@expo/mcp-tunnel/-/mcp-tunnel-0.1.0.tgz",
- "integrity": "sha512-rJ6hl0GnIZj9+ssaJvFsC7fwyrmndcGz+RGFzu+0gnlm78X01957yjtHgjcmnQAgL5hWEOR6pkT0ijY5nU5AWw==",
- "license": "MIT",
- "dependencies": {
- "ws": "^8.18.3",
- "zod": "^3.25.76",
- "zod-to-json-schema": "^3.24.6"
- },
- "peerDependencies": {
- "@modelcontextprotocol/sdk": "^1.13.2"
- },
- "peerDependenciesMeta": {
- "@modelcontextprotocol/sdk": {
- "optional": true
- }
- }
- },
"node_modules/@expo/metro": {
"version": "54.1.0",
"resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.1.0.tgz",
@@ -3027,15 +3017,15 @@
}
},
"node_modules/@expo/metro-config": {
- "version": "54.0.9",
- "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.9.tgz",
- "integrity": "sha512-CRI4WgFXrQ2Owyr8q0liEBJveUIF9DcYAKadMRsJV7NxGNBdrIIKzKvqreDfsGiRqivbLsw6UoNb3UE7/SvPfg==",
+ "version": "54.0.10",
+ "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.10.tgz",
+ "integrity": "sha512-AkSTwaWbMMDOiV4RRy4Mv6MZEOW5a7BZlgtrWxvzs6qYKRxKLKH/qqAuKe0bwGepF1+ws9oIX5nQjtnXRwezvQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.20.0",
"@babel/core": "^7.20.0",
"@babel/generator": "^7.20.5",
- "@expo/config": "~12.0.10",
+ "@expo/config": "~12.0.11",
"@expo/env": "~2.0.7",
"@expo/json-file": "~10.0.7",
"@expo/metro": "~54.1.0",
@@ -3046,7 +3036,7 @@
"dotenv": "~16.4.5",
"dotenv-expand": "~11.0.6",
"getenv": "^2.0.0",
- "glob": "^10.4.2",
+ "glob": "^13.0.0",
"hermes-parser": "^0.29.1",
"jsc-safe-url": "^0.2.4",
"lightningcss": "^1.30.1",
@@ -3085,25 +3075,46 @@
}
},
"node_modules/@expo/metro-config/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "license": "ISC",
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
+ "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
+ "minimatch": "^10.1.1",
"minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "path-scurry": "^2.0.0"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@expo/metro-config/node_modules/glob/node_modules/minimatch": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/brace-expansion": "^5.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/@expo/metro-config/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/@expo/metro-config/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -3128,6 +3139,22 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/@expo/metro-config/node_modules/path-scurry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
+ "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@expo/metro-runtime": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz",
@@ -3484,9 +3511,9 @@
}
},
"node_modules/@expo/osascript": {
- "version": "2.3.7",
- "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.7.tgz",
- "integrity": "sha512-IClSOXxR0YUFxIriUJVqyYki7lLMIHrrzOaP01yxAL1G8pj2DWV5eW1y5jSzIcIfSCNhtGsshGd1tU/AYup5iQ==",
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.8.tgz",
+ "integrity": "sha512-/TuOZvSG7Nn0I8c+FcEaoHeBO07yu6vwDgk7rZVvAXoeAK5rkA09jRyjYsZo+0tMEFaToBeywA6pj50Mb3ny9w==",
"license": "MIT",
"dependencies": {
"@expo/spawn-async": "^1.7.2",
@@ -3497,12 +3524,12 @@
}
},
"node_modules/@expo/package-manager": {
- "version": "1.9.8",
- "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.8.tgz",
- "integrity": "sha512-4/I6OWquKXYnzo38pkISHCOCOXxfeEmu4uDoERq1Ei/9Ur/s9y3kLbAamEkitUkDC7gHk1INxRWEfFNzGbmOrA==",
+ "version": "1.9.9",
+ "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.9.tgz",
+ "integrity": "sha512-Nv5THOwXzPprMJwbnXU01iXSrCp3vJqly9M4EJ2GkKko9Ifer2ucpg7x6OUsE09/lw+npaoUnHMXwkw7gcKxlg==",
"license": "MIT",
"dependencies": {
- "@expo/json-file": "^10.0.7",
+ "@expo/json-file": "^10.0.8",
"@expo/spawn-async": "^1.7.2",
"chalk": "^4.0.0",
"npm-package-arg": "^11.0.0",
@@ -3511,9 +3538,9 @@
}
},
"node_modules/@expo/plist": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.7.tgz",
- "integrity": "sha512-dGxqHPvCZKeRKDU1sJZMmuyVtcASuSYh1LPFVaM1DuffqPL36n6FMEL0iUqq2Tx3xhWk8wCnWl34IKplUjJDdA==",
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz",
+ "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==",
"license": "MIT",
"dependencies": {
"@xmldom/xmldom": "^0.8.8",
@@ -3522,16 +3549,16 @@
}
},
"node_modules/@expo/prebuild-config": {
- "version": "54.0.6",
- "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.6.tgz",
- "integrity": "sha512-xowuMmyPNy+WTNq+YX0m0EFO/Knc68swjThk4dKivgZa8zI1UjvFXOBIOp8RX4ljCXLzwxQJM5oBBTvyn+59ZA==",
+ "version": "54.0.7",
+ "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.7.tgz",
+ "integrity": "sha512-cKqBsiwcFFzpDWgtvemrCqJULJRLDLKo2QMF74NusoGNpfPI3vQVry1iwnYLeGht02AeD3dvfhpqBczD3wchxA==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~12.0.10",
- "@expo/config-plugins": "~54.0.2",
- "@expo/config-types": "^54.0.8",
- "@expo/image-utils": "^0.8.7",
- "@expo/json-file": "^10.0.7",
+ "@expo/config": "~12.0.11",
+ "@expo/config-plugins": "~54.0.3",
+ "@expo/config-types": "^54.0.9",
+ "@expo/image-utils": "^0.8.8",
+ "@expo/json-file": "^10.0.8",
"@react-native/normalize-colors": "0.81.5",
"debug": "^4.3.1",
"resolve-from": "^5.0.0",
@@ -3543,9 +3570,9 @@
}
},
"node_modules/@expo/prebuild-config/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -3555,9 +3582,9 @@
}
},
"node_modules/@expo/schema-utils": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.7.tgz",
- "integrity": "sha512-jWHoSuwRb5ZczjahrychMJ3GWZu54jK9ulNdh1d4OzAEq672K9E5yOlnlBsfIHWHGzUAT+0CL7Yt1INiXTz68g==",
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.8.tgz",
+ "integrity": "sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A==",
"license": "MIT"
},
"node_modules/@expo/sdk-runtime-versions": {
@@ -3747,121 +3774,46 @@
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
"license": "MIT"
},
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "license": "ISC",
- "dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "node_modules/@isaacs/balanced-match": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
+ "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ "node": "20 || >=22"
}
},
- "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "node_modules/@isaacs/brace-expansion": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
+ "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
- "engines": {
- "node": ">=12"
+ "dependencies": {
+ "@isaacs/balanced-match": "^4.0.1"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "engines": {
+ "node": "20 || >=22"
}
},
- "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "license": "MIT"
- },
- "node_modules/@isaacs/cliui/node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "license": "MIT",
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "license": "ISC",
"dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
+ "minipass": "^7.0.4"
},
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=18.0.0"
}
},
- "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "node_modules/@isaacs/fs-minipass/node_modules/minipass": {
"version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/@isaacs/fs-minipass": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
- "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
- "license": "ISC",
- "dependencies": {
- "minipass": "^7.0.4"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@isaacs/fs-minipass/node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": ">=16 || 14 >=14.17"
}
},
"node_modules/@isaacs/ttlcache": {
@@ -3902,7 +3854,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz",
"integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@@ -3920,7 +3872,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz",
"integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/console": "^29.7.0",
@@ -3980,7 +3932,7 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
"integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4019,7 +3971,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
"integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"expect": "^29.7.0",
@@ -4033,7 +3985,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
"integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"jest-get-type": "^29.6.3"
@@ -4077,7 +4029,7 @@
"version": "30.1.0",
"resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz",
"integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4087,7 +4039,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
"integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/environment": "^29.7.0",
@@ -4103,7 +4055,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
"integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@@ -4118,7 +4070,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
"integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^0.2.3",
@@ -4163,7 +4115,7 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -4184,7 +4136,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
"integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.23.9",
@@ -4201,7 +4153,7 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -4226,7 +4178,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz",
"integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.18",
@@ -4241,7 +4193,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz",
"integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/console": "^29.7.0",
@@ -4257,7 +4209,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz",
"integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/test-result": "^29.7.0",
@@ -4540,66 +4492,12 @@
"win32"
]
},
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=14"
- }
- },
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
- "node_modules/@radix-ui/react-collection": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
- "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
- "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -4630,60 +4528,6 @@
}
}
},
- "node_modules/@radix-ui/react-dialog": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
- "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-focus-guards": "1.1.3",
- "@radix-ui/react-focus-scope": "1.1.7",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
- "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@@ -4699,33 +4543,6 @@
}
}
},
- "node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
- "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-escape-keydown": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
@@ -4741,31 +4558,6 @@
}
}
},
- "node_modules/@radix-ui/react-focus-scope": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
- "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -4784,126 +4576,6 @@
}
}
},
- "node_modules/@radix-ui/react-portal": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
- "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-presence": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
- "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-primitive": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
- "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
- "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-roving-focus": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
- "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
@@ -4922,36 +4594,6 @@
}
}
},
- "node_modules/@radix-ui/react-tabs": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
- "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-roving-focus": "1.1.11",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@@ -5536,7 +5178,7 @@
"version": "13.3.3",
"resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.3.3.tgz",
"integrity": "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"jest-matcher-utils": "^30.0.5",
@@ -5563,7 +5205,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -5576,14 +5218,14 @@
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/@testing-library/react-native/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -5596,7 +5238,7 @@
"version": "30.1.2",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz",
"integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/diff-sequences": "30.0.1",
@@ -5612,7 +5254,7 @@
"version": "30.1.2",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz",
"integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
@@ -5628,7 +5270,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz",
"integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -5643,7 +5285,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/@tootallnate/once": {
@@ -5708,35 +5350,11 @@
"@babel/types": "^7.28.2"
}
},
- "node_modules/@types/eslint": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
- "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/estree": "*",
- "@types/json-schema": "*"
- }
- },
- "node_modules/@types/eslint-scope": {
- "version": "3.7.7",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
- "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/eslint": "*",
- "@types/estree": "*"
- }
- },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
@@ -5805,7 +5423,7 @@
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/json5": {
@@ -5828,7 +5446,7 @@
"version": "19.1.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -6445,182 +6063,6 @@
"@urql/core": "^5.0.0"
}
},
- "node_modules/@webassemblyjs/ast": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
- "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/helper-numbers": "1.13.2",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
- }
- },
- "node_modules/@webassemblyjs/floating-point-hex-parser": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
- "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
- "devOptional": true,
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/helper-api-error": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
- "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/helper-buffer": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
- "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
- "devOptional": true,
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/helper-numbers": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
- "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/floating-point-hex-parser": "1.13.2",
- "@webassemblyjs/helper-api-error": "1.13.2",
- "@xtuc/long": "4.2.2"
- }
- },
- "node_modules/@webassemblyjs/helper-wasm-bytecode": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
- "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
- "devOptional": true,
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/helper-wasm-section": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
- "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-buffer": "1.14.1",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
- "@webassemblyjs/wasm-gen": "1.14.1"
- }
- },
- "node_modules/@webassemblyjs/ieee754": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
- "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@xtuc/ieee754": "^1.2.0"
- }
- },
- "node_modules/@webassemblyjs/leb128": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
- "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
- "devOptional": true,
- "license": "Apache-2.0",
- "peer": true,
- "dependencies": {
- "@xtuc/long": "4.2.2"
- }
- },
- "node_modules/@webassemblyjs/utf8": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
- "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/wasm-edit": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
- "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-buffer": "1.14.1",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
- "@webassemblyjs/helper-wasm-section": "1.14.1",
- "@webassemblyjs/wasm-gen": "1.14.1",
- "@webassemblyjs/wasm-opt": "1.14.1",
- "@webassemblyjs/wasm-parser": "1.14.1",
- "@webassemblyjs/wast-printer": "1.14.1"
- }
- },
- "node_modules/@webassemblyjs/wasm-gen": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
- "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
- "@webassemblyjs/ieee754": "1.13.2",
- "@webassemblyjs/leb128": "1.13.2",
- "@webassemblyjs/utf8": "1.13.2"
- }
- },
- "node_modules/@webassemblyjs/wasm-opt": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
- "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-buffer": "1.14.1",
- "@webassemblyjs/wasm-gen": "1.14.1",
- "@webassemblyjs/wasm-parser": "1.14.1"
- }
- },
- "node_modules/@webassemblyjs/wasm-parser": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
- "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-api-error": "1.13.2",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
- "@webassemblyjs/ieee754": "1.13.2",
- "@webassemblyjs/leb128": "1.13.2",
- "@webassemblyjs/utf8": "1.13.2"
- }
- },
- "node_modules/@webassemblyjs/wast-printer": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
- "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@xtuc/long": "4.2.2"
- }
- },
"node_modules/@xmldom/xmldom": {
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
@@ -6630,22 +6072,6 @@
"node": ">=10.0.0"
}
},
- "node_modules/@xtuc/ieee754": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
- "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
- "devOptional": true,
- "license": "BSD-3-Clause",
- "peer": true
- },
- "node_modules/@xtuc/long": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
- "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
- "devOptional": true,
- "license": "Apache-2.0",
- "peer": true
- },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -6702,20 +6128,6 @@
"acorn-walk": "^8.0.2"
}
},
- "node_modules/acorn-import-phases": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
- "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=10.13.0"
- },
- "peerDependencies": {
- "acorn": "^8.14.0"
- }
- },
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@@ -6726,19 +6138,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
- "node_modules/acorn-loose": {
- "version": "8.5.2",
- "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz",
- "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "acorn": "^8.15.0"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
@@ -6777,39 +6176,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/ajv-formats": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
- "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "ajv": {
- "optional": true
- }
- }
- },
- "node_modules/ajv-keywords": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
- "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.3"
- },
- "peerDependencies": {
- "ajv": "^8.8.2"
- }
- },
"node_modules/anser": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
@@ -7255,9 +6621,9 @@
}
},
"node_modules/babel-plugin-react-native-web": {
- "version": "0.21.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.1.tgz",
- "integrity": "sha512-7XywfJ5QIRMwjOL+pwJt2w47Jmi5fFLvK7/So4fV4jIN6PcRbylCp9/l3cJY4VJbSz3lnWTeHDTD1LKIc1C09Q==",
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz",
+ "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==",
"license": "MIT"
},
"node_modules/babel-plugin-syntax-hermes-parser": {
@@ -7305,9 +6671,9 @@
}
},
"node_modules/babel-preset-expo": {
- "version": "54.0.7",
- "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.7.tgz",
- "integrity": "sha512-JENWk0bvxW4I1ftveO8GRtX2t2TH6N4Z0TPvIHxroZ/4SswUfyNsUNbbP7Fm4erj3ar/JHGri5kTZ+s3xdjHZw==",
+ "version": "54.0.8",
+ "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.8.tgz",
+ "integrity": "sha512-3ZJ4Q7uQpm8IR/C9xbKhE/IUjGpLm+OIjF8YCedLgqoe/wN1Ns2wLT7HwG6ZXXb6/rzN8IMCiKFQ2F93qlN6GA==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.25.9",
@@ -7647,7 +7013,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -7702,7 +7068,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
"integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -7735,17 +7101,6 @@
"node": ">=12.13.0"
}
},
- "node_modules/chrome-trace-event": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
- "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=6.0"
- }
- },
"node_modules/chromium-edge-launcher": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz",
@@ -7779,7 +7134,7 @@
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/cli-cursor": {
@@ -7839,7 +7194,7 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"iojs": ">= 1.0.0",
@@ -7850,7 +7205,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
"integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/color": {
@@ -8071,7 +7426,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
"integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@@ -8196,7 +7551,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/data-urls": {
@@ -8304,7 +7659,7 @@
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
"integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"peerDependencies": {
"babel-plugin-macros": "^3.1.0"
@@ -8436,7 +7791,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -8452,7 +7807,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
"integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -8606,12 +7961,6 @@
"node": ">= 0.4"
}
},
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "license": "MIT"
- },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -8628,7 +7977,7 @@
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
"integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -8646,25 +7995,10 @@
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/enhanced-resolve": {
- "version": "5.18.3",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
- "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
- },
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
"engines": {
- "node": ">=10.13.0"
+ "node": ">= 0.8"
}
},
"node_modules/entities": {
@@ -8693,7 +8027,7 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
@@ -8823,14 +8157,6 @@
"node": ">= 0.4"
}
},
- "node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
- "devOptional": true,
- "license": "MIT",
- "peer": true
- },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -9394,7 +8720,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@@ -9407,7 +8733,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -9441,17 +8767,6 @@
"node": ">=6"
}
},
- "node_modules/events": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
- "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=0.8.x"
- }
- },
"node_modules/exec-async": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
@@ -9462,7 +8777,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.3",
@@ -9486,7 +8801,7 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
"integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
- "devOptional": true,
+ "dev": true,
"engines": {
"node": ">= 0.8.0"
}
@@ -9495,7 +8810,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
"integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/expect-utils": "^29.7.0",
@@ -9509,29 +8824,29 @@
}
},
"node_modules/expo": {
- "version": "54.0.25",
- "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.25.tgz",
- "integrity": "sha512-+iSeBJfHRHzNPnHMZceEXhSGw4t5bNqFyd/5xMUoGfM+39rO7F72wxiLRpBKj0M6+0GQtMaEs+eTbcCrO7XyJQ==",
+ "version": "54.0.27",
+ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.27.tgz",
+ "integrity": "sha512-50BcJs8eqGwRiMUoWwphkRGYtKFS2bBnemxLzy0lrGVA1E6F4Q7L5h3WT6w1ehEZybtOVkfJu4Z6GWo2IJcpEA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.0",
- "@expo/cli": "54.0.16",
- "@expo/config": "~12.0.10",
- "@expo/config-plugins": "~54.0.2",
- "@expo/devtools": "0.1.7",
- "@expo/fingerprint": "0.15.3",
+ "@expo/cli": "54.0.18",
+ "@expo/config": "~12.0.11",
+ "@expo/config-plugins": "~54.0.3",
+ "@expo/devtools": "0.1.8",
+ "@expo/fingerprint": "0.15.4",
"@expo/metro": "~54.1.0",
- "@expo/metro-config": "54.0.9",
+ "@expo/metro-config": "54.0.10",
"@expo/vector-icons": "^15.0.3",
"@ungap/structured-clone": "^1.3.0",
- "babel-preset-expo": "~54.0.7",
- "expo-asset": "~12.0.10",
- "expo-constants": "~18.0.10",
- "expo-file-system": "~19.0.19",
- "expo-font": "~14.0.9",
- "expo-keep-awake": "~15.0.7",
- "expo-modules-autolinking": "3.0.22",
- "expo-modules-core": "3.0.26",
+ "babel-preset-expo": "~54.0.8",
+ "expo-asset": "~12.0.11",
+ "expo-constants": "~18.0.11",
+ "expo-file-system": "~19.0.20",
+ "expo-font": "~14.0.10",
+ "expo-keep-awake": "~15.0.8",
+ "expo-modules-autolinking": "3.0.23",
+ "expo-modules-core": "3.0.28",
"pretty-format": "^29.7.0",
"react-refresh": "^0.14.2",
"whatwg-url-without-unicode": "8.0.0-3"
@@ -9561,22 +8876,22 @@
}
},
"node_modules/expo-application": {
- "version": "7.0.7",
- "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.7.tgz",
- "integrity": "sha512-Jt1/qqnoDUbZ+bK91+dHaZ1vrPDtRBOltRa681EeedkisqguuEeUx4UHqwVyDK2oHWsK6lO3ojetoA4h8OmNcg==",
+ "version": "7.0.8",
+ "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
+ "integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-asset": {
- "version": "12.0.10",
- "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.10.tgz",
- "integrity": "sha512-pZyeJkoDsALh4gpCQDzTA/UCLaPH/1rjQNGubmLn/uDM27S4iYJb/YWw4+CNZOtd5bCUOhDPg5DtGQnydNFSXg==",
+ "version": "12.0.11",
+ "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.11.tgz",
+ "integrity": "sha512-pnK/gQ5iritDPBeK54BV35ZpG7yeW5DtgGvJHruIXkyDT9BCoQq3i0AAxfcWG/e4eiRmTzAt5kNVYFJi48uo+A==",
"license": "MIT",
"dependencies": {
- "@expo/image-utils": "^0.8.7",
- "expo-constants": "~18.0.10"
+ "@expo/image-utils": "^0.8.8",
+ "expo-constants": "~18.0.11"
},
"peerDependencies": {
"expo": "*",
@@ -9585,9 +8900,9 @@
}
},
"node_modules/expo-blur": {
- "version": "15.0.7",
- "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-15.0.7.tgz",
- "integrity": "sha512-SugQQbQd+zRPy8z2G5qDD4NqhcD7srBF7fN7O7yq6q7ZFK59VWvpDxtMoUkmSfdxgqONsrBN/rLdk00USADrMg==",
+ "version": "15.0.8",
+ "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-15.0.8.tgz",
+ "integrity": "sha512-rWyE1NBRZEu9WD+X+5l7gyPRszw7n12cW3IRNAb5i6KFzaBp8cxqT5oeaphJapqURvcqhkOZn2k5EtBSbsuU7w==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
@@ -9596,9 +8911,9 @@
}
},
"node_modules/expo-build-properties": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-1.0.9.tgz",
- "integrity": "sha512-2icttCy3OPTk/GWIFt+vwA+0hup53jnmYb7JKRbvNvrrOrz+WblzpeoiaOleI2dYG/vjwpNO8to8qVyKhYJtrQ==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-1.0.10.tgz",
+ "integrity": "sha512-mFCZbrbrv0AP5RB151tAoRzwRJelqM7bCJzCkxpu+owOyH+p/rFC/q7H5q8B9EpVWj8etaIuszR+gKwohpmu1Q==",
"license": "MIT",
"dependencies": {
"ajv": "^8.11.0",
@@ -9621,13 +8936,13 @@
}
},
"node_modules/expo-constants": {
- "version": "18.0.10",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz",
- "integrity": "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw==",
+ "version": "18.0.11",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.11.tgz",
+ "integrity": "sha512-xnfrfZ7lHjb+03skhmDSYeFF7OU2K3Xn/lAeP+7RhkV2xp2f5RCKtOUYajCnYeZesvMrsUxOsbGOP2JXSOH3NA==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~12.0.10",
- "@expo/env": "~2.0.7"
+ "@expo/config": "~12.0.11",
+ "@expo/env": "~2.0.8"
},
"peerDependencies": {
"expo": "*",
@@ -9635,9 +8950,9 @@
}
},
"node_modules/expo-crypto": {
- "version": "15.0.7",
- "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.7.tgz",
- "integrity": "sha512-FUo41TwwGT2e5rA45PsjezI868Ch3M6wbCZsmqTWdF/hr+HyPcrp1L//dsh/hsrsyrQdpY/U96Lu71/wXePJeg==",
+ "version": "15.0.8",
+ "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.8.tgz",
+ "integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0"
@@ -9647,15 +8962,15 @@
}
},
"node_modules/expo-dev-client": {
- "version": "6.0.18",
- "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.18.tgz",
- "integrity": "sha512-8QKWvhsoZpMkecAMlmWoRHnaTNiPS3aO7E42spZOMjyiaNRJMHZsnB8W2b63dt3Yg3oLyskLAoI8IOmnqVX8vA==",
+ "version": "6.0.20",
+ "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.20.tgz",
+ "integrity": "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==",
"license": "MIT",
"dependencies": {
- "expo-dev-launcher": "6.0.18",
- "expo-dev-menu": "7.0.17",
+ "expo-dev-launcher": "6.0.20",
+ "expo-dev-menu": "7.0.18",
"expo-dev-menu-interface": "2.0.0",
- "expo-manifests": "~1.0.9",
+ "expo-manifests": "~1.0.10",
"expo-updates-interface": "~2.0.0"
},
"peerDependencies": {
@@ -9663,22 +8978,23 @@
}
},
"node_modules/expo-dev-launcher": {
- "version": "6.0.18",
- "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.18.tgz",
- "integrity": "sha512-JTtcIfNvHO9PTdRJLmHs+7HJILXXZjF95jxgzu6hsJrgsTg/AZDtEsIt/qa6ctEYQTqrLdsLDgDhiXVel3AoQA==",
+ "version": "6.0.20",
+ "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz",
+ "integrity": "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==",
"license": "MIT",
"dependencies": {
- "expo-dev-menu": "7.0.17",
- "expo-manifests": "~1.0.9"
+ "ajv": "^8.11.0",
+ "expo-dev-menu": "7.0.18",
+ "expo-manifests": "~1.0.10"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-menu": {
- "version": "7.0.17",
- "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.17.tgz",
- "integrity": "sha512-NIu7TdaZf+A8+DROa6BB6lDfxjXxwaD+Q8QbNSVa0E0x6yl3P0ZJ80QbD2cCQeBzlx3Ufd3hNhczQWk4+A29HQ==",
+ "version": "7.0.18",
+ "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz",
+ "integrity": "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==",
"license": "MIT",
"dependencies": {
"expo-dev-menu-interface": "2.0.0"
@@ -9697,9 +9013,9 @@
}
},
"node_modules/expo-device": {
- "version": "8.0.9",
- "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.9.tgz",
- "integrity": "sha512-XqRpaljDNAYZGZzMpC+b9KZfzfydtkwx3pJAp6ODDH+O/5wjAw+mLc5wQMGJCx8/aqVmMsAokec7iebxDPFZDA==",
+ "version": "8.0.10",
+ "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
+ "integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
"license": "MIT",
"dependencies": {
"ua-parser-js": "^0.7.33"
@@ -9735,9 +9051,9 @@
}
},
"node_modules/expo-document-picker": {
- "version": "14.0.7",
- "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.7.tgz",
- "integrity": "sha512-81Jh8RDD0GYBUoSTmIBq30hXXjmkDV1ZY2BNIp1+3HR5PDSh2WmdhD/Ezz5YFsv46hIXHsQc+Kh1q8vn6OLT9Q==",
+ "version": "14.0.8",
+ "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.8.tgz",
+ "integrity": "sha512-3tyQKpPqWWFlI8p9RiMX1+T1Zge5mEKeBuXWp1h8PEItFMUDSiOJbQ112sfdC6Hxt8wSxreV9bCRl/NgBdt+fA==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
@@ -9750,9 +9066,9 @@
"license": "MIT"
},
"node_modules/expo-file-system": {
- "version": "19.0.19",
- "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.19.tgz",
- "integrity": "sha512-OrpOV4fEBFMFv+jy7PnENpPbsWoBmqWGidSwh1Ai52PLl6JIInYGfZTc6kqyPNGtFTwm7Y9mSWnE8g+dtLxu7g==",
+ "version": "19.0.20",
+ "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.20.tgz",
+ "integrity": "sha512-Jr/nNvJmUlptS3cHLKVBNyTyGMHNyxYBKRph1KRe0Nb3RzZza1gZLZXMG5Ky//sO2azTn+OaT0dv/lAyL0vJNA==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
@@ -9760,9 +9076,9 @@
}
},
"node_modules/expo-font": {
- "version": "14.0.9",
- "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.9.tgz",
- "integrity": "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg==",
+ "version": "14.0.10",
+ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz",
+ "integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==",
"license": "MIT",
"dependencies": {
"fontfaceobserver": "^2.1.0"
@@ -9774,18 +9090,18 @@
}
},
"node_modules/expo-haptics": {
- "version": "15.0.7",
- "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-15.0.7.tgz",
- "integrity": "sha512-7flWsYPrwjJxZ8x82RiJtzsnk1Xp9ahnbd9PhCy3NnsemyMApoWIEUr4waPqFr80DtiLZfhD9VMLL1CKa8AImQ==",
+ "version": "15.0.8",
+ "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-15.0.8.tgz",
+ "integrity": "sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-3.0.10.tgz",
- "integrity": "sha512-i4qNCEf9Ur7vDqdfDdFfWnNCAF2efDTdahuDy9iELPS2nzMKBLeeGA2KxYEPuRylGCS96Rwm+SOZJu6INc2ADQ==",
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-3.0.11.tgz",
+ "integrity": "sha512-4TudfUCLgYgENv+f48omnU8tjS2S0Pd9EaON5/s1ZUBRwZ7K8acEr4NfvLPSaeXvxW24iLAiyQ7sV7BXQH3RoA==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
@@ -9806,9 +9122,9 @@
"license": "MIT"
},
"node_modules/expo-keep-awake": {
- "version": "15.0.7",
- "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.7.tgz",
- "integrity": "sha512-CgBNcWVPnrIVII5G54QDqoE125l+zmqR4HR8q+MQaCfHet+dYpS5vX5zii/RMayzGN4jPgA4XYIQ28ePKFjHoA==",
+ "version": "15.0.8",
+ "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz",
+ "integrity": "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
@@ -9816,9 +9132,9 @@
}
},
"node_modules/expo-linear-gradient": {
- "version": "15.0.7",
- "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.7.tgz",
- "integrity": "sha512-yF+y+9Shpr/OQFfy/wglB/0bykFMbwHBTuMRa5Of/r2P1wbkcacx8rg0JsUWkXH/rn2i2iWdubyqlxSJa3ggZA==",
+ "version": "15.0.8",
+ "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.8.tgz",
+ "integrity": "sha512-V2d8Wjn0VzhPHO+rrSBtcl+Fo+jUUccdlmQ6OoL9/XQB7Qk3d9lYrqKDJyccwDxmQT10JdST3Tmf2K52NLc3kw==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
@@ -9827,12 +9143,12 @@
}
},
"node_modules/expo-linking": {
- "version": "8.0.9",
- "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.9.tgz",
- "integrity": "sha512-a0UHhlVyfwIbn8b1PSFPoFiIDJeps2iEq109hVH3CHd0CMKuRxFfNio9Axe2BjXhiJCYWR4OV1iIyzY/GjiVkQ==",
+ "version": "8.0.10",
+ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.10.tgz",
+ "integrity": "sha512-0EKtn4Sk6OYmb/5ZqK8riO0k1Ic+wyT3xExbmDvUYhT7p/cKqlVUExMuOIAt3Cx3KUUU1WCgGmdd493W/D5XjA==",
"license": "MIT",
"dependencies": {
- "expo-constants": "~18.0.10",
+ "expo-constants": "~18.0.11",
"invariant": "^2.2.4"
},
"peerDependencies": {
@@ -9841,9 +9157,9 @@
}
},
"node_modules/expo-local-authentication": {
- "version": "17.0.7",
- "resolved": "https://registry.npmjs.org/expo-local-authentication/-/expo-local-authentication-17.0.7.tgz",
- "integrity": "sha512-yRWcgYn/OIwxEDEk7cM7tRjQSHaTp5hpKwzq+g9NmSMJ1etzUzt0yGzkDiOjObj3YqFo0ucyDJ8WfanLhZDtMw==",
+ "version": "17.0.8",
+ "resolved": "https://registry.npmjs.org/expo-local-authentication/-/expo-local-authentication-17.0.8.tgz",
+ "integrity": "sha512-Q5fXHhu6w3pVPlFCibU72SYIAN+9wX7QpFn9h49IUqs0Equ44QgswtGrxeh7fdnDqJrrYGPet5iBzjnE70uolA==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4"
@@ -9853,12 +9169,12 @@
}
},
"node_modules/expo-manifests": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.9.tgz",
- "integrity": "sha512-5uVgvIo0o+xBcEJiYn4uVh72QSIqyHePbYTWXYa4QamXd+AmGY/yWmtHaNqCqjsPLCwXyn4OxPr7jXJCeTWLow==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.10.tgz",
+ "integrity": "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~12.0.10",
+ "@expo/config": "~12.0.11",
"expo-json-utils": "~0.15.0"
},
"peerDependencies": {
@@ -9866,9 +9182,9 @@
}
},
"node_modules/expo-media-library": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-18.2.0.tgz",
- "integrity": "sha512-aIYLIqmU8LFWrQcfZdwg9f/iWm0wC8uhZ7HiUiTnrigtxf417cVvNokX9afXpIOKBHAHRjVIbcs1nN8KZDE2Fw==",
+ "version": "18.2.1",
+ "resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-18.2.1.tgz",
+ "integrity": "sha512-dV1acx6Aseu+I5hmF61wY8UkD4vdt8d7YXHDfgNp6ZSs06qxayUxgrBsiG2eigLe54VLm3ycbFBbWi31lhfsCA==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
@@ -9876,9 +9192,9 @@
}
},
"node_modules/expo-modules-autolinking": {
- "version": "3.0.22",
- "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.22.tgz",
- "integrity": "sha512-Ej4SsZAnUUVFmbn6SoBso8K308mRKg8xgapdhP7v7IaSgfbexUoqxoiV31949HQQXuzmgvpkXCfp6Ex+mDW0EQ==",
+ "version": "3.0.23",
+ "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.23.tgz",
+ "integrity": "sha512-YZnaE0G+52xftjH5nsIRaWsoVBY38SQCECclpdgLisdbRY/6Mzo7ndokjauOv3mpFmzMZACHyJNu1YSAffQwTg==",
"license": "MIT",
"dependencies": {
"@expo/spawn-async": "^1.7.2",
@@ -9892,9 +9208,9 @@
}
},
"node_modules/expo-modules-core": {
- "version": "3.0.26",
- "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.26.tgz",
- "integrity": "sha512-WWjficXz32VmQ+xDoO+c0+jwDME0n/47wONrJkRvtm32H9W8n3MXkOMGemDl95HyPKYsaYKhjFGUOVOxIF3hcQ==",
+ "version": "3.0.28",
+ "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.28.tgz",
+ "integrity": "sha512-8EDpksNxnN4HXWE+yhYUYAZAWTEDRzK2VpZjPSp+UBF2LtWZicXKLOCODCvsjCkTCVVA2JKKcWtGxWiteV3ueA==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4"
@@ -9905,18 +9221,18 @@
}
},
"node_modules/expo-notifications": {
- "version": "0.32.13",
- "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.13.tgz",
- "integrity": "sha512-PL0R1ulLVUgAswlXtRDKxBlcipNM3YA6+P5nB5JIhXbsjLJ7y+EKVaEhHhbaGzuK1QVsRQSJNm/4oISX+vsmFQ==",
+ "version": "0.32.14",
+ "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.14.tgz",
+ "integrity": "sha512-IRxzsd94+c1sim7R9OWdICPINmL4iwsLWcG3n6FKgzZal2ZZbBym2/m/k5yv3NQORUpytqB373WBJDZvaPCtgw==",
"license": "MIT",
"dependencies": {
- "@expo/image-utils": "^0.8.7",
+ "@expo/image-utils": "^0.8.8",
"@ide/backoff": "^1.0.0",
"abort-controller": "^3.0.0",
"assert": "^2.0.0",
"badgin": "^1.1.5",
- "expo-application": "~7.0.7",
- "expo-constants": "~18.0.10"
+ "expo-application": "~7.0.8",
+ "expo-constants": "~18.0.11"
},
"peerDependencies": {
"expo": "*",
@@ -9925,13 +9241,13 @@
}
},
"node_modules/expo-router": {
- "version": "6.0.15",
- "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.15.tgz",
- "integrity": "sha512-PAettvLifQzb6hibCmBqxbR9UljlH61GvDRLyarGxs/tG9OpMXCoZHZo8gGCO24K1/6cchBKBcjvQ0PRrKwPew==",
+ "version": "6.0.17",
+ "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.17.tgz",
+ "integrity": "sha512-2n0lTidH2H+dOjk/Lu+krKIgK7b1qQ3O/9RWmf9P5IEuFiu7BSUgSDc+g69bUEElTnca8FR+zPTyk15kMXHrXg==",
"license": "MIT",
"dependencies": {
"@expo/metro-runtime": "^6.1.2",
- "@expo/schema-utils": "^0.1.7",
+ "@expo/schema-utils": "^0.1.8",
"@radix-ui/react-slot": "1.2.0",
"@radix-ui/react-tabs": "^1.1.12",
"@react-navigation/bottom-tabs": "^7.4.0",
@@ -9940,7 +9256,7 @@
"client-only": "^0.0.1",
"debug": "^4.3.4",
"escape-string-regexp": "^4.0.0",
- "expo-server": "^1.0.4",
+ "expo-server": "^1.0.5",
"fast-deep-equal": "^3.1.3",
"invariant": "^2.2.4",
"nanoid": "^3.3.8",
@@ -9959,8 +9275,8 @@
"@react-navigation/drawer": "^7.5.0",
"@testing-library/react-native": ">= 12.0.0",
"expo": "*",
- "expo-constants": "^18.0.10",
- "expo-linking": "^8.0.9",
+ "expo-constants": "^18.0.11",
+ "expo-linking": "^8.0.10",
"react": "*",
"react-dom": "*",
"react-native": "*",
@@ -9969,7 +9285,7 @@
"react-native-safe-area-context": ">= 5.4.0",
"react-native-screens": "*",
"react-native-web": "*",
- "react-server-dom-webpack": ">= 19.0.0"
+ "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1"
},
"peerDependenciesMeta": {
"@react-navigation/drawer": {
@@ -9995,6 +9311,176 @@
}
}
},
+ "node_modules/expo-router/node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/expo-router/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/expo-router/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/expo-router/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/expo-router/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/expo-router/node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/expo-router/node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/expo-router/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
@@ -10008,48 +9494,48 @@
}
},
"node_modules/expo-secure-store": {
- "version": "15.0.7",
- "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.7.tgz",
- "integrity": "sha512-9q7+G1Zxr5P6J5NRIlm86KulvmYwc6UnQlYPjQLDu1drDnerz6AT6l884dPu29HgtDTn4rR0heYeeGFhMKM7/Q==",
+ "version": "15.0.8",
+ "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz",
+ "integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-server": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.4.tgz",
- "integrity": "sha512-IN06r3oPxFh3plSXdvBL7dx0x6k+0/g0bgxJlNISs6qL5Z+gyPuWS750dpTzOeu37KyBG0RcyO9cXUKzjYgd4A==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz",
+ "integrity": "sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA==",
"license": "MIT",
"engines": {
"node": ">=20.16.0"
}
},
"node_modules/expo-sharing": {
- "version": "14.0.7",
- "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-14.0.7.tgz",
- "integrity": "sha512-t/5tR8ZJNH6tMkHXlF7453UafNIfrpfTG+THN9EMLC4Wsi4bJuESPm3NdmWDg2D4LDALJI/LQo0iEnLAd5Sp4g==",
+ "version": "14.0.8",
+ "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-14.0.8.tgz",
+ "integrity": "sha512-A1pPr2iBrxypFDCWVAESk532HK+db7MFXbvO2sCV9ienaFXAk7lIBm6bkqgE6vzRd9O3RGdEGzYx80cYlc089Q==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-splash-screen": {
- "version": "31.0.11",
- "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.11.tgz",
- "integrity": "sha512-D7MQflYn/PAN3+fACSyxHO4oxZMBezllbgFdVY8roAS1gXpCy8SS6LrGHTD0VpOPEp3X4Gn7evTnXSI9nFoI5Q==",
+ "version": "31.0.12",
+ "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.12.tgz",
+ "integrity": "sha512-o466xFYh7Fld7CuBrzx5I12LONo7a4xzOSbxS+buOEObL/Wp4Xu4QhXg80ZY7puCGbJbtm7Ltjgg5olnWOU/Rg==",
"license": "MIT",
"dependencies": {
- "@expo/prebuild-config": "^54.0.6"
+ "@expo/prebuild-config": "^54.0.7"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-status-bar": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.8.tgz",
- "integrity": "sha512-L248XKPhum7tvREoS1VfE0H6dPCaGtoUWzRsUv7hGKdiB4cus33Rc0sxkWkoQ77wE8stlnUlL5lvmT0oqZ3ZBw==",
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz",
+ "integrity": "sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw==",
"license": "MIT",
"dependencies": {
"react-native-is-edge-to-edge": "^1.2.1"
@@ -10066,9 +9552,9 @@
"license": "MIT"
},
"node_modules/expo-symbols": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-1.0.7.tgz",
- "integrity": "sha512-ZqFUeTXbwO6BrE00n37wTXYfJmsjFrfB446jeB9k9w7aA8a6eugNUIzNsUIUfbFWoOiY4wrGmpLSLPBwk4PH+g==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-1.0.8.tgz",
+ "integrity": "sha512-7bNjK350PaQgxBf0owpmSYkdZIpdYYmaPttDBb2WIp6rIKtcEtdzdfmhsc2fTmjBURHYkg36+eCxBFXO25/1hw==",
"license": "MIT",
"dependencies": {
"sf-symbols-typescript": "^2.0.0"
@@ -10079,9 +9565,9 @@
}
},
"node_modules/expo-system-ui": {
- "version": "6.0.8",
- "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-6.0.8.tgz",
- "integrity": "sha512-DzJYqG2fibBSLzPDL4BybGCiilYOtnI1OWhcYFwoM4k0pnEzMBt1Vj8Z67bXglDDuz2HCQPGNtB3tQft5saKqQ==",
+ "version": "6.0.9",
+ "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-6.0.9.tgz",
+ "integrity": "sha512-eQTYGzw1V4RYiYHL9xDLYID3Wsec2aZS+ypEssmF64D38aDrqbDgz1a2MSlHLQp2jHXSs3FvojhZ9FVela1Zcg==",
"license": "MIT",
"dependencies": {
"@react-native/normalize-colors": "0.81.5",
@@ -10099,9 +9585,9 @@
}
},
"node_modules/expo-updates": {
- "version": "29.0.13",
- "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-29.0.13.tgz",
- "integrity": "sha512-tf/yex7U7betbIyDNwaSyDWDxMQVgmJ5qyghGEDlHP0052CPKUvbNEdtdf4DNCpsL3uxn8+71A4O4NxQdJEFuA==",
+ "version": "29.0.14",
+ "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-29.0.14.tgz",
+ "integrity": "sha512-VgXtjczQ4A/r4Jy/XEj+jWimk0vSd+GdDsYfLzl3CG/9fyQ6NXDP20PgiGfeF+A9rfA4IU3VyWdNJFBPyPPIgg==",
"license": "MIT",
"dependencies": {
"@expo/code-signing-certificates": "0.0.5",
@@ -10115,7 +9601,7 @@
"expo-structured-headers": "~5.0.0",
"expo-updates-interface": "~2.0.0",
"getenv": "^2.0.0",
- "glob": "^10.4.2",
+ "glob": "^13.0.0",
"ignore": "^5.3.1",
"resolve-from": "^5.0.0"
},
@@ -10143,45 +9629,42 @@
"integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==",
"license": "MIT"
},
- "node_modules/expo-updates/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
"node_modules/expo-updates/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "license": "ISC",
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
+ "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
+ "minimatch": "^10.1.1",
"minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "path-scurry": "^2.0.0"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/expo-updates/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/expo-updates/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -10196,10 +9679,26 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/expo-updates/node_modules/path-scurry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
+ "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/expo-web-browser": {
- "version": "15.0.9",
- "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.9.tgz",
- "integrity": "sha512-Dj8kNFO+oXsxqCDNlUT/GhOrJnm10kAElH++3RplLydogFm5jTzXYWDEeNIDmV+F+BzGYs+sIhxiBf7RyaxXZg==",
+ "version": "15.0.10",
+ "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.10.tgz",
+ "integrity": "sha512-fvDhW4bhmXAeWFNFiInmsGCK83PAqAcQaFyp/3pE/jbdKmFKoRCWr46uZGIfN4msLK/OODhaQ/+US7GSJNDHJg==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
@@ -10207,27 +9706,26 @@
}
},
"node_modules/expo/node_modules/@expo/cli": {
- "version": "54.0.16",
- "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.16.tgz",
- "integrity": "sha512-hY/OdRaJMs5WsVPuVSZ+RLH3VObJmL/pv5CGCHEZHN2PxZjSZSdctyKV8UcFBXTF0yIKNAJ9XLs1dlNYXHh4Cw==",
+ "version": "54.0.18",
+ "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.18.tgz",
+ "integrity": "sha512-hN4kolUXLah9T8DQJ8ue1ZTvRNbeNJOEOhLBak6EU7h90FKfjLA32nz99jRnHmis+aF+9qsrQG9yQx9eCSVDcg==",
"license": "MIT",
"dependencies": {
"@0no-co/graphql.web": "^1.0.8",
"@expo/code-signing-certificates": "^0.0.5",
- "@expo/config": "~12.0.10",
- "@expo/config-plugins": "~54.0.2",
- "@expo/devcert": "^1.1.2",
- "@expo/env": "~2.0.7",
- "@expo/image-utils": "^0.8.7",
- "@expo/json-file": "^10.0.7",
- "@expo/mcp-tunnel": "~0.1.0",
+ "@expo/config": "~12.0.11",
+ "@expo/config-plugins": "~54.0.3",
+ "@expo/devcert": "^1.2.1",
+ "@expo/env": "~2.0.8",
+ "@expo/image-utils": "^0.8.8",
+ "@expo/json-file": "^10.0.8",
"@expo/metro": "~54.1.0",
- "@expo/metro-config": "~54.0.9",
- "@expo/osascript": "^2.3.7",
- "@expo/package-manager": "^1.9.8",
- "@expo/plist": "^0.4.7",
- "@expo/prebuild-config": "^54.0.6",
- "@expo/schema-utils": "^0.1.7",
+ "@expo/metro-config": "~54.0.10",
+ "@expo/osascript": "^2.3.8",
+ "@expo/package-manager": "^1.9.9",
+ "@expo/plist": "^0.4.8",
+ "@expo/prebuild-config": "^54.0.7",
+ "@expo/schema-utils": "^0.1.8",
"@expo/spawn-async": "^1.7.2",
"@expo/ws-tunnel": "^1.0.1",
"@expo/xcpretty": "^4.3.0",
@@ -10245,10 +9743,10 @@
"connect": "^3.7.0",
"debug": "^4.3.4",
"env-editor": "^0.4.1",
- "expo-server": "^1.0.4",
+ "expo-server": "^1.0.5",
"freeport-async": "^2.0.0",
"getenv": "^2.0.0",
- "glob": "^10.4.2",
+ "glob": "^13.0.0",
"lan-network": "^0.1.6",
"minimatch": "^9.0.0",
"node-forge": "^1.3.1",
@@ -10271,7 +9769,7 @@
"source-map-support": "~0.5.21",
"stacktrace-parser": "^0.1.10",
"structured-headers": "^0.4.1",
- "tar": "^7.4.3",
+ "tar": "^7.5.2",
"terminal-link": "^2.1.1",
"undici": "^6.18.2",
"wrap-ansi": "^7.0.0",
@@ -10304,25 +9802,46 @@
}
},
"node_modules/expo/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "license": "ISC",
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
+ "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
+ "minimatch": "^10.1.1",
"minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "path-scurry": "^2.0.0"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/expo/node_modules/glob/node_modules/minimatch": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/brace-expansion": "^5.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/expo/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/expo/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -10347,6 +9866,22 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/expo/node_modules/path-scurry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
+ "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/expo/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -10663,7 +10198,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -10835,34 +10369,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/foreground-child": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
- "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
- "license": "ISC",
- "dependencies": {
- "cross-spawn": "^7.0.6",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/foreground-child/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
@@ -11044,7 +10550,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -11136,14 +10642,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/glob-to-regexp": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
- "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
- "devOptional": true,
- "license": "BSD-2-Clause",
- "peer": true
- },
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -11391,7 +10889,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/http-errors": {
@@ -11450,7 +10948,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.17.0"
@@ -11550,7 +11048,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
"integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"pkg-dir": "^4.2.0",
@@ -11579,7 +11077,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -11689,7 +11187,7 @@
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/is-async-function": {
@@ -11884,7 +11382,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
"integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -12063,7 +11561,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -12222,7 +11720,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
@@ -12237,7 +11735,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
"integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"debug": "^4.1.1",
@@ -12252,7 +11750,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
@@ -12280,26 +11778,11 @@
"node": ">= 0.4"
}
},
- "node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
"node_modules/jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/core": "^29.7.0",
@@ -12326,7 +11809,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz",
"integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"execa": "^5.0.0",
@@ -12341,7 +11824,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz",
"integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/environment": "^29.7.0",
@@ -12373,7 +11856,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz",
"integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/core": "^29.7.0",
@@ -12407,7 +11890,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz",
"integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.11.6",
@@ -12454,7 +11937,7 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -12475,7 +11958,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
"integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.0.0",
@@ -12491,7 +11974,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz",
"integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"detect-newline": "^3.0.0"
@@ -12504,7 +11987,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz",
"integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@@ -12817,14 +12300,14 @@
}
},
"node_modules/jest-expo": {
- "version": "54.0.13",
- "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-54.0.13.tgz",
- "integrity": "sha512-V0xefV7VJ9RD6v6Jo64I8RzQCchgEWVn6ip5r+u4TlgsGau0DA8CAqzitn4ShoSKlmjmpuaMqcGxeCz1p9Cfvg==",
+ "version": "54.0.14",
+ "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-54.0.14.tgz",
+ "integrity": "sha512-94EgBAfmP+TVbMzNaVh2c4vhrZe9isZJEUIONiVn6wK+DgI+UUbOr7BnlMFtd2M/5s0eawK3yC3SZurhCFXiyg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@expo/config": "~12.0.10",
- "@expo/json-file": "^10.0.7",
+ "@expo/config": "~12.0.11",
+ "@expo/json-file": "^10.0.8",
"@jest/create-cache-key-function": "^29.2.1",
"@jest/globals": "^29.2.1",
"babel-jest": "^29.2.1",
@@ -12834,7 +12317,6 @@
"jest-watch-typeahead": "2.2.1",
"json5": "^2.2.3",
"lodash": "^4.17.19",
- "react-server-dom-webpack": "~19.0.0",
"react-test-renderer": "19.1.0",
"server-only": "^0.0.1",
"stacktrace-js": "^2.0.2"
@@ -12844,7 +12326,13 @@
},
"peerDependencies": {
"expo": "*",
- "react-native": "*"
+ "react-native": "*",
+ "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1"
+ },
+ "peerDependenciesMeta": {
+ "react-server-dom-webpack": {
+ "optional": true
+ }
}
},
"node_modules/jest-get-type": {
@@ -12885,7 +12373,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz",
"integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"jest-get-type": "^29.6.3",
@@ -12899,7 +12387,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
"integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.0.0",
@@ -12935,7 +12423,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
"integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -12962,7 +12450,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz",
"integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.0.0",
@@ -12983,7 +12471,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz",
"integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"jest-regex-util": "^29.6.3",
@@ -12997,7 +12485,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz",
"integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/console": "^29.7.0",
@@ -13030,7 +12518,7 @@
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
"integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
@@ -13041,7 +12529,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz",
"integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/environment": "^29.7.0",
@@ -13076,7 +12564,7 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -13097,7 +12585,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
"integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@@ -13112,7 +12600,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
"integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.11.6",
@@ -13144,7 +12632,7 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -13345,7 +12833,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz",
"integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jest/test-result": "^29.7.0",
@@ -13485,7 +12973,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
@@ -13860,17 +13348,6 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
- "node_modules/loader-runner": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
- "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=6.11.5"
- }
- },
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -14017,7 +13494,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
@@ -14033,7 +13510,7 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -14553,7 +14030,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -14563,7 +14040,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -14688,7 +14165,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/negotiator": {
@@ -14700,13 +14177,6 @@
"node": ">= 0.6"
}
},
- "node_modules/neo-async": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
- "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
- "devOptional": true,
- "license": "MIT"
- },
"node_modules/nested-error-stacks": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz",
@@ -14816,7 +14286,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.0.0"
@@ -15032,7 +14502,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"mimic-fn": "^2.1.0"
@@ -15291,12 +14761,6 @@
"node": ">=6"
}
},
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "license": "BlueOak-1.0.0"
- },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -15314,7 +14778,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
@@ -15400,6 +14864,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
@@ -15416,12 +14881,14 @@
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
"license": "ISC"
},
"node_modules/path-scurry/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -15469,7 +14936,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"find-up": "^4.0.0"
@@ -15798,7 +15265,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
- "devOptional": true,
+ "dev": true,
"funding": [
{
"type": "individual",
@@ -15890,17 +15357,6 @@
],
"license": "MIT"
},
- "node_modules/randombytes": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
- "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "safe-buffer": "^5.1.0"
- }
- },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -16383,9 +15839,9 @@
}
},
"node_modules/react-remove-scroll": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
- "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
@@ -16429,26 +15885,6 @@
}
}
},
- "node_modules/react-server-dom-webpack": {
- "version": "19.0.0",
- "resolved": "https://registry.npmjs.org/react-server-dom-webpack/-/react-server-dom-webpack-19.0.0.tgz",
- "integrity": "sha512-hLug9KEXLc8vnU9lDNe2b2rKKDaqrp5gNiES4uyu2Up3FZfZJZmdwLFXlWzdA9gTB/6/cWduSB2K1Lfag2pSvw==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "acorn-loose": "^8.3.0",
- "neo-async": "^2.6.1",
- "webpack-sources": "^3.2.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "peerDependencies": {
- "react": "^19.0.0",
- "react-dom": "^19.0.0",
- "webpack": "^5.59.0"
- }
- },
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@@ -16475,7 +15911,7 @@
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
"integrity": "sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"react-is": "^19.1.0",
@@ -16489,7 +15925,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"indent-string": "^4.0.0",
@@ -16692,7 +16128,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
"integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"resolve-from": "^5.0.0"
@@ -16983,27 +16419,6 @@
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT"
},
- "node_modules/schema-utils": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
- "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/json-schema": "^7.0.9",
- "ajv": "^8.9.0",
- "ajv-formats": "^2.1.1",
- "ajv-keywords": "^5.1.0"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -17091,17 +16506,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/serialize-javascript": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
- "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
- "devOptional": true,
- "license": "BSD-3-Clause",
- "peer": true,
- "dependencies": {
- "randombytes": "^2.1.0"
- }
- },
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
@@ -17639,7 +17043,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
"integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"char-regex": "^1.0.2",
@@ -17663,21 +17067,6 @@
"node": ">=8"
}
},
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/string.prototype.matchall": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
@@ -17788,24 +17177,11 @@
"node": ">=8"
}
},
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/strip-bom": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
"integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -17815,7 +17191,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -17825,7 +17201,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"min-indent": "^1.0.0"
@@ -17838,7 +17214,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -17860,17 +17236,17 @@
"license": "MIT"
},
"node_modules/sucrase": {
- "version": "3.35.0",
- "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
- "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
- "glob": "^10.3.10",
"lines-and-columns": "^1.1.6",
"mz": "^2.7.0",
"pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
"ts-interface-checker": "^0.1.9"
},
"bin": {
@@ -17881,15 +17257,6 @@
"node": ">=16 || 14 >=14.17"
}
},
- "node_modules/sucrase/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
"node_modules/sucrase/node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -17899,50 +17266,6 @@
"node": ">= 6"
}
},
- "node_modules/sucrase/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/sucrase/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/sucrase/node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -17987,21 +17310,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/tapable": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
- "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
"node_modules/tar": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
@@ -18067,85 +17375,16 @@
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"license": "BSD-2-Clause",
"dependencies": {
- "@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.15.0",
- "commander": "^2.20.0",
- "source-map-support": "~0.5.20"
- },
- "bin": {
- "terser": "bin/terser"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/terser-webpack-plugin": {
- "version": "5.3.14",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
- "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.25",
- "jest-worker": "^27.4.5",
- "schema-utils": "^4.3.0",
- "serialize-javascript": "^6.0.2",
- "terser": "^5.31.1"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^5.1.0"
- },
- "peerDependenciesMeta": {
- "@swc/core": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "uglify-js": {
- "optional": true
- }
- }
- },
- "node_modules/terser-webpack-plugin/node_modules/jest-worker": {
- "version": "27.5.1",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
- "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/node": "*",
- "merge-stream": "^2.0.0",
- "supports-color": "^8.0.0"
- },
- "engines": {
- "node": ">= 10.13.0"
- }
- },
- "node_modules/terser-webpack-plugin/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "has-flag": "^4.0.0"
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
},
"engines": {
"node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/terser/node_modules/commander": {
@@ -18220,7 +17459,6 @@
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -18237,7 +17475,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -18851,7 +18088,7 @@
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
"integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.12",
@@ -18893,6 +18130,183 @@
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
+ "node_modules/vaul/node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vaul/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vaul/node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vaul/node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vaul/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vaul/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vaul/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vlq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz",
@@ -18927,21 +18341,6 @@
"integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==",
"license": "MIT"
},
- "node_modules/watchpack": {
- "version": "2.4.4",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
- "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "glob-to-regexp": "^0.4.1",
- "graceful-fs": "^4.1.2"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -18961,92 +18360,6 @@
"node": ">=12"
}
},
- "node_modules/webpack": {
- "version": "5.101.3",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
- "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/eslint-scope": "^3.7.7",
- "@types/estree": "^1.0.8",
- "@types/json-schema": "^7.0.15",
- "@webassemblyjs/ast": "^1.14.1",
- "@webassemblyjs/wasm-edit": "^1.14.1",
- "@webassemblyjs/wasm-parser": "^1.14.1",
- "acorn": "^8.15.0",
- "acorn-import-phases": "^1.0.3",
- "browserslist": "^4.24.0",
- "chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.17.3",
- "es-module-lexer": "^1.2.1",
- "eslint-scope": "5.1.1",
- "events": "^3.2.0",
- "glob-to-regexp": "^0.4.1",
- "graceful-fs": "^4.2.11",
- "json-parse-even-better-errors": "^2.3.1",
- "loader-runner": "^4.2.0",
- "mime-types": "^2.1.27",
- "neo-async": "^2.6.2",
- "schema-utils": "^4.3.2",
- "tapable": "^2.1.1",
- "terser-webpack-plugin": "^5.3.11",
- "watchpack": "^2.4.1",
- "webpack-sources": "^3.3.3"
- },
- "bin": {
- "webpack": "bin/webpack.js"
- },
- "engines": {
- "node": ">=10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependenciesMeta": {
- "webpack-cli": {
- "optional": true
- }
- }
- },
- "node_modules/webpack-sources": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
- "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/webpack/node_modules/eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
- "devOptional": true,
- "license": "BSD-2-Clause",
- "peer": true,
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/webpack/node_modules/estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
- "devOptional": true,
- "license": "BSD-2-Clause",
- "peer": true,
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
@@ -19249,24 +18562,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -19443,24 +18738,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/zod": {
- "version": "3.25.76",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
- "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
- },
- "node_modules/zod-to-json-schema": {
- "version": "3.25.0",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
- "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
- "license": "ISC",
- "peerDependencies": {
- "zod": "^3.25 || ^4"
- }
- },
"node_modules/zxcvbn": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
diff --git a/package.json b/package.json
index 5802bc6c..245fb081 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "brewtracker",
"main": "expo-router/entry",
- "version": "3.2.6",
+ "version": "3.3.15",
"license": "GPL-3.0-or-later",
"scripts": {
"start": "expo start",
@@ -49,31 +49,31 @@
"ajv": "^8.17.1",
"axios": "^1.12.2",
"expo": "~54.0.25",
- "expo-blur": "~15.0.7",
- "expo-build-properties": "~1.0.7",
+ "expo-blur": "~15.0.8",
+ "expo-build-properties": "~1.0.10",
"expo-constants": "~18.0.9",
- "expo-crypto": "~15.0.7",
- "expo-dev-client": "~6.0.18",
- "expo-device": "~8.0.9",
- "expo-document-picker": "~14.0.7",
+ "expo-crypto": "~15.0.8",
+ "expo-dev-client": "~6.0.20",
+ "expo-device": "~8.0.10",
+ "expo-document-picker": "~14.0.8",
"expo-file-system": "~19.0.14",
"expo-font": "~14.0.8",
- "expo-haptics": "~15.0.7",
- "expo-image": "~3.0.10",
- "expo-linear-gradient": "~15.0.7",
- "expo-linking": "~8.0.9",
- "expo-local-authentication": "~17.0.7",
- "expo-media-library": "~18.2.0",
- "expo-notifications": "~0.32.13",
- "expo-router": "~6.0.15",
- "expo-secure-store": "~15.0.7",
- "expo-sharing": "~14.0.7",
- "expo-splash-screen": "~31.0.11",
- "expo-status-bar": "~3.0.8",
- "expo-symbols": "~1.0.7",
- "expo-system-ui": "~6.0.8",
- "expo-updates": "~29.0.13",
- "expo-web-browser": "~15.0.9",
+ "expo-haptics": "~15.0.8",
+ "expo-image": "~3.0.11",
+ "expo-linear-gradient": "~15.0.8",
+ "expo-linking": "~8.0.10",
+ "expo-local-authentication": "~17.0.8",
+ "expo-media-library": "~18.2.1",
+ "expo-notifications": "~0.32.14",
+ "expo-router": "~6.0.17",
+ "expo-secure-store": "~15.0.8",
+ "expo-sharing": "~14.0.8",
+ "expo-splash-screen": "~31.0.12",
+ "expo-status-bar": "~3.0.9",
+ "expo-symbols": "~1.0.8",
+ "expo-system-ui": "~6.0.9",
+ "expo-updates": "~29.0.14",
+ "expo-web-browser": "~15.0.10",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
@@ -109,7 +109,7 @@
"eslint-config-expo": "~10.0.0",
"express": "^5.1.0",
"jest": "~29.7.0",
- "jest-expo": "~54.0.13",
+ "jest-expo": "~54.0.14",
"jsdom": "^26.1.0",
"oxlint": "^1.14.0",
"prettier": "^3.4.2",
diff --git a/src/components/banners/StaleDataBanner.tsx b/src/components/banners/StaleDataBanner.tsx
index 1387921a..a6dee7c4 100644
--- a/src/components/banners/StaleDataBanner.tsx
+++ b/src/components/banners/StaleDataBanner.tsx
@@ -111,16 +111,6 @@ export const StaleDataBanner: React.FC = ({
const staleThresholdMs = staleThresholdHours * 60 * 60 * 1000;
const now = Date.now();
- await UnifiedLogger.debug(
- "StaleDataBanner.checkStaleData",
- "Checking for stale data",
- {
- totalQueries: queries.length,
- staleThresholdHours,
- staleThresholdMs,
- }
- );
-
let oldestDataTimestamp = now;
let hasAnyStaleData = false;
const staleQueries: any[] = [];
@@ -198,15 +188,6 @@ export const StaleDataBanner: React.FC = ({
setOldestDataAge(`${ageDays}d ago`);
}
} else {
- await UnifiedLogger.debug(
- "StaleDataBanner.checkStaleData",
- "No stale data found",
- {
- totalQueries: queries.length,
- successfulQueries: queries.filter(q => q.state.status === "success")
- .length,
- }
- );
}
}, [queryClient, staleThresholdHours]);
@@ -298,23 +279,7 @@ export const StaleDataBanner: React.FC = ({
// 2. Not dismissed recently
if (!hasStaleData || isDismissed) {
if (hasStaleData && isDismissed) {
- void UnifiedLogger.debug(
- "StaleDataBanner.render",
- "Banner hidden - dismissed by user",
- {
- hasStaleData,
- isDismissed,
- }
- );
} else if (!hasStaleData) {
- void UnifiedLogger.debug(
- "StaleDataBanner.render",
- "Banner hidden - no stale data",
- {
- hasStaleData,
- isDismissed,
- }
- );
}
return null;
}
diff --git a/src/components/beerxml/UnitConversionChoiceModal.tsx b/src/components/beerxml/UnitConversionChoiceModal.tsx
new file mode 100644
index 00000000..1820e2e3
--- /dev/null
+++ b/src/components/beerxml/UnitConversionChoiceModal.tsx
@@ -0,0 +1,341 @@
+/**
+ * Unit Conversion Choice Modal Component
+ *
+ * Modal for choosing which unit system to use when importing BeerXML recipes.
+ * BeerXML files are always in metric per spec, but users can choose to import
+ * as metric or convert to imperial.
+ *
+ * Features:
+ * - Clear explanation that BeerXML is metric
+ * - Recommendations based on user's preferred unit system
+ * - Applies normalization to both choices (e.g., 28.3g -> 30g)
+ * - Loading state during conversion
+ * - Accessible design with proper ARIA labels
+ *
+ * @example
+ * ```typescript
+ *
+ * ```
+ */
+
+import React from "react";
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ Modal,
+ ActivityIndicator,
+} from "react-native";
+import { MaterialIcons } from "@expo/vector-icons";
+import { useTheme } from "@contexts/ThemeContext";
+import { unitConversionModalStyles } from "@styles/components/beerxml/unitConversionModalStyles";
+import { UnitSystem } from "@src/types";
+
+interface UnitConversionChoiceModalProps {
+ /**
+ * Whether the modal is visible
+ */
+ visible: boolean;
+ /**
+ * The user's preferred unit system (for recommendation)
+ */
+ userUnitSystem: UnitSystem;
+ /**
+ * Which unit system is currently being converted to, or null if no conversion in progress
+ */
+ convertingTarget: UnitSystem | null;
+ /**
+ * Optional recipe name to display
+ */
+ recipeName?: string;
+ /**
+ * Callback when user chooses metric
+ */
+ onChooseMetric: () => void;
+ /**
+ * Callback when user chooses imperial
+ */
+ onChooseImperial: () => void;
+ /**
+ * Callback when user cancels/closes the modal
+ */
+ onCancel: () => void;
+}
+
+/**
+ * Unit Conversion Choice Modal Component
+ *
+ * Presents the user with a choice of which unit system to use when
+ * importing a BeerXML recipe (always metric per spec).
+ */
+export const UnitConversionChoiceModal: React.FC<
+ UnitConversionChoiceModalProps
+> = ({
+ visible,
+ userUnitSystem,
+ convertingTarget,
+ recipeName,
+ onChooseMetric,
+ onChooseImperial,
+ onCancel,
+}) => {
+ const { colors } = useTheme();
+
+ // Determine which button is in loading state and which is just disabled
+ const isConvertingMetric = convertingTarget === "metric";
+ const isConvertingImperial = convertingTarget === "imperial";
+ const isAnyConversion = convertingTarget !== null;
+
+ return (
+
+
+
+
+ {/* Header */}
+
+
+
+ Choose Import Units
+
+
+
+ {/* Message */}
+
+ {recipeName && (
+
+ {recipeName}
+
+ )}
+
+ BeerXML files use metric units by default. Choose which unit
+ system you'd like to use in BrewTracker.
+
+
+
+ Both options apply brewing-friendly normalization (e.g., 28.3g →
+ 30g) for practical measurements.
+
+
+
+ {/* Action Buttons */}
+
+ {/* Metric Choice */}
+
+ {isConvertingMetric ? (
+ <>
+
+
+ Converting...
+
+ >
+ ) : (
+ <>
+
+
+
+ Import as Metric (kg, L, °C)
+
+ {userUnitSystem === "metric" && (
+
+ Recommended for your preference
+
+ )}
+
+ >
+ )}
+
+
+ {/* Imperial Choice */}
+
+ {isConvertingImperial ? (
+ <>
+
+
+ Converting...
+
+ >
+ ) : (
+ <>
+
+
+
+ Import as Imperial (lbs, gal, °F)
+
+ {userUnitSystem === "imperial" && (
+
+ Recommended for your preference
+
+ )}
+
+ >
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/components/brewSessions/DryHopTracker.tsx b/src/components/brewSessions/DryHopTracker.tsx
index ec238b01..393df081 100644
--- a/src/components/brewSessions/DryHopTracker.tsx
+++ b/src/components/brewSessions/DryHopTracker.tsx
@@ -32,7 +32,7 @@ import { getDryHopsFromRecipe } from "@utils/recipeUtils";
import { useTheme } from "@contexts/ThemeContext";
import { TEST_IDS } from "@src/constants/testIDs";
import { dryHopTrackerStyles } from "@styles/components/brewSessions/dryHopTrackerStyles";
-import UnifiedLogger from "@services/logger/UnifiedLogger";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
interface DryHopTrackerProps {
recipe: Recipe | null | undefined;
diff --git a/src/components/brewSessions/FermentationChart.tsx b/src/components/brewSessions/FermentationChart.tsx
index 2eace5d6..ece25afa 100644
--- a/src/components/brewSessions/FermentationChart.tsx
+++ b/src/components/brewSessions/FermentationChart.tsx
@@ -29,7 +29,7 @@
import React from "react";
import { View, Text, TouchableOpacity, Modal, Pressable } from "react-native";
import { LineChart } from "react-native-gifted-charts";
-import { FermentationEntry, Recipe } from "@src/types";
+import { FermentationEntry, Recipe, TemperatureUnit } from "@src/types";
import { useTheme } from "@contexts/ThemeContext";
import { useUnits } from "@contexts/UnitContext";
import { useScreenDimensions } from "@contexts/ScreenDimensionsContext";
@@ -244,7 +244,7 @@ interface FermentationChartProps {
fermentationData: FermentationEntry[];
expectedFG?: number;
actualOG?: number;
- temperatureUnit?: "C" | "F"; // Session-specific temperature unit
+ temperatureUnit?: TemperatureUnit; // Session-specific temperature unit
forceRefresh?: number; // External refresh trigger
recipeData?: Recipe; // Recipe data for accessing estimated_fg
}
diff --git a/src/components/calculators/UnitToggle.tsx b/src/components/calculators/UnitToggle.tsx
index 014ccf93..5b18b8e3 100644
--- a/src/components/calculators/UnitToggle.tsx
+++ b/src/components/calculators/UnitToggle.tsx
@@ -22,8 +22,8 @@
* value={tempUnit}
* onChange={setTempUnit}
* options={[
- * { label: "°F", value: "f", description: "Fahrenheit" },
- * { label: "°C", value: "c", description: "Celsius" }
+ * { label: "°F", value: "F", description: "Fahrenheit" },
+ * { label: "°C", value: "C", description: "Celsius" }
* ]}
* />
* ```
diff --git a/src/components/recipes/AIAnalysisResultsModal.tsx b/src/components/recipes/AIAnalysisResultsModal.tsx
index 24bf1b85..c082eb78 100644
--- a/src/components/recipes/AIAnalysisResultsModal.tsx
+++ b/src/components/recipes/AIAnalysisResultsModal.tsx
@@ -53,7 +53,6 @@ import {
normalizeBackendMetrics,
} from "@utils/aiHelpers";
import { beerStyleAnalysisService } from "@services/beerStyles/BeerStyleAnalysisService";
-import { UnifiedLogger } from "@services/logger/UnifiedLogger";
interface AIAnalysisResultsModalProps {
/**
@@ -285,25 +284,8 @@ export function AIAnalysisResultsModal({
);
// Debug logging
- UnifiedLogger.debug("AIAnalysisResultsModal", "Rendering modal", {
- hasOptimisation,
- hasStyle: !!style,
- styleName: style?.name,
- styleId: style?.id,
- recipeChangesCount: result.recipe_changes?.length || 0,
- hasOriginalMetrics: !!result.original_metrics,
- hasOptimizedMetrics: !!result.optimized_metrics,
- iterationsCompleted: result.iterations_completed,
- });
// Debug grouped changes
- UnifiedLogger.debug("AIAnalysisResultsModal", "Grouped changes", {
- hasGroupedChanges: !!groupedChanges,
- parametersCount: groupedChanges?.parameters.length || 0,
- modificationsCount: groupedChanges?.modifications.length || 0,
- additionsCount: groupedChanges?.additions.length || 0,
- removalsCount: groupedChanges?.removals.length || 0,
- });
// Debug conditionals - ensure all are explicit booleans
const showSummary = hasOptimisation;
@@ -313,19 +295,6 @@ export function AIAnalysisResultsModal({
!!normalizedOptimizedMetrics;
const showChanges = hasOptimisation && !!groupedChanges;
- UnifiedLogger.debug("AIAnalysisResultsModal", "Boolean check", {
- hasOptimisationType: typeof hasOptimisation,
- hasOptimisationValue: hasOptimisation,
- showSummaryType: typeof showSummary,
- showSummaryValue: showSummary,
- });
-
- UnifiedLogger.debug("AIAnalysisResultsModal", "Section visibility", {
- showSummary,
- showMetrics,
- showChanges,
- });
-
return (
{
if (ingredientType === "grain") {
switch (unit) {
diff --git a/src/components/recipes/RecipeForm/ParametersForm.tsx b/src/components/recipes/RecipeForm/ParametersForm.tsx
index 38c5a07f..a51278ba 100644
--- a/src/components/recipes/RecipeForm/ParametersForm.tsx
+++ b/src/components/recipes/RecipeForm/ParametersForm.tsx
@@ -4,7 +4,7 @@ import { MaterialIcons } from "@expo/vector-icons";
import { useTheme } from "@contexts/ThemeContext";
import { useUnits } from "@contexts/UnitContext";
-import { RecipeFormData } from "@src/types";
+import { RecipeFormData, TemperatureUnit } from "@src/types";
import { createRecipeStyles } from "@styles/modals/createRecipeStyles";
import { StyleAnalysis } from "@src/components/recipes/StyleAnalysis";
import { TEST_IDS } from "@src/constants/testIDs";
@@ -98,7 +98,7 @@ export function ParametersForm({
validateField(field, value);
};
- const handleMashTempUnitChange = (unit: "F" | "C") => {
+ const handleMashTempUnitChange = (unit: TemperatureUnit) => {
// Convert temperature when changing units
let newTemp = recipeData.mash_temperature;
@@ -224,7 +224,8 @@ export function ParametersForm({
]}
value={recipeData.mash_temperature.toString()}
onChangeText={text => {
- const numValue = parseFloat(text) || 0;
+ const numValue =
+ text.trim() === "" ? Number.NaN : parseFloat(text);
handleFieldChange("mash_temperature", numValue);
}}
placeholder={unitSystem === "imperial" ? "152" : "67"}
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
index 5066afe0..1a644529 100644
--- a/src/contexts/AuthContext.tsx
+++ b/src/contexts/AuthContext.tsx
@@ -351,20 +351,21 @@ export const AuthProvider: React.FC = ({
// Update state
setUser(normalizedUser);
- await UnifiedLogger.debug("auth", "Session applied successfully", {
- userId: normalizedUser.id,
- username: normalizedUser.username,
- });
-
// Cache ingredients in background (don't block)
StaticDataService.updateIngredientsCache()
.then(() => {
StaticDataService.getCacheStats()
.then(stats => console.log("V2 Cache Status:", stats))
- .catch(error => console.warn("Failed to get cache stats:", error));
+ .catch(error =>
+ UnifiedLogger.warn("auth", "Failed to get cache stats:", error)
+ );
})
.catch(error => {
- console.warn("Failed to cache ingredients after login:", error);
+ UnifiedLogger.warn(
+ "auth",
+ "Failed to cache ingredients after login:",
+ error
+ );
});
} catch (error) {
// Rollback: Clear token, cached data, and auth status if session setup fails
@@ -379,7 +380,6 @@ export const AuthProvider: React.FC = ({
await AsyncStorage.removeItem(STORAGE_KEYS.USER_DATA);
setAuthStatus("unauthenticated");
setUser(null);
- await UnifiedLogger.debug("auth", "Session rollback completed");
} catch (rollbackError) {
await UnifiedLogger.error(
"auth",
@@ -556,10 +556,13 @@ export const AuthProvider: React.FC = ({
.then(() => {
StaticDataService.getCacheStats()
.then(stats => console.log("V2 Cache Status:", stats))
- .catch(error => console.warn("Failed to get cache stats:", error));
+ .catch(error =>
+ UnifiedLogger.warn("auth", "Failed to get cache stats:", error)
+ );
})
.catch(error => {
- console.warn(
+ UnifiedLogger.warn(
+ "auth",
"Failed to cache ingredients during auth initialization:",
error
);
@@ -667,7 +670,7 @@ export const AuthProvider: React.FC = ({
// Note: Token should be handled if provided
}
} catch (error: any) {
- console.error("Registration failed:", error);
+ UnifiedLogger.error("auth", "Registration failed:", error);
setError(error.response?.data?.message || "Registration failed");
throw error;
} finally {
@@ -686,7 +689,7 @@ export const AuthProvider: React.FC = ({
// Apply session (token storage, auth status update, caching, etc.)
await applyNewSession(access_token, userData);
} catch (error: any) {
- console.error("Google sign-in failed:", error);
+ UnifiedLogger.error("auth", "Google sign-in failed:", error);
setError(error.response?.data?.message || "Google sign-in failed");
throw error;
} finally {
@@ -714,14 +717,9 @@ export const AuthProvider: React.FC = ({
// Clear JWT token from SecureStore
await ApiService.token.removeToken();
- await UnifiedLogger.debug("auth", "JWT token removed from SecureStore");
// Clear device token (but keep biometric credentials for backward compatibility)
await DeviceTokenService.clearDeviceToken();
- await UnifiedLogger.debug(
- "auth",
- "Device token cleared from SecureStore"
- );
// Clear cached data
await AsyncStorage.multiRemove([
@@ -729,19 +727,16 @@ export const AuthProvider: React.FC = ({
STORAGE_KEYS.USER_SETTINGS,
STORAGE_KEYS.CACHED_INGREDIENTS,
]);
- await UnifiedLogger.debug("auth", "AsyncStorage cleared");
// Clear React Query cache and persisted storage
cacheUtils.clearAll();
await cacheUtils.clearUserPersistedCache(userId);
- await UnifiedLogger.debug("auth", "React Query cache cleared");
// Clear user-scoped offline data
const { UserCacheService } = await import(
"@services/offlineV2/UserCacheService"
);
await UserCacheService.clearUserData(userId);
- await UnifiedLogger.debug("auth", "User offline data cleared");
await UnifiedLogger.info("auth", "Logout completed successfully", {
userId,
@@ -826,7 +821,7 @@ export const AuthProvider: React.FC = ({
setUser(userData);
} catch (error: any) {
- console.error("Failed to refresh user:", error);
+ UnifiedLogger.error("auth", "Failed to refresh user:", error);
// Don't set error state for refresh failures unless it's a 401
if (error.response?.status === 401) {
await logout();
@@ -850,7 +845,7 @@ export const AuthProvider: React.FC = ({
await refreshUser();
}
} catch (error: any) {
- console.error("Email verification failed:", error);
+ UnifiedLogger.error("auth", "Email verification failed:", error);
setError(error.response?.data?.message || "Email verification failed");
throw error;
} finally {
@@ -863,7 +858,7 @@ export const AuthProvider: React.FC = ({
setError(null);
await ApiService.auth.resendVerification();
} catch (error: any) {
- console.error("Failed to resend verification:", error);
+ UnifiedLogger.error("auth", "Failed to resend verification:", error);
setError(
error.response?.data?.message || "Failed to resend verification"
);
@@ -888,7 +883,11 @@ export const AuthProvider: React.FC = ({
);
}
} catch (error: any) {
- console.error("Failed to check verification status:", error);
+ UnifiedLogger.error(
+ "auth",
+ "Failed to check verification status:",
+ error
+ );
}
};
@@ -898,7 +897,7 @@ export const AuthProvider: React.FC = ({
setError(null);
await ApiService.auth.forgotPassword({ email });
} catch (error: any) {
- console.error("Failed to send password reset:", error);
+ UnifiedLogger.error("auth", "Failed to send password reset:", error);
setError(
error.response?.data?.error || "Failed to send password reset email"
);
@@ -917,7 +916,7 @@ export const AuthProvider: React.FC = ({
setError(null);
await ApiService.auth.resetPassword({ token, new_password: newPassword });
} catch (error: any) {
- console.error("Failed to reset password:", error);
+ UnifiedLogger.error("auth", "Failed to reset password:", error);
setError(error.response?.data?.error || "Failed to reset password");
throw error;
} finally {
@@ -940,7 +939,11 @@ export const AuthProvider: React.FC = ({
setIsBiometricAvailable(available);
setIsBiometricEnabled(enabled);
} catch (error) {
- console.error("Failed to check biometric availability:", error);
+ UnifiedLogger.error(
+ "auth",
+ "Failed to check biometric availability:",
+ error
+ );
setIsBiometricAvailable(false);
setIsBiometricEnabled(false);
}
@@ -962,12 +965,6 @@ export const AuthProvider: React.FC = ({
const biometricEnabled = await BiometricService.isBiometricEnabled();
const hasDeviceToken = await BiometricService.hasStoredDeviceToken();
- await UnifiedLogger.debug("auth", "Biometric pre-flight check", {
- biometricAvailable,
- biometricEnabled,
- hasDeviceToken,
- });
-
if (!hasDeviceToken) {
await UnifiedLogger.warn(
"auth",
@@ -979,20 +976,17 @@ export const AuthProvider: React.FC = ({
}
// Authenticate with biometrics and exchange device token for access token
- await UnifiedLogger.debug(
- "auth",
- "Requesting biometric authentication from BiometricService"
- );
-
+ if (!biometricAvailable || !biometricEnabled) {
+ await UnifiedLogger.warn(
+ "auth",
+ "Biometric login failed: Biometrics not available or not enabled"
+ );
+ throw new Error(
+ "Biometric authentication is not available or not enabled on this device."
+ );
+ }
const result = await BiometricService.authenticateWithBiometrics();
- await UnifiedLogger.debug("auth", "Biometric authentication result", {
- success: result.success,
- errorCode: result.errorCode,
- hasAccessToken: !!result.accessToken,
- hasUser: !!result.user,
- });
-
if (!result.success || !result.accessToken || !result.user) {
await UnifiedLogger.warn("auth", "Biometric authentication rejected", {
error: result.error,
@@ -1009,11 +1003,6 @@ export const AuthProvider: React.FC = ({
throw error;
}
- await UnifiedLogger.debug(
- "auth",
- "Biometric authentication successful, device token exchanged for access token"
- );
-
const { accessToken: access_token, user: userData } = result;
// Validate user data structure
@@ -1021,12 +1010,6 @@ export const AuthProvider: React.FC = ({
throw new Error("Invalid user data received from biometric login");
}
- await UnifiedLogger.debug("auth", "Biometric login successful", {
- userId: userData.id,
- username: userData.username,
- hasAccessToken: true,
- });
-
// Apply session (token storage, auth status update, caching, etc.)
// Note: applyNewSession handles user data validation
await applyNewSession(access_token, userData as User);
@@ -1074,7 +1057,7 @@ export const AuthProvider: React.FC = ({
await BiometricService.enableBiometrics(username);
await checkBiometricAvailability();
} catch (error: any) {
- console.error("Failed to enable biometrics:", error);
+ UnifiedLogger.error("auth", "Failed to enable biometrics:", error);
throw error;
}
};
@@ -1088,7 +1071,7 @@ export const AuthProvider: React.FC = ({
await BiometricService.disableBiometricsLocally();
await checkBiometricAvailability();
} catch (error: any) {
- console.error("Failed to disable biometrics:", error);
+ UnifiedLogger.error("auth", "Failed to disable biometrics:", error);
throw error;
}
};
@@ -1114,7 +1097,7 @@ export const AuthProvider: React.FC = ({
return extractUserIdFromJWT(token);
} catch (error) {
- console.warn("Failed to extract user ID:", error);
+ UnifiedLogger.warn("auth", "Failed to extract user ID:", error);
return null;
}
};
diff --git a/src/contexts/CalculatorsContext.tsx b/src/contexts/CalculatorsContext.tsx
index da782564..7d5f696f 100644
--- a/src/contexts/CalculatorsContext.tsx
+++ b/src/contexts/CalculatorsContext.tsx
@@ -49,6 +49,7 @@ import React, {
ReactNode,
} from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
+import { TemperatureUnit, UnitSystem } from "../types";
/**
* Type definitions for individual calculator states
@@ -75,7 +76,7 @@ export interface StrikeWaterState {
grainWeightUnit: string;
grainTemp: string;
targetMashTemp: string;
- tempUnit: "f" | "c";
+ tempUnit: TemperatureUnit;
waterToGrainRatio: string;
result: {
strikeTemp: number;
@@ -87,7 +88,7 @@ export interface HydrometerCorrectionState {
measuredGravity: string;
wortTemp: string;
calibrationTemp: string;
- tempUnit: "f" | "c";
+ tempUnit: TemperatureUnit;
result: number | null;
}
@@ -169,8 +170,8 @@ export interface BoilTimerState {
}
export interface UserPreferences {
- defaultUnits: "metric" | "imperial";
- temperatureUnit: "f" | "c";
+ defaultUnits: UnitSystem;
+ temperatureUnit: TemperatureUnit;
saveHistory: boolean;
}
@@ -254,7 +255,7 @@ const initialState: CalculatorState = {
grainWeightUnit: "lb",
grainTemp: "",
targetMashTemp: "",
- tempUnit: "f",
+ tempUnit: "F",
waterToGrainRatio: "1.25",
result: null,
},
@@ -262,7 +263,7 @@ const initialState: CalculatorState = {
measuredGravity: "",
wortTemp: "",
calibrationTemp: "60",
- tempUnit: "f",
+ tempUnit: "F",
result: null,
},
dilution: {
@@ -310,8 +311,8 @@ const initialState: CalculatorState = {
timerStartedAt: undefined,
},
preferences: {
- defaultUnits: "imperial",
- temperatureUnit: "f",
+ defaultUnits: "metric",
+ temperatureUnit: "C",
saveHistory: true,
},
history: {},
diff --git a/src/contexts/DeveloperContext.tsx b/src/contexts/DeveloperContext.tsx
index 0e45c28c..3879f3f8 100644
--- a/src/contexts/DeveloperContext.tsx
+++ b/src/contexts/DeveloperContext.tsx
@@ -30,7 +30,7 @@ import React, {
} from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { STORAGE_KEYS } from "@services/config";
-import UnifiedLogger from "@services/logger/UnifiedLogger";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
/**
* Network simulation modes for testing
@@ -156,7 +156,11 @@ export const DeveloperProvider: React.FC = ({
}
}
} catch (error) {
- console.warn("Failed to load developer settings:", error);
+ UnifiedLogger.warn(
+ "developer",
+ "Failed to load developer settings:",
+ error
+ );
// Self-heal on any error by ensuring we have a safe default
setNetworkSimulationModeState("normal");
}
@@ -220,7 +224,11 @@ export const DeveloperProvider: React.FC = ({
{ error: errorMessage }
);
- console.warn("Developer mode sync of pending operations failed:", error);
+ UnifiedLogger.warn(
+ "developer",
+ "Developer mode sync of pending operations failed:",
+ error
+ );
throw error; // Re-throw so callers can handle if needed
}
};
@@ -287,7 +295,11 @@ export const DeveloperProvider: React.FC = ({
error: error instanceof Error ? error.message : "Unknown error",
}
);
- console.error("Failed to set network simulation mode:", error);
+ UnifiedLogger.error(
+ "developer",
+ "Failed to set network simulation mode:",
+ error
+ );
}
};
@@ -310,7 +322,11 @@ export const DeveloperProvider: React.FC = ({
setNetworkSimulationModeState("normal");
console.log("Developer settings reset to defaults");
} catch (error) {
- console.error("Failed to reset developer settings:", error);
+ UnifiedLogger.error(
+ "developer",
+ "Failed to reset developer settings:",
+ error
+ );
}
};
@@ -332,7 +348,7 @@ export const DeveloperProvider: React.FC = ({
console.log("Developer: V2 system handles cleanup automatically");
return { removedTombstones: 0, tombstoneNames: [] };
} catch (error) {
- console.error("Failed to cleanup tombstones:", error);
+ UnifiedLogger.error("developer", "Failed to cleanup tombstones:", error);
throw error;
}
};
@@ -375,7 +391,7 @@ export const DeveloperProvider: React.FC = ({
"Failed to make cache stale",
{ error }
);
- console.error("Failed to make cache stale:", error);
+ UnifiedLogger.error("developer", "Failed to make cache stale:", error);
throw error;
}
};
@@ -405,7 +421,7 @@ export const DeveloperProvider: React.FC = ({
"Failed to clear cache",
{ error }
);
- console.error("Failed to clear cache:", error);
+ UnifiedLogger.error("developer", "Failed to clear cache:", error);
throw error;
}
};
@@ -473,7 +489,7 @@ export const DeveloperProvider: React.FC = ({
"Failed to invalidate token",
{ error }
);
- console.error("Failed to invalidate token:", error);
+ UnifiedLogger.error("developer", "Failed to invalidate token:", error);
throw error;
}
};
diff --git a/src/contexts/NetworkContext.tsx b/src/contexts/NetworkContext.tsx
index b312e292..efd7f5a6 100644
--- a/src/contexts/NetworkContext.tsx
+++ b/src/contexts/NetworkContext.tsx
@@ -166,7 +166,11 @@ export const NetworkProvider: React.FC = ({
// Load any cached network preferences
await loadCachedNetworkState();
} catch (error) {
- console.warn("Failed to initialize network monitoring:", error);
+ void UnifiedLogger.warn(
+ "network",
+ "Failed to initialize network monitoring:",
+ error
+ );
// Fallback to optimistic connected state
setIsConnected(true);
setConnectionType("unknown");
@@ -223,8 +227,10 @@ export const NetworkProvider: React.FC = ({
const isNowOnline = connected && (reachable ?? true);
const shouldRefresh = wasOffline && isNowOnline;
- // Log network state changes
- void UnifiedLogger.info(
+ // Log network state changes (info when status flips, debug otherwise)
+ const logMethod =
+ previousOnlineState.current !== isNowOnline ? "info" : "debug";
+ void UnifiedLogger[logMethod](
"NetworkContext.handleStateChange",
"Network state change detected",
{
@@ -237,35 +243,6 @@ export const NetworkProvider: React.FC = ({
previousOnlineState: previousOnlineState.current,
}
);
- if (previousOnlineState.current !== isNowOnline) {
- void UnifiedLogger.info(
- "NetworkContext.handleStateChange",
- "Network state change detected",
- {
- connected,
- reachable,
- type,
- wasOffline,
- isNowOnline,
- shouldRefresh,
- previousOnlineState: previousOnlineState.current,
- }
- );
- } else {
- void UnifiedLogger.debug(
- "NetworkContext.handleStateChange",
- "Network state change detected",
- {
- connected,
- reachable,
- type,
- wasOffline,
- isNowOnline,
- shouldRefresh,
- previousOnlineState: previousOnlineState.current,
- }
- );
- }
// Also refresh if it's been more than 4 hours since last refresh
const timeSinceRefresh = Date.now() - lastCacheRefresh.current;
@@ -275,16 +252,6 @@ export const NetworkProvider: React.FC = ({
if (shouldRefresh || shouldPeriodicRefresh) {
lastCacheRefresh.current = Date.now();
- void UnifiedLogger.debug(
- "NetworkContext.handleStateChange",
- "Triggering background refresh and sync",
- {
- shouldRefresh,
- shouldPeriodicRefresh,
- isComingBackOnline: shouldRefresh,
- }
- );
-
// Trigger comprehensive background cache refresh using V2 system (non-blocking)
Promise.allSettled([
StaticDataService.updateIngredientsCache(),
@@ -301,9 +268,6 @@ export const NetworkProvider: React.FC = ({
: "Background cache refresh completed",
{ results }
);
- if (failures.length > 0) {
- console.warn("Background cache refresh had failures:", failures);
- }
})
.catch(error => {
void UnifiedLogger.error(
@@ -311,7 +275,6 @@ export const NetworkProvider: React.FC = ({
"Background cache refresh failed",
{ error: error instanceof Error ? error.message : String(error) }
);
- console.warn("Background cache refresh failed:", error);
});
// **CRITICAL FIX**: Also trigger sync of pending operations when coming back online
@@ -353,10 +316,6 @@ export const NetworkProvider: React.FC = ({
error instanceof Error ? error.message : "Unknown error",
}
);
- console.warn(
- "Background sync of pending operations failed:",
- error
- );
});
})
.catch(error => {
@@ -386,7 +345,11 @@ export const NetworkProvider: React.FC = ({
})
);
} catch (error) {
- console.warn("Failed to cache network state:", error);
+ void UnifiedLogger.warn(
+ "network",
+ "Failed to cache network state:",
+ error
+ );
}
};
@@ -411,7 +374,11 @@ export const NetworkProvider: React.FC = ({
}
}
} catch (error) {
- console.warn("Failed to load cached network state:", error);
+ void UnifiedLogger.warn(
+ "network",
+ "Failed to load cached network state:",
+ error
+ );
}
};
@@ -423,7 +390,11 @@ export const NetworkProvider: React.FC = ({
const state = await NetInfo.fetch();
await updateNetworkState(state);
} catch (error) {
- console.warn("Failed to refresh network state:", error);
+ void UnifiedLogger.warn(
+ "network",
+ "Failed to refresh network state:",
+ error
+ );
throw error;
}
};
diff --git a/src/contexts/UnitContext.tsx b/src/contexts/UnitContext.tsx
index be0bb09b..f09b1059 100644
--- a/src/contexts/UnitContext.tsx
+++ b/src/contexts/UnitContext.tsx
@@ -37,6 +37,7 @@ import ApiService from "@services/api/apiService";
import { STORAGE_KEYS } from "@services/config";
import { UnitSystem, MeasurementType, UserSettings } from "@src/types";
import { useAuth } from "@contexts/AuthContext";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
// Unit option interface for common units
interface UnitOption {
@@ -158,7 +159,11 @@ export const UnitProvider: React.FC = ({
try {
settings = JSON.parse(cachedSettings) as UserSettings;
} catch (parseErr) {
- console.warn("Corrupted cached user settings, removing:", parseErr);
+ void UnifiedLogger.warn(
+ "units",
+ "Corrupted cached user settings, removing:",
+ parseErr
+ );
await AsyncStorage.removeItem(STORAGE_KEYS.USER_SETTINGS);
// Treat as no-cache: fetch if authed, else default.
try {
@@ -180,7 +185,8 @@ export const UnitProvider: React.FC = ({
}
} catch (bgError: any) {
if (bgError?.response?.status !== 401) {
- console.warn(
+ void UnifiedLogger.warn(
+ "units",
"Settings fetch after cache corruption failed:",
bgError
);
@@ -220,7 +226,11 @@ export const UnitProvider: React.FC = ({
} catch (bgError: any) {
// Silently handle background fetch errors for unauthenticated users
if (bgError.response?.status !== 401) {
- console.warn("Background settings fetch failed:", bgError);
+ void UnifiedLogger.warn(
+ "units",
+ "Background settings fetch failed:",
+ bgError
+ );
}
}
}
@@ -249,7 +259,11 @@ export const UnitProvider: React.FC = ({
} catch (err: any) {
// Only log non-auth errors
if (err.response?.status !== 401) {
- console.warn("Failed to load unit preferences, using default:", err);
+ void UnifiedLogger.warn(
+ "units",
+ "Failed to load unit preferences, using default:",
+ err
+ );
}
if (isMounted) {
setUnitSystem("metric");
@@ -291,7 +305,8 @@ export const UnitProvider: React.FC = ({
try {
settings = JSON.parse(cachedSettings);
} catch {
- console.warn(
+ void UnifiedLogger.warn(
+ "units",
"Corrupted cached user settings during update; re-initializing."
);
}
@@ -308,7 +323,7 @@ export const UnitProvider: React.FC = ({
);
}
} catch (err) {
- console.error("Failed to update unit system:", err);
+ await UnifiedLogger.error("units", "Failed to update unit system:", err);
setError("Failed to save unit preference");
setUnitSystem(previousSystem); // Revert on error
} finally {
@@ -332,7 +347,7 @@ export const UnitProvider: React.FC = ({
case "volume":
return unitSystem === "metric" ? "l" : "gal";
case "temperature":
- return unitSystem === "metric" ? "c" : "f";
+ return unitSystem === "metric" ? "C" : "F";
default:
return unitSystem === "metric" ? "kg" : "lb";
}
@@ -401,15 +416,18 @@ export const UnitProvider: React.FC = ({
}
// Temperature conversions
- else if (fromUnit === "f" && toUnit === "c") {
+ else if (fromUnit === "F" && toUnit === "C") {
convertedValue = ((numValue - 32) * 5) / 9;
- } else if (fromUnit === "c" && toUnit === "f") {
+ } else if (fromUnit === "C" && toUnit === "F") {
convertedValue = (numValue * 9) / 5 + 32;
}
// If no conversion found, return original
else {
- console.warn(`No conversion available from ${fromUnit} to ${toUnit}`);
+ void UnifiedLogger.warn(
+ "units",
+ `No conversion available from ${fromUnit} to ${toUnit}`
+ );
return { value: numValue, unit: fromUnit };
}
@@ -610,8 +628,8 @@ export const UnitProvider: React.FC = ({
case "temperature":
return unitSystem === "metric"
- ? [{ value: "c", label: "°C", description: "Celsius" }]
- : [{ value: "f", label: "°F", description: "Fahrenheit" }];
+ ? [{ value: "C", label: "°C", description: "Celsius" }]
+ : [{ value: "F", label: "°F", description: "Fahrenheit" }];
default:
return [];
diff --git a/src/hooks/offlineV2/useUserData.ts b/src/hooks/offlineV2/useUserData.ts
index 4ea5a298..a04ad610 100644
--- a/src/hooks/offlineV2/useUserData.ts
+++ b/src/hooks/offlineV2/useUserData.ts
@@ -10,7 +10,7 @@ import { UserCacheService } from "@services/offlineV2/UserCacheService";
import { UseUserDataReturn, SyncResult, Recipe, BrewSession } from "@src/types";
import { useAuth } from "@contexts/AuthContext";
import { useUnits } from "@contexts/UnitContext";
-import UnifiedLogger from "@services/logger/UnifiedLogger";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
/**
* Hook for managing recipes with offline capabilities
@@ -59,7 +59,11 @@ export function useRecipes(): UseUserDataReturn {
const pending = await UserCacheService.getPendingOperationsCount();
setPendingCount(pending);
} catch (err) {
- console.error("Error loading recipes:", err);
+ await UnifiedLogger.error(
+ "useRecipes.loadData",
+ "Error loading recipes:",
+ err
+ );
setError(err instanceof Error ? err.message : "Failed to load recipes");
} finally {
setIsLoading(false);
@@ -215,7 +219,11 @@ export function useRecipes(): UseUserDataReturn {
const pending = await UserCacheService.getPendingOperationsCount();
setPendingCount(pending);
} catch (cacheError) {
- console.error("Failed to load offline recipe cache:", cacheError);
+ await UnifiedLogger.error(
+ "useRecipes.refresh",
+ "Failed to load offline recipe cache:",
+ cacheError
+ );
// Only set error if we can't even load offline cache
setError(
error instanceof Error ? error.message : "Failed to refresh recipes"
@@ -332,7 +340,11 @@ export function useBrewSessions(): UseUserDataReturn {
const pending = await UserCacheService.getPendingOperationsCount();
setPendingCount(pending);
} catch (err) {
- console.error("Error loading brew sessions:", err);
+ await UnifiedLogger.error(
+ "useBrewSessions.loadData",
+ "Error loading brew sessions:",
+ err
+ );
setError(
err instanceof Error ? err.message : "Failed to load brew sessions"
);
@@ -479,7 +491,11 @@ export function useBrewSessions(): UseUserDataReturn {
setPendingCount(pending);
setLastSync(Date.now());
} catch (error) {
- console.error("Brew sessions refresh failed:", error);
+ await UnifiedLogger.error(
+ "useBrewSessions.refresh",
+ "Brew sessions refresh failed:",
+ error
+ );
// Don't set error state for refresh failures - preserve offline cache
// Try to load existing offline data to ensure offline-created sessions are available
@@ -501,7 +517,11 @@ export function useBrewSessions(): UseUserDataReturn {
const pending = await UserCacheService.getPendingOperationsCount();
setPendingCount(pending);
} catch (cacheError) {
- console.error("Failed to load offline brew session cache:", cacheError);
+ await UnifiedLogger.error(
+ "useBrewSessions.refresh",
+ "Failed to load offline brew session cache:",
+ cacheError
+ );
// Only set error if we can't even load offline cache
setError(
error instanceof Error ? error.message : "Failed to refresh sessions"
diff --git a/src/hooks/useRecipeMetrics.ts b/src/hooks/useRecipeMetrics.ts
index d1b06510..b6101878 100644
--- a/src/hooks/useRecipeMetrics.ts
+++ b/src/hooks/useRecipeMetrics.ts
@@ -1,16 +1,14 @@
import { useQuery } from "@tanstack/react-query";
-import ApiService from "@services/api/apiService";
import { RecipeFormData, RecipeMetrics } from "@src/types";
import { useDebounce } from "./useDebounce";
-import { useNetwork } from "@contexts/NetworkContext";
import { OfflineMetricsCalculator } from "@services/brewing/OfflineMetricsCalculator";
/**
- * Hook for calculating recipe metrics with offline support
+ * Hook for calculating recipe metrics with offline-first approach
*
- * Automatically recalculates metrics when recipe data changes with offline fallback.
- * Tries API calculation when online, provides reasonable fallback metrics when offline.
- * Includes proper debouncing to prevent excessive calculations and network calls.
+ * Automatically recalculates metrics when recipe data changes using local calculations.
+ * Always calculates locally for instant results without network dependency.
+ * Includes proper debouncing to prevent excessive calculations during rapid changes.
*
* @param recipeData - Current recipe form data
* @param enabled - Whether to enable the query (default: true when ingredients exist)
@@ -20,7 +18,6 @@ export function useRecipeMetrics(
recipeData: RecipeFormData,
enabled?: boolean
) {
- const { isConnected } = useNetwork();
const FALLBACK_METRICS: RecipeMetrics = {
og: 1.05,
fg: 1.012,
@@ -42,8 +39,7 @@ export function useRecipeMetrics(
return useQuery>({
queryKey: [
"recipeMetrics",
- "offline-aware",
- isConnected ? "online" : "offline",
+ "offline-first",
// Include relevant recipe parameters in query key for proper caching
debouncedRecipeData.batch_size,
debouncedRecipeData.batch_size_unit,
@@ -78,8 +74,7 @@ export function useRecipeMetrics(
),
],
queryFn: async (): Promise => {
- // Basic validation for recipe data (V2 system)
- // Removed validationParams - not used in V2 system
+ // Basic validation for recipe data
if (
!debouncedRecipeData.batch_size ||
debouncedRecipeData.batch_size <= 0 ||
@@ -88,52 +83,13 @@ export function useRecipeMetrics(
return FALLBACK_METRICS;
}
- // Try API calculation when online
- if (isConnected) {
- try {
- const response = await ApiService.recipes.calculateMetricsPreview({
- batch_size: debouncedRecipeData.batch_size,
- batch_size_unit: debouncedRecipeData.batch_size_unit,
- efficiency: debouncedRecipeData.efficiency,
- boil_time: debouncedRecipeData.boil_time,
- ingredients: debouncedRecipeData.ingredients,
- mash_temperature: debouncedRecipeData.mash_temperature,
- mash_temp_unit: debouncedRecipeData.mash_temp_unit,
- });
-
- return response.data;
- } catch (error) {
- // Fallback to offline calculation on API failure
- console.warn(
- "API metrics calculation failed, using offline calculation:",
- error
- );
-
- // Try offline calculation instead of fallback values
- try {
- const validation =
- OfflineMetricsCalculator.validateRecipeData(debouncedRecipeData);
- if (validation.isValid) {
- const calculatedMetrics =
- OfflineMetricsCalculator.calculateMetrics(debouncedRecipeData);
- return calculatedMetrics;
- }
- } catch (offlineError) {
- console.error("Offline calculation also failed:", offlineError);
- }
-
- // Only use fallback if everything fails
- return FALLBACK_METRICS;
- }
- }
-
- // Calculate metrics offline using brewing formulas
+ // Always calculate metrics locally using brewing formulas (offline-first)
try {
const validation =
OfflineMetricsCalculator.validateRecipeData(debouncedRecipeData);
if (!validation.isValid) {
console.warn(
- "Invalid recipe data for offline calculation:",
+ "Invalid recipe data for metrics calculation:",
validation.errors
);
return FALLBACK_METRICS;
@@ -143,30 +99,22 @@ export function useRecipeMetrics(
OfflineMetricsCalculator.calculateMetrics(debouncedRecipeData);
return calculatedMetrics;
} catch (error) {
- console.error("Offline metrics calculation failed:", error);
+ console.error("Metrics calculation failed:", error);
return FALLBACK_METRICS;
}
},
enabled: shouldEnable,
- // Cache configuration for offline support
- staleTime: isConnected ? 30000 : Infinity, // 30s online, never stale offline
+ // Cache configuration - calculations are deterministic and fast
+ staleTime: Infinity, // Never stale - recalculate only when inputs change
gcTime: 300000, // 5 minutes - keep in cache for quick access
- refetchOnMount: isConnected ? "always" : false,
- refetchOnReconnect: true,
+ refetchOnMount: false, // No need to refetch, calculations are deterministic
+ refetchOnReconnect: false, // No network dependency
refetchOnWindowFocus: false,
- // Retry configuration
- retry: (failureCount, error: any) => {
- // Don't retry on validation errors (400)
- if (error?.response?.status === 400) {
- return false;
- }
- // Only retry network errors when online
- return isConnected && failureCount < 2;
- },
- retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 5000),
+ // Retry configuration - only retry on unexpected errors
+ retry: false, // Local calculations don't need retries
// Data transformation
select: (data): Partial => {
diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts
index de9b76d5..1095dbee 100644
--- a/src/services/NotificationService.ts
+++ b/src/services/NotificationService.ts
@@ -21,6 +21,7 @@
import * as Notifications from "expo-notifications";
import * as Haptics from "expo-haptics";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
export interface HopAlert {
time: number;
@@ -102,7 +103,10 @@ export class NotificationService {
}
if (finalStatus !== "granted") {
- console.warn("Notification permissions not granted");
+ UnifiedLogger.warn(
+ "notifications",
+ "Notification permissions not granted"
+ );
return false;
}
@@ -119,7 +123,11 @@ export class NotificationService {
this.isInitialized = true;
return true;
} catch (error) {
- console.error("Failed to initialize notifications:", error);
+ UnifiedLogger.error(
+ "notifications",
+ "Failed to initialize notifications:",
+ error
+ );
return false;
}
}
@@ -158,7 +166,11 @@ export class NotificationService {
this.notificationIdentifiers.push(identifier);
return identifier;
} catch (error) {
- console.error("Failed to schedule hop alert:", error);
+ UnifiedLogger.error(
+ "notifications",
+ "Failed to schedule hop alert:",
+ error
+ );
return null;
}
}
@@ -187,7 +199,7 @@ export class NotificationService {
// Validate notification time - must be at least 1 second in the future
if (timeInSeconds < 1) {
// if (__DEV__) {
- // console.warn(
+ // UnifiedLogger.warn("notifications",
// `⚠️ Invalid notification time: ${timeInSeconds}s - must be >= 1s`
// );
// }
@@ -219,7 +231,11 @@ export class NotificationService {
return identifier;
} catch (error) {
- console.error("Failed to schedule timer alert:", error);
+ UnifiedLogger.error(
+ "notifications",
+ "Failed to schedule timer alert:",
+ error
+ );
return null;
}
}
@@ -263,7 +279,7 @@ export class NotificationService {
{ milestone: milestone.time }
);
} else if (__DEV__) {
- // console.warn(
+ // UnifiedLogger.warn("notifications",
// `⚠️ Skipping milestone ${milestone.time / 60}min - notifyTime too small (${notifyTime}s)`
// );
}
@@ -304,7 +320,11 @@ export class NotificationService {
await Notifications.cancelAllScheduledNotificationsAsync();
this.notificationIdentifiers = [];
} catch (error) {
- console.error("Failed to cancel notifications:", error);
+ UnifiedLogger.error(
+ "notifications",
+ "Failed to cancel notifications:",
+ error
+ );
}
}
@@ -318,7 +338,11 @@ export class NotificationService {
(id: string) => id !== identifier
);
} catch (error) {
- console.error("Failed to cancel notification:", error);
+ UnifiedLogger.error(
+ "notifications",
+ "Failed to cancel notification:",
+ error
+ );
}
}
@@ -341,7 +365,11 @@ export class NotificationService {
break;
}
} catch (error) {
- console.error("Failed to trigger haptic feedback:", error);
+ UnifiedLogger.error(
+ "notifications",
+ "Failed to trigger haptic feedback:",
+ error
+ );
}
}
@@ -374,7 +402,11 @@ export class NotificationService {
// Also trigger haptic feedback
await this.triggerHapticFeedback("medium");
} catch (error) {
- console.error("Failed to send immediate notification:", error);
+ UnifiedLogger.error(
+ "notifications",
+ "Failed to send immediate notification:",
+ error
+ );
}
}
@@ -386,7 +418,11 @@ export class NotificationService {
const { status } = await Notifications.getPermissionsAsync();
return status === "granted";
} catch (error) {
- console.error("Failed to check notification permissions:", error);
+ UnifiedLogger.error(
+ "notifications",
+ "Failed to check notification permissions:",
+ error
+ );
return false;
}
}
@@ -400,7 +436,11 @@ export class NotificationService {
try {
return await Notifications.getAllScheduledNotificationsAsync();
} catch (error) {
- console.error("Failed to get scheduled notifications:", error);
+ UnifiedLogger.error(
+ "notifications",
+ "Failed to get scheduled notifications:",
+ error
+ );
return [];
}
}
@@ -482,7 +522,8 @@ export class NotificationService {
}
} else if (__DEV__) {
// Debug logging for skipped hop alerts
- console.warn(
+ UnifiedLogger.warn(
+ "notifications",
`⚠️ Skipping hop alert for "${hop.name}" at ${hop.time}min - alertTime too small (${alertTime}s)`
);
}
diff --git a/src/services/api/apiService.ts b/src/services/api/apiService.ts
index 0691d09a..3b1e400f 100644
--- a/src/services/api/apiService.ts
+++ b/src/services/api/apiService.ts
@@ -28,6 +28,7 @@ import axios, {
} from "axios";
import * as SecureStore from "expo-secure-store";
import NetInfo from "@react-native-community/netinfo";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { STORAGE_KEYS, ENDPOINTS } from "@services/config";
import { setupIDInterceptors } from "./idInterceptor";
@@ -94,6 +95,10 @@ import {
CreateDryHopFromRecipeRequest,
UpdateDryHopRequest,
+ // BeerXML types
+ BeerXMLConvertRecipeRequest,
+ BeerXMLConvertRecipeResponse,
+
// Common types
ID,
IngredientType,
@@ -369,7 +374,7 @@ async function logApiError(normalizedError: NormalizedApiError, err: unknown) {
base.request?.url?.includes("/recipes/")
)
) {
- console.error("API Error:", base);
+ UnifiedLogger.error("api", "API Error:", base);
}
}
@@ -446,7 +451,7 @@ class TokenManager {
try {
return await SecureStore.getItemAsync(STORAGE_KEYS.ACCESS_TOKEN);
} catch (error) {
- console.error("Error getting token:", error);
+ UnifiedLogger.error("api", "Error getting token:", error);
return null;
}
}
@@ -459,7 +464,7 @@ class TokenManager {
try {
await SecureStore.setItemAsync(STORAGE_KEYS.ACCESS_TOKEN, token);
} catch (error) {
- console.error("Error setting token:", error);
+ UnifiedLogger.error("api", "Error setting token:", error);
throw error;
}
}
@@ -471,7 +476,7 @@ class TokenManager {
try {
await SecureStore.deleteItemAsync(STORAGE_KEYS.ACCESS_TOKEN);
} catch (error) {
- console.error("Error removing token:", error);
+ UnifiedLogger.error("api", "Error removing token:", error);
}
}
}
@@ -504,7 +509,11 @@ class DeveloperModeManager {
}
return false;
} catch (error) {
- console.warn("Failed to check simulated offline mode:", error);
+ UnifiedLogger.warn(
+ "api",
+ "Failed to check simulated offline mode:",
+ error
+ );
return false;
}
}
@@ -1057,6 +1066,11 @@ const ApiService = {
ingredients: any[];
}): Promise> =>
api.post(ENDPOINTS.BEERXML.CREATE_INGREDIENTS, data),
+
+ convertRecipe: (
+ data: BeerXMLConvertRecipeRequest
+ ): Promise> =>
+ api.post(ENDPOINTS.BEERXML.CONVERT_RECIPE, data),
},
// AI endpoints
diff --git a/src/services/api/queryClient.ts b/src/services/api/queryClient.ts
index 092105d3..07e02a37 100644
--- a/src/services/api/queryClient.ts
+++ b/src/services/api/queryClient.ts
@@ -105,6 +105,7 @@ export const QUERY_KEYS = {
// Recipes
RECIPES: ["recipes"] as const,
+ USER_RECIPES: ["userRecipes"] as const,
RECIPE: (id: string) => ["recipes", id] as const,
RECIPE_METRICS: (id: string) => ["recipes", id, "metrics"] as const,
RECIPE_VERSIONS: (id: string) => ["recipes", id, "versions"] as const,
diff --git a/src/services/beerxml/BeerXMLService.ts b/src/services/beerxml/BeerXMLService.ts
index f5aaeaa3..746a1a89 100644
--- a/src/services/beerxml/BeerXMLService.ts
+++ b/src/services/beerxml/BeerXMLService.ts
@@ -1,6 +1,7 @@
import ApiService from "@services/api/apiService";
import { BeerXMLService as StorageBeerXMLService } from "@services/storageService";
-import { Recipe, RecipeIngredient } from "@src/types";
+import { Recipe, RecipeIngredient, UnitSystem } from "@src/types";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
// Service-specific interfaces for BeerXML operations
@@ -9,7 +10,7 @@ interface FileValidationResult {
errors: string[];
}
-interface BeerXMLRecipe extends Partial {
+export interface BeerXMLRecipe extends Partial {
ingredients: RecipeIngredient[];
metadata?: BeerXMLMetadata;
}
@@ -125,7 +126,7 @@ class BeerXMLService {
saveMethod: saveResult.method,
};
} catch (error) {
- console.error("🍺 BeerXML Export - Error:", error);
+ void UnifiedLogger.error("beerxml", "🍺 BeerXML Export - Error:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Export failed",
@@ -155,7 +156,7 @@ class BeerXMLService {
filename: result.filename,
};
} catch (error) {
- console.error("🍺 BeerXML Import - Error:", error);
+ void UnifiedLogger.error("beerxml", "🍺 BeerXML Import - Error:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Import failed",
@@ -181,7 +182,10 @@ class BeerXMLService {
? response.data.recipes
: [];
if (!Array.isArray(recipes) || recipes.length === 0) {
- console.warn("🍺 BeerXML Parse - No recipes found in response");
+ void UnifiedLogger.warn(
+ "beerxml",
+ "🍺 BeerXML Parse - No recipes found in response"
+ );
}
const transformedRecipes = recipes.map((recipeData: any) => ({
...recipeData.recipe,
@@ -191,7 +195,7 @@ class BeerXMLService {
return transformedRecipes;
} catch (error) {
- console.error("🍺 BeerXML Parse - Error:", error);
+ void UnifiedLogger.error("beerxml", "🍺 BeerXML Parse - Error:", error);
throw new Error(
`Failed to parse BeerXML: ${error instanceof Error ? error.message : "Unknown error"}`
);
@@ -245,7 +249,7 @@ class BeerXMLService {
return matchingResults as IngredientMatchingResult[];
} catch (error) {
- console.error("🍺 BeerXML Match - Error:", error);
+ void UnifiedLogger.error("beerxml", "🍺 BeerXML Match - Error:", error);
throw new Error(
`Failed to match ingredients: ${error instanceof Error ? error.message : "Unknown error"}`
);
@@ -269,7 +273,7 @@ class BeerXMLService {
return createdIngredients;
} catch (error) {
- console.error("🍺 BeerXML Create - Error:", error);
+ void UnifiedLogger.error("beerxml", "🍺 BeerXML Create - Error:", error);
throw new Error(
`Failed to create ingredients: ${error instanceof Error ? error.message : "Unknown error"}`
);
@@ -396,6 +400,107 @@ class BeerXMLService {
highConfidence,
};
}
+
+ /**
+ * Detect if recipe uses different unit system than user preference
+ * Returns the detected recipe unit system
+ */
+ detectRecipeUnitSystem(recipe: BeerXMLRecipe): UnitSystem | "mixed" {
+ let metricCount = 0;
+ let imperialCount = 0;
+
+ // Check batch size unit (trim to handle whitespace)
+ const batchUnit = recipe.batch_size_unit?.toLowerCase().trim() || "";
+ if (["l", "liter", "liters", "litre", "litres", "ml"].includes(batchUnit)) {
+ metricCount++;
+ } else if (["gal", "gallon", "gallons"].includes(batchUnit)) {
+ imperialCount++;
+ }
+
+ // Check ingredient units (trim to handle whitespace)
+ recipe.ingredients?.forEach(ingredient => {
+ const unit = ingredient.unit?.toLowerCase().trim() || "";
+ if (
+ ["g", "kg", "gram", "grams", "kilogram", "kilograms"].includes(unit)
+ ) {
+ metricCount++;
+ } else if (
+ ["oz", "lb", "lbs", "ounce", "ounces", "pound", "pounds"].includes(unit)
+ ) {
+ imperialCount++;
+ }
+ });
+
+ // Determine predominant system
+ if (metricCount > 0 && imperialCount === 0) {
+ return "metric";
+ }
+ if (imperialCount > 0 && metricCount === 0) {
+ return "imperial";
+ }
+ if (metricCount > imperialCount) {
+ return "metric";
+ }
+ if (imperialCount > metricCount) {
+ return "imperial";
+ }
+ return "mixed"; // Equal or unknown
+ }
+
+ /**
+ * Convert recipe units to target unit system with brewing-friendly normalization
+ *
+ * BeerXML files are always in metric per spec. This function:
+ * 1. Converts from metric to target system (if imperial)
+ * 2. Normalizes values to brewing-friendly increments (e.g., 1oz -> 30g)
+ *
+ * IMPORTANT: Normalization is applied even when importing as metric, because
+ * the recipe may have originally been imperial and converted to metric for
+ * BeerXML export, resulting in odd values like 28.3g (1oz). The backend's
+ * conversion endpoint rounds these to practical brewing increments (30g) unless normalization argument is explicitly passed as false.
+ */
+ async convertRecipeUnits(
+ recipe: BeerXMLRecipe,
+ targetUnitSystem: UnitSystem,
+ normalize: boolean = true
+ ): Promise<{ recipe: BeerXMLRecipe; warnings?: string[] }> {
+ try {
+ // Call dedicated BeerXML conversion endpoint
+ const response = await ApiService.beerxml.convertRecipe({
+ recipe,
+ target_system: targetUnitSystem,
+ normalize,
+ });
+
+ const convertedRecipe = response.data.recipe;
+ const warnings = response.data.warnings ?? [];
+
+ if (!convertedRecipe) {
+ void UnifiedLogger.warn(
+ "beerxml",
+ "No converted recipe returned, using original"
+ );
+ return { recipe, warnings: [] };
+ }
+
+ return {
+ recipe: convertedRecipe as BeerXMLRecipe,
+ warnings,
+ };
+ } catch (error) {
+ void UnifiedLogger.error(
+ "beerxml",
+ "Error converting recipe units:",
+ error
+ );
+ // Return original recipe if conversion fails - don't block import
+ void UnifiedLogger.warn(
+ "beerxml",
+ "Unit conversion failed, continuing with original units"
+ );
+ return { recipe, warnings: [] };
+ }
+ }
}
// Create and export singleton instance
diff --git a/src/services/brewing/OfflineMetricsCalculator.ts b/src/services/brewing/OfflineMetricsCalculator.ts
index f72d886f..1f18b212 100644
--- a/src/services/brewing/OfflineMetricsCalculator.ts
+++ b/src/services/brewing/OfflineMetricsCalculator.ts
@@ -5,13 +5,21 @@
* Implements standard brewing formulas for OG, FG, ABV, IBU, and SRM.
*/
-import { RecipeMetrics, RecipeFormData, RecipeIngredient } from "@src/types";
+import { isDryHopIngredient } from "@/src/utils/recipeUtils";
+import {
+ RecipeMetrics,
+ RecipeFormData,
+ RecipeMetricsInput,
+ RecipeIngredient,
+} from "@src/types";
export class OfflineMetricsCalculator {
/**
* Calculate recipe metrics offline using standard brewing formulas
*/
- static calculateMetrics(recipeData: RecipeFormData): RecipeMetrics {
+ static calculateMetrics(
+ recipeData: RecipeFormData | RecipeMetricsInput
+ ): RecipeMetrics {
// Validate first
const { isValid } = this.validateRecipeData(recipeData);
if (!isValid) {
@@ -68,7 +76,8 @@ export class OfflineMetricsCalculator {
for (const fermentable of fermentables) {
// Get potential (extract potential) - default to 35 if not specified
- const potential = fermentable.potential ?? 35;
+ const potential =
+ "potential" in fermentable ? (fermentable.potential ?? 35) : 35;
// Convert amount to pounds based on unit
const amountLbs = this.convertToPounds(
fermentable.amount ?? 0,
@@ -98,8 +107,10 @@ export class OfflineMetricsCalculator {
let attenuationCount = 0;
for (const yeast of yeasts) {
- if (yeast.attenuation !== undefined && yeast.attenuation >= 0) {
- totalAttenuation += yeast.attenuation;
+ const attenuation =
+ "attenuation" in yeast ? yeast.attenuation : undefined;
+ if (attenuation !== undefined && attenuation >= 0) {
+ totalAttenuation += attenuation;
attenuationCount++;
}
}
@@ -145,16 +156,11 @@ export class OfflineMetricsCalculator {
? (hop.time ?? 0) // only force 0 when time is missing
: (hop.time ?? boilTime); // default to boil time for boil additions
- // Skip non-bittering additions
- if (
- use === "dry-hop" ||
- use === "dry hop" ||
- use === "dryhop" ||
- hopTime <= 0
- ) {
+ // Skip non-bittering additions (dry hops or hops with no boil time)
+ if (isDryHopIngredient(hop) || hopTime <= 0) {
continue;
}
- const alphaAcid = hop.alpha_acid ?? 5; // Default 5% AA (allow 0)
+ const alphaAcid = "alpha_acid" in hop ? (hop.alpha_acid ?? 5) : 5; // Default 5% AA (allow 0)
// Convert hop amount to ounces for IBU calculation
const amountOz = this.convertToOunces(hop.amount ?? 0, hop.unit);
@@ -196,7 +202,7 @@ export class OfflineMetricsCalculator {
let totalMCU = 0; // Malt Color Units
for (const grain of grains) {
- const colorLovibond = grain.color ?? 2; // Default to 2L if not specified
+ const colorLovibond = "color" in grain ? (grain.color ?? 2) : 2; // Default to 2L if not specified
// Convert grain amount to pounds for SRM calculation
const amountLbs = this.convertToPounds(grain.amount ?? 0, grain.unit);
@@ -213,7 +219,7 @@ export class OfflineMetricsCalculator {
/**
* Validate recipe data for calculations
*/
- static validateRecipeData(recipeData: RecipeFormData): {
+ static validateRecipeData(recipeData: RecipeFormData | RecipeMetricsInput): {
isValid: boolean;
errors: string[];
} {
@@ -254,17 +260,19 @@ export class OfflineMetricsCalculator {
if (ing.amount !== undefined && ing.amount < 0) {
errors.push(`${ing.name || "Ingredient"} amount must be >= 0`);
}
+ const alphaAcid = "alpha_acid" in ing ? ing.alpha_acid : undefined;
if (
ing.type === "hop" &&
- ing.alpha_acid !== undefined &&
- (ing.alpha_acid < 0 || ing.alpha_acid > 30)
+ alphaAcid !== undefined &&
+ (alphaAcid < 0 || alphaAcid > 30)
) {
errors.push("Hop alpha acid must be between 0 and 30");
}
+ const attenuation = "attenuation" in ing ? ing.attenuation : undefined;
if (
ing.type === "yeast" &&
- ing.attenuation !== undefined &&
- (ing.attenuation < 0 || ing.attenuation > 100)
+ attenuation !== undefined &&
+ (attenuation < 0 || attenuation > 100)
) {
errors.push("Yeast attenuation must be between 0 and 100");
}
diff --git a/src/services/calculators/HydrometerCorrectionCalculator.ts b/src/services/calculators/HydrometerCorrectionCalculator.ts
index d939e884..36031362 100644
--- a/src/services/calculators/HydrometerCorrectionCalculator.ts
+++ b/src/services/calculators/HydrometerCorrectionCalculator.ts
@@ -4,6 +4,7 @@
* Most hydrometers are calibrated at 60°F (15.5°C)
*/
+import { TemperatureUnit } from "@/src/types/common";
import { UnitConverter } from "./UnitConverter";
export interface CorrectionResult {
@@ -28,7 +29,7 @@ export class HydrometerCorrectionCalculator {
measuredGravity: number,
wortTemp: number,
calibrationTemp: number,
- tempUnit: "f" | "c" = "f"
+ tempUnit: TemperatureUnit = "F"
): CorrectionResult {
// Validate inputs
this.validateInputs(measuredGravity, wortTemp, calibrationTemp, tempUnit);
@@ -37,12 +38,12 @@ export class HydrometerCorrectionCalculator {
let wortTempF = wortTemp;
let calibrationTempF = calibrationTemp;
- if (tempUnit === "c") {
- wortTempF = UnitConverter.convertTemperature(wortTemp, "c", "f");
+ if (tempUnit === "C") {
+ wortTempF = UnitConverter.convertTemperature(wortTemp, "C", "F");
calibrationTempF = UnitConverter.convertTemperature(
calibrationTemp,
- "c",
- "f"
+ "C",
+ "F"
);
}
@@ -83,10 +84,10 @@ export class HydrometerCorrectionCalculator {
public static calculateCorrectionDefault(
measuredGravity: number,
wortTemp: number,
- tempUnit: "f" | "c" = "f"
+ tempUnit: TemperatureUnit = "F"
): CorrectionResult {
const defaultCalibrationTemp =
- tempUnit === "f"
+ tempUnit === "F"
? this.DEFAULT_CALIBRATION_TEMP_F
: this.DEFAULT_CALIBRATION_TEMP_C;
@@ -107,11 +108,11 @@ export class HydrometerCorrectionCalculator {
measuredGravity: number,
targetGravity: number,
calibrationTemp: number,
- tempUnit: "f" | "c" = "f"
+ tempUnit: TemperatureUnit = "F"
): number {
// Set initial bounds based on physical temperature limits
let low: number, high: number;
- if (tempUnit === "f") {
+ if (tempUnit === "F") {
low = 32; // Freezing point of water in Fahrenheit
high = 212; // Boiling point of water in Fahrenheit
} else {
@@ -153,14 +154,21 @@ export class HydrometerCorrectionCalculator {
/**
* Get correction table for common temperatures
+ *
+ * @param measuredGravity - The specific gravity reading from the hydrometer
+ * @param calibrationTemp - The calibration temperature of the hydrometer (default: 60°F)
+ * Note: Default of 60 is meaningful only when tempUnit="F".
+ * If using tempUnit="C", explicitly pass 15.5 for standard calibration.
+ * @param tempUnit - Temperature unit ("F" or "C", default: "F")
+ * @returns Array of temperature-correction pairs for display
*/
public static getCorrectionTable(
measuredGravity: number,
calibrationTemp: number = 60,
- tempUnit: "f" | "c" = "f"
+ tempUnit: TemperatureUnit = "F"
): { temp: number; correctedGravity: number; correction: number }[] {
const temps =
- tempUnit === "f"
+ tempUnit === "F"
? [50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 110, 120]
: [10, 13, 15.5, 18, 21, 24, 27, 29, 32, 35, 38, 43, 49];
@@ -186,7 +194,7 @@ export class HydrometerCorrectionCalculator {
public static isCorrectionSignificant(
wortTemp: number,
calibrationTemp: number,
- tempUnit: "f" | "c" = "f"
+ tempUnit: TemperatureUnit = "F"
): boolean {
// Calculate correction for a reference gravity of 1.050
const result = this.calculateCorrection(
@@ -205,7 +213,7 @@ export class HydrometerCorrectionCalculator {
measuredGravity: number,
wortTemp: number,
calibrationTemp: number,
- tempUnit: "f" | "c"
+ tempUnit: TemperatureUnit
): void {
// Validate specific gravity
if (measuredGravity < 0.99 || measuredGravity > 1.2) {
@@ -213,7 +221,7 @@ export class HydrometerCorrectionCalculator {
}
// Validate temperatures based on unit
- if (tempUnit === "f") {
+ if (tempUnit === "F") {
if (wortTemp < 32 || wortTemp > 212) {
throw new Error("Wort temperature must be between 32°F and 212°F");
}
@@ -239,13 +247,13 @@ export class HydrometerCorrectionCalculator {
*/
public static getCalibrationTemperatures(): Record<
string,
- { f: number; c: number }
+ { F: number; C: number }
> {
return {
- Standard: { f: 60, c: 15.5 },
- European: { f: 68, c: 20 },
- Precision: { f: 68, c: 20 },
- Digital: { f: 68, c: 20 },
+ Standard: { F: 60, C: 15.5 },
+ European: { F: 68, C: 20 },
+ Precision: { F: 68, C: 20 },
+ Digital: { F: 68, C: 20 },
};
}
}
diff --git a/src/services/calculators/PrimingSugarCalculator.ts b/src/services/calculators/PrimingSugarCalculator.ts
index c8498b9e..64c883b0 100644
--- a/src/services/calculators/PrimingSugarCalculator.ts
+++ b/src/services/calculators/PrimingSugarCalculator.ts
@@ -3,6 +3,7 @@
* Calculates amount of priming sugar needed for carbonation
*/
+import { TemperatureUnit } from "@/src/types/common";
import { UnitConverter } from "./UnitConverter";
export interface PrimingSugarResult {
@@ -94,11 +95,11 @@ export class PrimingSugarCalculator {
*/
public static estimateResidualCO2(
fermentationTemp: number,
- tempUnit: "f" | "c" = "f"
+ tempUnit: TemperatureUnit = "F"
): number {
let tempF = fermentationTemp;
- if (tempUnit === "c") {
- tempF = UnitConverter.convertTemperature(fermentationTemp, "c", "f");
+ if (tempUnit === "C") {
+ tempF = UnitConverter.convertTemperature(fermentationTemp, "C", "F");
}
// Find closest temperature in lookup table
diff --git a/src/services/calculators/StrikeWaterCalculator.ts b/src/services/calculators/StrikeWaterCalculator.ts
index 5a761672..8fe806fc 100644
--- a/src/services/calculators/StrikeWaterCalculator.ts
+++ b/src/services/calculators/StrikeWaterCalculator.ts
@@ -3,6 +3,7 @@
* Calculates the temperature of water needed to achieve target mash temperature
*/
+import { TemperatureUnit } from "@/src/types/common";
import { UnitConverter } from "./UnitConverter";
export interface StrikeWaterResult {
@@ -32,7 +33,7 @@ export class StrikeWaterCalculator {
grainTemp: number,
targetMashTemp: number,
waterToGrainRatio: number = 1.25,
- tempUnit: "f" | "c" = "f",
+ tempUnit: TemperatureUnit = "F",
tunWeight: number = 10 // pounds of tun material
): StrikeWaterResult {
// Convert grain weight to pounds for calculations
@@ -46,12 +47,12 @@ export class StrikeWaterCalculator {
let grainTempF = grainTemp;
let targetMashTempF = targetMashTemp;
- if (tempUnit === "c") {
- grainTempF = UnitConverter.convertTemperature(grainTemp, "c", "f");
+ if (tempUnit === "C") {
+ grainTempF = UnitConverter.convertTemperature(grainTemp, "C", "F");
targetMashTempF = UnitConverter.convertTemperature(
targetMashTemp,
- "c",
- "f"
+ "C",
+ "F"
);
}
@@ -74,8 +75,8 @@ export class StrikeWaterCalculator {
// Convert results back to requested unit
let strikeTemp = strikeWaterTempF;
- if (tempUnit === "c") {
- strikeTemp = UnitConverter.convertTemperature(strikeWaterTempF, "f", "c");
+ if (tempUnit === "C") {
+ strikeTemp = UnitConverter.convertTemperature(strikeWaterTempF, "F", "C");
}
return {
@@ -93,24 +94,24 @@ export class StrikeWaterCalculator {
targetMashTemp: number,
currentMashVolume: number, // quarts
infusionWaterTemp: number,
- tempUnit: "f" | "c" = "f"
+ tempUnit: TemperatureUnit = "F"
): InfusionResult {
// Convert temperatures to Fahrenheit for calculations
let currentTempF = currentMashTemp;
let targetTempF = targetMashTemp;
let infusionTempF = infusionWaterTemp;
- if (tempUnit === "c") {
+ if (tempUnit === "C") {
currentTempF = UnitConverter.convertTemperature(
currentMashTemp,
- "c",
- "f"
+ "C",
+ "F"
);
- targetTempF = UnitConverter.convertTemperature(targetMashTemp, "c", "f");
+ targetTempF = UnitConverter.convertTemperature(targetMashTemp, "C", "F");
infusionTempF = UnitConverter.convertTemperature(
infusionWaterTemp,
- "c",
- "f"
+ "C",
+ "F"
);
}
@@ -131,9 +132,9 @@ export class StrikeWaterCalculator {
// Convert temperatures back to requested unit
let targetTemp = targetTempF;
let infusionTemp = infusionTempF;
- if (tempUnit === "c") {
- targetTemp = UnitConverter.convertTemperature(targetTempF, "f", "c");
- infusionTemp = UnitConverter.convertTemperature(infusionTempF, "f", "c");
+ if (tempUnit === "C") {
+ targetTemp = UnitConverter.convertTemperature(targetTempF, "F", "C");
+ infusionTemp = UnitConverter.convertTemperature(infusionTempF, "F", "C");
}
return {
@@ -178,7 +179,7 @@ export class StrikeWaterCalculator {
grainTemp: number,
targetMashTemp: number,
waterToGrainRatio: number,
- tempUnit: "f" | "c"
+ tempUnit: TemperatureUnit
): void {
if (grainWeight <= 0) {
throw new Error("Grain weight must be greater than 0");
@@ -189,7 +190,7 @@ export class StrikeWaterCalculator {
}
// Temperature validation based on unit
- if (tempUnit === "f") {
+ if (tempUnit === "F") {
if (grainTemp < 32 || grainTemp > 120) {
throw new Error("Grain temperature must be between 32°F and 120°F");
}
diff --git a/src/services/calculators/UnitConverter.ts b/src/services/calculators/UnitConverter.ts
index 57966588..25429360 100644
--- a/src/services/calculators/UnitConverter.ts
+++ b/src/services/calculators/UnitConverter.ts
@@ -47,51 +47,48 @@ export class UnitConverter {
tbsp: 0.0147868,
};
+ /**
+ * Normalize temperature unit to canonical form ("C" or "F")
+ * Only supports Celsius and Fahrenheit (Kelvin is not used in brewing)
+ * @private
+ */
+ private static normalizeTemperatureUnit(unit: string): "C" | "F" {
+ const normalized = unit.toLowerCase();
+ switch (normalized) {
+ case "f":
+ case "fahrenheit":
+ return "F";
+ case "c":
+ case "celsius":
+ return "C";
+ default:
+ throw new Error(
+ `Unsupported temperature unit: ${unit}. Only Celsius (C) and Fahrenheit (F) are supported for brewing.`
+ );
+ }
+ }
+
// Temperature conversion functions
public static convertTemperature(
value: number,
fromUnit: string,
toUnit: string
): number {
- const from = fromUnit.toLowerCase();
- const to = toUnit.toLowerCase();
+ // Normalize units to canonical form
+ const from = this.normalizeTemperatureUnit(fromUnit);
+ const to = this.normalizeTemperatureUnit(toUnit);
if (from === to) {
return value;
}
- // Convert to Celsius first
- let celsius: number;
- switch (from) {
- case "f":
- case "fahrenheit":
- celsius = (value - 32) * (5 / 9);
- break;
- case "c":
- case "celsius":
- celsius = value;
- break;
- case "k":
- case "kelvin":
- celsius = value - 273.15;
- break;
- default:
- throw new Error(`Unknown temperature unit: ${fromUnit}`);
- }
-
- // Convert from Celsius to target unit
- switch (to) {
- case "f":
- case "fahrenheit":
- return celsius * (9 / 5) + 32;
- case "c":
- case "celsius":
- return celsius;
- case "k":
- case "kelvin":
- return celsius + 273.15;
- default:
- throw new Error(`Unknown temperature unit: ${toUnit}`);
+ // Direct conversion between C and F
+ if (from === "F") {
+ // F to C
+ return (value - 32) * (5 / 9);
+ } else {
+ // C to F
+ return value * (9 / 5) + 32;
}
}
@@ -190,7 +187,7 @@ export class UnitConverter {
}
public static getTemperatureUnits(): string[] {
- return ["f", "c", "k", "fahrenheit", "celsius", "kelvin"];
+ return ["F", "C"]; // Only Celsius and Fahrenheit are used in brewing
}
// Validation methods
@@ -203,8 +200,12 @@ export class UnitConverter {
}
public static isValidTemperatureUnit(unit: string): boolean {
- const validUnits = ["f", "c", "k", "fahrenheit", "celsius", "kelvin"];
- return validUnits.includes(unit.toLowerCase());
+ try {
+ this.normalizeTemperatureUnit(unit);
+ return true;
+ } catch {
+ return false;
+ }
}
// Formatting methods for display
@@ -219,6 +220,7 @@ export class UnitConverter {
}
public static formatTemperature(temp: number, unit: string): string {
- return `${temp.toFixed(1)}°${unit.toUpperCase()}`;
+ const normalizedUnit = this.normalizeTemperatureUnit(unit);
+ return `${temp.toFixed(1)}°${normalizedUnit}`;
}
}
diff --git a/src/services/config.ts b/src/services/config.ts
index 46fac297..d7792be5 100644
--- a/src/services/config.ts
+++ b/src/services/config.ts
@@ -166,6 +166,7 @@ export const ENDPOINTS = {
PARSE: "/beerxml/parse",
MATCH_INGREDIENTS: "/beerxml/match-ingredients",
CREATE_INGREDIENTS: "/beerxml/create-ingredients",
+ CONVERT_RECIPE: "/beerxml/convert-recipe",
},
// AI
diff --git a/src/services/offlineV2/LegacyMigrationService.ts b/src/services/offlineV2/LegacyMigrationService.ts
index e153ec6c..03a21844 100644
--- a/src/services/offlineV2/LegacyMigrationService.ts
+++ b/src/services/offlineV2/LegacyMigrationService.ts
@@ -7,10 +7,11 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { UserCacheService } from "./UserCacheService";
-import { Recipe } from "@src/types";
+import { Recipe, UnitSystem } from "@src/types";
import { OfflineRecipe } from "@src/types/offline";
import { STORAGE_KEYS } from "@services/config";
import { generateUniqueId } from "@utils/keyUtils";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
export interface MigrationResult {
migrated: number;
@@ -25,7 +26,7 @@ export class LegacyMigrationService {
*/
static async migrateLegacyRecipesToV2(
userId: string,
- userUnitSystem: "imperial" | "metric" = "imperial"
+ userUnitSystem: UnitSystem = "metric"
): Promise {
const result: MigrationResult = {
migrated: 0,
@@ -35,16 +36,18 @@ export class LegacyMigrationService {
};
try {
- console.log(`[LegacyMigration] Starting migration for user: ${userId}`);
-
// Get legacy recipes from old storage
const legacyRecipes = await this.getLegacyRecipes(userId);
- console.log(
- `[LegacyMigration] Found ${legacyRecipes.length} legacy recipes`
+ void UnifiedLogger.info(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Found ${legacyRecipes.length} legacy recipes`
);
if (legacyRecipes.length === 0) {
- console.log(`[LegacyMigration] No legacy recipes to migrate`);
+ void UnifiedLogger.info(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `No legacy recipes to migrate`
+ );
return result;
}
@@ -55,8 +58,9 @@ export class LegacyMigrationService {
);
const existingIds = new Set(existingV2Recipes.map(r => r.id));
- console.log(
- `[LegacyMigration] Found ${existingV2Recipes.length} existing V2 recipes`
+ void UnifiedLogger.info(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Found ${existingV2Recipes.length} existing V2 recipes`
);
// Migrate each legacy recipe
@@ -64,8 +68,9 @@ export class LegacyMigrationService {
try {
// Skip if already exists in V2 cache
if (existingIds.has(legacyRecipe.id)) {
- console.log(
- `[LegacyMigration] Skipping duplicate recipe: ${legacyRecipe.id}`
+ void UnifiedLogger.info(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Skipping duplicate recipe: ${legacyRecipe.id}`
);
result.skipped++;
continue;
@@ -88,13 +93,15 @@ export class LegacyMigrationService {
tempId: legacyRecipe.tempId,
});
- console.log(
- `[LegacyMigration] Migrated recipe: ${legacyRecipe.name} (${legacyRecipe.id})`
+ void UnifiedLogger.info(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Migrated recipe: ${legacyRecipe.name} (${legacyRecipe.id})`
);
result.migrated++;
} catch (error) {
- console.error(
- `[LegacyMigration] Failed to migrate recipe ${legacyRecipe.id}:`,
+ void UnifiedLogger.error(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Failed to migrate recipe ${legacyRecipe.id}:`,
error
);
result.errors++;
@@ -104,18 +111,24 @@ export class LegacyMigrationService {
}
}
- console.log(`[LegacyMigration] Migration completed:`, result);
+ void UnifiedLogger.info(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Migration completed:`,
+ result
+ );
// Clear legacy recipes after successful migration
if (result.migrated > 0) {
try {
await this.clearLegacyRecipes(userId);
- console.log(
- `[LegacyMigration] Successfully cleared legacy recipes after migration`
+ void UnifiedLogger.info(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Successfully cleared legacy recipes after migration`
);
} catch (clearError) {
- console.error(
- `[LegacyMigration] Failed to clear legacy recipes after migration:`,
+ void UnifiedLogger.error(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Failed to clear legacy recipes after migration:`,
clearError
);
// Don't fail the entire migration if cleanup fails
@@ -124,7 +137,11 @@ export class LegacyMigrationService {
return result;
} catch (error) {
- console.error(`[LegacyMigration] Migration failed:`, error);
+ void UnifiedLogger.error(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Migration failed:`,
+ error
+ );
result.errors++;
result.errorDetails.push(
`Migration failed: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -155,7 +172,11 @@ export class LegacyMigrationService {
recipe => recipe.user_id === userId && !recipe.isDeleted
);
} catch (error) {
- console.error(`[LegacyMigration] Failed to load legacy recipes:`, error);
+ void UnifiedLogger.error(
+ "LegacyMigration.migrateLegacyRecipesToV2",
+ `Failed to load legacy recipes:`,
+ error
+ );
return [];
}
}
@@ -166,7 +187,7 @@ export class LegacyMigrationService {
private static convertLegacyToV2Recipe(
legacyRecipe: OfflineRecipe,
userId: string,
- userUnitSystem: "imperial" | "metric"
+ userUnitSystem: UnitSystem
): Recipe {
// Create base recipe from legacy data
const v2Recipe: Recipe = {
@@ -193,10 +214,16 @@ export class LegacyMigrationService {
parent_recipe_id: legacyRecipe.parent_recipe_id,
original_author: legacyRecipe.original_author,
// Add missing required fields based on user's unit system
- batch_size_unit: userUnitSystem === "metric" ? "l" : "gal",
- unit_system: userUnitSystem,
- mash_temperature: userUnitSystem === "metric" ? 67 : 152, // 67°C ≈ 152°F
- mash_temp_unit: userUnitSystem === "metric" ? "C" : "F",
+ batch_size_unit:
+ legacyRecipe.batch_size_unit ||
+ (userUnitSystem === "metric" ? "l" : "gal"),
+ unit_system: legacyRecipe.unit_system || userUnitSystem,
+ mash_temperature:
+ legacyRecipe.mash_temperature ||
+ (userUnitSystem === "metric" ? 67 : 152), // Default mash temp values
+ mash_temp_unit:
+ legacyRecipe.mash_temp_unit ||
+ (userUnitSystem === "metric" ? "C" : "F"),
notes: "", // Default empty notes
};
@@ -225,8 +252,9 @@ export class LegacyMigrationService {
*/
static async clearLegacyRecipes(userId: string): Promise {
try {
- console.log(
- `[LegacyMigration] Clearing legacy recipes for user: ${userId}`
+ void UnifiedLogger.info(
+ "LegacyMigration.clearLegacyRecipes",
+ `Clearing legacy recipes for user: ${userId}`
);
const offlineData = await AsyncStorage.getItem(
@@ -255,11 +283,16 @@ export class LegacyMigrationService {
JSON.stringify(updatedData)
);
- console.log(
- `[LegacyMigration] Cleared legacy recipes for user ${userId}`
+ void UnifiedLogger.info(
+ "LegacyMigration.clearLegacyRecipes",
+ `Cleared legacy recipes for user: ${userId}`
);
} catch (error) {
- console.error(`[LegacyMigration] Failed to clear legacy recipes:`, error);
+ void UnifiedLogger.error(
+ "LegacyMigration.clearLegacyRecipes",
+ `Failed to clear legacy recipes:`,
+ error
+ );
throw error;
}
}
diff --git a/src/services/offlineV2/StartupHydrationService.ts b/src/services/offlineV2/StartupHydrationService.ts
index 144b485f..83eade59 100644
--- a/src/services/offlineV2/StartupHydrationService.ts
+++ b/src/services/offlineV2/StartupHydrationService.ts
@@ -7,6 +7,8 @@
import { UserCacheService } from "./UserCacheService";
import { StaticDataService } from "./StaticDataService";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
+import type { UnitSystem } from "@/src/types";
export class StartupHydrationService {
private static isHydrating = false;
@@ -17,32 +19,45 @@ export class StartupHydrationService {
*/
static async hydrateOnStartup(
userId: string,
- userUnitSystem: "imperial" | "metric" = "imperial"
+ userUnitSystem: UnitSystem = "metric"
): Promise {
// Prevent multiple concurrent hydrations
if (this.isHydrating || this.hasHydrated) {
- console.log(
- `[StartupHydrationService] Hydration already in progress or completed`
- );
return;
}
this.isHydrating = true;
- console.log(
- `[StartupHydrationService] Starting hydration for user: "${userId}"`
- );
try {
// Hydrate in parallel for better performance
- await Promise.allSettled([
+ const results = await Promise.allSettled([
this.hydrateUserData(userId, userUnitSystem),
this.hydrateStaticData(),
]);
- this.hasHydrated = true;
- console.log(`[StartupHydrationService] Hydration completed successfully`);
+ // Only mark as hydrated if both succeeded
+ const hasFailure = results.some(r => r.status === "rejected");
+ if (!hasFailure) {
+ this.hasHydrated = true;
+ } else {
+ void UnifiedLogger.warn(
+ "offline-hydration",
+ "[StartupHydrationService] Partial hydration failure - will retry on next startup",
+ {
+ results: results.map((r, i) => ({
+ type: i === 0 ? "userData" : "staticData",
+ status: r.status,
+ reason: r.status === "rejected" ? r.reason : undefined,
+ })),
+ }
+ );
+ }
} catch (error) {
- console.error(`[StartupHydrationService] Hydration failed:`, error);
+ void UnifiedLogger.error(
+ "offline-hydration",
+ "[StartupHydrationService] Hydration failed:",
+ error
+ );
// Don't throw - app should still work even if hydration fails
} finally {
this.isHydrating = false;
@@ -54,11 +69,9 @@ export class StartupHydrationService {
*/
private static async hydrateUserData(
userId: string,
- userUnitSystem: "imperial" | "metric" = "imperial"
+ userUnitSystem: UnitSystem = "metric"
): Promise {
try {
- console.log(`[StartupHydrationService] Hydrating user data...`);
-
// Check if user already has cached recipes
const existingRecipes = await UserCacheService.getRecipes(
userId,
@@ -66,23 +79,15 @@ export class StartupHydrationService {
);
if (existingRecipes.length === 0) {
- console.log(
- `[StartupHydrationService] No cached recipes found, will hydrate from server`
- );
// The UserCacheService.getRecipes() method will automatically hydrate
// So we don't need to do anything special here
- } else {
- console.log(
- `[StartupHydrationService] User already has ${existingRecipes.length} cached recipes`
- );
}
// TODO: Add brew sessions hydration when implemented
// await this.hydrateBrewSessions(userId);
-
- console.log(`[StartupHydrationService] User data hydration completed`);
} catch (error) {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-hydration",
`[StartupHydrationService] User data hydration failed:`,
error
);
@@ -95,22 +100,15 @@ export class StartupHydrationService {
*/
private static async hydrateStaticData(): Promise {
try {
- console.log(`[StartupHydrationService] Hydrating static data...`);
-
- // Check and update ingredients cache
- const ingredientsStats = await StaticDataService.getCacheStats();
- if (!ingredientsStats.ingredients.cached) {
- console.log(
- `[StartupHydrationService] No cached ingredients found, fetching...`
- );
+ // Check and update ingredients and beer styles cache
+ const cacheStats = await StaticDataService.getCacheStats();
+ if (!cacheStats.ingredients.cached) {
await StaticDataService.getIngredients(); // This will cache automatically
} else {
- console.log(
- `[StartupHydrationService] Ingredients already cached (${ingredientsStats.ingredients.record_count} items)`
- );
// Check for updates in background
StaticDataService.updateIngredientsCache().catch(error => {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-hydration",
`[StartupHydrationService] Background ingredients update failed:`,
error
);
@@ -118,27 +116,21 @@ export class StartupHydrationService {
}
// Check and update beer styles cache
- if (!ingredientsStats.beerStyles.cached) {
- console.log(
- `[StartupHydrationService] No cached beer styles found, fetching...`
- );
+ if (!cacheStats.beerStyles.cached) {
await StaticDataService.getBeerStyles(); // This will cache automatically
} else {
- console.log(
- `[StartupHydrationService] Beer styles already cached (${ingredientsStats.beerStyles.record_count} items)`
- );
// Check for updates in background
StaticDataService.updateBeerStylesCache().catch(error => {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-hydration",
`[StartupHydrationService] Background beer styles update failed:`,
error
);
});
}
-
- console.log(`[StartupHydrationService] Static data hydration completed`);
} catch (error) {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-hydration",
`[StartupHydrationService] Static data hydration failed:`,
error
);
@@ -151,7 +143,6 @@ export class StartupHydrationService {
static resetHydrationState(): void {
this.isHydrating = false;
this.hasHydrated = false;
- console.log(`[StartupHydrationService] Hydration state reset`);
}
/**
diff --git a/src/services/offlineV2/StaticDataService.ts b/src/services/offlineV2/StaticDataService.ts
index d8daad06..b621fb7b 100644
--- a/src/services/offlineV2/StaticDataService.ts
+++ b/src/services/offlineV2/StaticDataService.ts
@@ -14,6 +14,7 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import ApiService from "@services/api/apiService";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
import {
CachedStaticData,
StaticDataCacheStats,
@@ -72,7 +73,11 @@ export class StaticDataService {
const freshData = await this.fetchAndCacheIngredients();
return this.applyIngredientFilters(freshData, filters);
} catch (error) {
- console.error("Error getting ingredients:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Error getting ingredients:",
+ error
+ );
throw new OfflineError(
"Failed to get ingredients data",
"INGREDIENTS_ERROR",
@@ -106,7 +111,11 @@ export class StaticDataService {
const freshData = await this.fetchAndCacheBeerStyles();
return this.applyBeerStyleFilters(freshData, filters);
} catch (error) {
- console.error("Error getting beer styles:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Error getting beer styles:",
+ error
+ );
throw new OfflineError(
"Failed to get beer styles data",
"BEER_STYLES_ERROR",
@@ -147,7 +156,11 @@ export class StaticDataService {
String(cachedBeerStyles) !== String(beerStylesVersion.version),
};
} catch (error) {
- console.warn("Failed to check for updates:", error);
+ await UnifiedLogger.warn(
+ "offline-static",
+ "Failed to check for updates:",
+ error
+ );
// Return false for both if check fails
return { ingredients: false, beerStyles: false };
}
@@ -160,7 +173,11 @@ export class StaticDataService {
try {
await this.fetchAndCacheIngredients();
} catch (error) {
- console.error("Failed to update ingredients cache:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Failed to update ingredients cache:",
+ error
+ );
throw new VersionError(
"Failed to update ingredients cache",
"ingredients"
@@ -175,7 +192,11 @@ export class StaticDataService {
try {
await this.fetchAndCacheBeerStyles();
} catch (error) {
- console.error("Failed to update beer styles cache:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Failed to update beer styles cache:",
+ error
+ );
throw new VersionError(
"Failed to update beer styles cache",
"beer_styles"
@@ -192,7 +213,8 @@ export class StaticDataService {
try {
await this.fetchAndCacheIngredients();
} catch (error) {
- console.warn(
+ await UnifiedLogger.warn(
+ "offline-static",
"Failed to refresh ingredients after authentication:",
error
);
@@ -216,7 +238,11 @@ export class StaticDataService {
AsyncStorage.removeItem(STORAGE_KEYS_V2.BEER_STYLES_VERSION),
]);
} catch (error) {
- console.error("Failed to clear cache:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Failed to clear cache:",
+ error
+ );
throw new OfflineError(
"Failed to clear cache",
"CACHE_CLEAR_ERROR",
@@ -264,7 +290,11 @@ export class StaticDataService {
},
};
} catch (error) {
- console.error("Failed to get cache stats:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Failed to get cache stats:",
+ error
+ );
// Return empty stats on error
return {
ingredients: {
@@ -304,7 +334,8 @@ export class StaticDataService {
// In this case, we can't fetch ingredients, so return empty array
// but still cache the version info for when user logs in
if (authError?.status === 401 || authError?.status === 403) {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-static",
"Cannot fetch ingredients: user not authenticated. Ingredients will be available after login."
);
@@ -356,7 +387,11 @@ export class StaticDataService {
return ingredients;
} catch (error) {
- console.error("Failed to fetch ingredients:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Failed to fetch ingredients:",
+ error
+ );
throw new OfflineError(
"Failed to fetch ingredients",
"FETCH_ERROR",
@@ -371,7 +406,8 @@ export class StaticDataService {
private static async fetchAndCacheBeerStyles(): Promise {
try {
if (__DEV__) {
- console.log(
+ void UnifiedLogger.debug(
+ "offline-static",
`[StaticDataService.fetchAndCacheBeerStyles] Starting fetch...`
);
}
@@ -383,7 +419,8 @@ export class StaticDataService {
]);
if (__DEV__) {
- console.log(
+ void UnifiedLogger.debug(
+ "offline-static",
`[StaticDataService.fetchAndCacheBeerStyles] API responses received - version: ${versionResponse?.data?.version}`
);
}
@@ -395,9 +432,12 @@ export class StaticDataService {
if (Array.isArray(beerStylesData)) {
// If it's already an array, process normally
- console.log(
- `[StaticDataService.fetchAndCacheBeerStyles] Processing array format with ${beerStylesData.length} items`
- );
+ if (__DEV__) {
+ void UnifiedLogger.debug(
+ "offline-static",
+ `[StaticDataService.fetchAndCacheBeerStyles] Processing array format with ${beerStylesData.length} items`
+ );
+ }
beerStylesData.forEach((item: any) => {
if (Array.isArray(item?.styles)) {
// Item is a category with styles array
@@ -416,7 +456,8 @@ export class StaticDataService {
) {
// If it's an object with numeric keys (like "1", "2", etc.), convert to array
if (__DEV__) {
- console.log(
+ void UnifiedLogger.debug(
+ "offline-static",
`[StaticDataService.fetchAndCacheBeerStyles] Processing object format with keys: ${Object.keys(beerStylesData).length}`
);
}
@@ -432,10 +473,10 @@ export class StaticDataService {
}
});
} else {
- console.error(
+ await UnifiedLogger.error(
+ "offline-static",
`[StaticDataService.fetchAndCacheBeerStyles] Unexpected data format:`,
- typeof beerStylesData,
- beerStylesData
+ { type: typeof beerStylesData, data: beerStylesData }
);
throw new OfflineError(
"Beer styles data is not in expected format",
@@ -459,7 +500,11 @@ export class StaticDataService {
return allStyles;
} catch (error) {
- console.error("Failed to fetch beer styles:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Failed to fetch beer styles:",
+ error
+ );
throw new OfflineError(
"Failed to fetch beer styles",
"FETCH_ERROR",
@@ -478,7 +523,11 @@ export class StaticDataService {
);
return cached ? JSON.parse(cached) : null;
} catch (error) {
- console.error("Failed to get cached ingredients:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Failed to get cached ingredients:",
+ error
+ );
return null;
}
}
@@ -493,7 +542,11 @@ export class StaticDataService {
);
return cached ? JSON.parse(cached) : null;
} catch (error) {
- console.error("Failed to get cached beer styles:", error);
+ await UnifiedLogger.error(
+ "offline-static",
+ "Failed to get cached beer styles:",
+ error
+ );
return null;
}
}
@@ -512,7 +565,11 @@ export class StaticDataService {
return await AsyncStorage.getItem(key);
} catch (error) {
- console.error(`Failed to get cached version for ${dataType}:`, error);
+ await UnifiedLogger.error(
+ "offline-static",
+ `Failed to get cached version for ${dataType}:`,
+ error
+ );
return null;
}
}
@@ -559,7 +616,8 @@ export class StaticDataService {
dataType === "ingredients" &&
(error?.status === 401 || error?.status === 403)
) {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-static",
"Background ingredients update failed: authentication required"
);
} else {
@@ -569,7 +627,11 @@ export class StaticDataService {
}
} catch (error) {
// Silent fail for background checks
- console.warn(`Background version check failed for ${dataType}:`, error);
+ void UnifiedLogger.warn(
+ "offline-static",
+ `Background version check failed for ${dataType}:`,
+ error
+ );
} finally {
this.versionCheckInProgress[dataType] = false;
}
diff --git a/src/services/offlineV2/UserCacheService.ts b/src/services/offlineV2/UserCacheService.ts
index 8fc3d9fa..73531740 100644
--- a/src/services/offlineV2/UserCacheService.ts
+++ b/src/services/offlineV2/UserCacheService.ts
@@ -14,7 +14,7 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { UserValidationService } from "@utils/userValidation";
-import UnifiedLogger from "@services/logger/UnifiedLogger";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
import { isTempId } from "@utils/recipeUtils";
import {
TempIdMapping,
@@ -26,6 +26,7 @@ import {
STORAGE_KEYS_V2,
Recipe,
BrewSession,
+ UnitSystem,
} from "@src/types";
// Simple per-key queue (no external deps) with race condition protection
@@ -44,7 +45,8 @@ async function withKeyQueue(key: string, fn: () => Promise): Promise {
// Log warning if too many concurrent calls for the same key
if (currentCount > 10) {
- console.warn(
+ await UnifiedLogger.warn(
+ "offline-cache",
`[withKeyQueue] High concurrent call count for key "${key}": ${currentCount} calls`
);
}
@@ -52,7 +54,8 @@ async function withKeyQueue(key: string, fn: () => Promise): Promise {
// Break infinite loops by limiting max concurrent calls per key
if (currentCount > 50) {
queueDebugCounters.set(key, 0); // Reset counter
- console.error(
+ await UnifiedLogger.error(
+ "offline-cache",
`[withKeyQueue] Breaking potential infinite loop for key "${key}" after ${currentCount} calls`
);
// Execute directly to break the loop
@@ -109,7 +112,8 @@ export class UserCacheService {
try {
// Require userId for security - prevent cross-user data access
if (!userId) {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService.getRecipeById] User ID is required for security`
);
return null;
@@ -134,7 +138,11 @@ export class UserCacheService {
return recipeItem.data;
} catch (error) {
- console.error(`[UserCacheService.getRecipeById] Error:`, error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ `[UserCacheService.getRecipeById] Error:`,
+ error
+ );
return null;
}
}
@@ -171,7 +179,8 @@ export class UserCacheService {
isDeleted: !!recipeItem.isDeleted,
};
} catch (error) {
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService.getRecipeByIdIncludingDeleted] Error:`,
error
);
@@ -184,71 +193,22 @@ export class UserCacheService {
*/
static async getRecipes(
userId: string,
- userUnitSystem: "imperial" | "metric" = "imperial"
+ userUnitSystem: UnitSystem = "metric"
): Promise {
try {
- await UnifiedLogger.debug(
- "UserCacheService.getRecipes",
- `Retrieving recipes for user ${userId}`,
- {
- userId,
- unitSystem: userUnitSystem,
- }
- );
-
- console.log(
- `[UserCacheService.getRecipes] Getting recipes for user ID: "${userId}"`
- );
-
const cached = await this.getCachedRecipes(userId);
- // Log detailed info about what we found in cache
- const deletedCount = cached.filter(item => item.isDeleted).length;
- const pendingSyncCount = cached.filter(item => item.needsSync).length;
- const deletedPendingSyncCount = cached.filter(
- item => item.isDeleted && item.needsSync
- ).length;
-
- await UnifiedLogger.debug(
- "UserCacheService.getRecipes",
- `Cache contents analysis`,
- {
- userId,
- totalItems: cached.length,
- deletedItems: deletedCount,
- pendingSyncItems: pendingSyncCount,
- deletedPendingSyncItems: deletedPendingSyncCount,
- itemDetails: cached.map(item => ({
- id: item.id,
- dataId: item.data.id,
- name: item.data.name,
- isDeleted: item.isDeleted || false,
- needsSync: item.needsSync,
- syncStatus: item.syncStatus,
- })),
- }
- );
-
- console.log(
- `[UserCacheService.getRecipes] getCachedRecipes returned ${cached.length} items for user "${userId}"`
- );
-
// If no cached recipes found, try to hydrate from server
if (cached.length === 0) {
- console.log(
- `[UserCacheService.getRecipes] No cached recipes found, attempting to hydrate from server...`
- );
try {
await this.hydrateRecipesFromServer(userId, false, userUnitSystem);
// Try again after hydration
const hydratedCached = await this.getCachedRecipes(userId);
- console.log(
- `[UserCacheService.getRecipes] After hydration: ${hydratedCached.length} recipes cached`
- );
return this.filterAndSortHydrated(hydratedCached);
} catch (hydrationError) {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService.getRecipes] Failed to hydrate from server:`,
hydrationError
);
@@ -258,34 +218,9 @@ export class UserCacheService {
// Filter out deleted items and return data
const filteredRecipes = cached.filter(item => !item.isDeleted);
- console.log(
- `[UserCacheService.getRecipes] After filtering out deleted: ${filteredRecipes.length} recipes`
- );
-
- if (filteredRecipes.length > 0) {
- const recipeIds = filteredRecipes.map(item => item.data.id);
- console.log(
- `[UserCacheService.getRecipes] Recipe IDs: [${recipeIds.join(", ")}]`
- );
- }
const finalRecipes = this.filterAndSortHydrated(filteredRecipes);
- await UnifiedLogger.debug(
- "UserCacheService.getRecipes",
- `Returning filtered recipes to UI`,
- {
- userId,
- returnedCount: finalRecipes.length,
- filteredOutCount: filteredRecipes.length - finalRecipes.length,
- returnedRecipes: finalRecipes.map(recipe => ({
- id: recipe.id,
- name: recipe.name,
- style: recipe.style || "Unknown",
- })),
- }
- );
-
return finalRecipes;
} catch (error) {
await UnifiedLogger.error(
@@ -296,7 +231,6 @@ export class UserCacheService {
error: error instanceof Error ? error.message : "Unknown error",
}
);
- console.error("Error getting recipes:", error);
throw new OfflineError("Failed to get recipes", "RECIPES_ERROR", true);
}
}
@@ -321,21 +255,6 @@ export class UserCacheService {
);
}
- await UnifiedLogger.info(
- "UserCacheService.createRecipe",
- `Starting recipe creation for user ${currentUserId}`,
- {
- userId: currentUserId,
- tempId,
- recipeName: recipe.name || "Untitled",
- recipeStyle: recipe.style || "Unknown",
- hasIngredients: !!(
- recipe.ingredients && recipe.ingredients.length > 0
- ),
- ingredientCount: recipe.ingredients?.length || 0,
- timestamp: new Date().toISOString(),
- }
- );
const newRecipe: Recipe = {
...recipe,
id: tempId,
@@ -400,7 +319,11 @@ export class UserCacheService {
return newRecipe;
} catch (error) {
- console.error("Error creating recipe:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error creating recipe:",
+ error
+ );
throw new OfflineError("Failed to create recipe", "CREATE_ERROR", true);
}
}
@@ -501,7 +424,11 @@ export class UserCacheService {
return updatedRecipe;
} catch (error) {
- console.error("Error updating recipe:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error updating recipe:",
+ error
+ );
if (error instanceof OfflineError) {
throw error;
}
@@ -550,17 +477,6 @@ export class UserCacheService {
).length,
}
);
- console.log(
- `[UserCacheService.deleteRecipe] Recipe not found. Looking for ID: "${id}"`
- );
- console.log(
- `[UserCacheService.deleteRecipe] Available recipe IDs:`,
- cached.map(item => ({
- id: item.id,
- dataId: item.data.id,
- tempId: item.tempId,
- }))
- );
throw new OfflineError("Recipe not found", "NOT_FOUND", false);
}
@@ -681,7 +597,11 @@ export class UserCacheService {
// Trigger background sync
this.backgroundSync();
} catch (error) {
- console.error("Error deleting recipe:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error deleting recipe:",
+ error
+ );
if (error instanceof OfflineError) {
throw error;
}
@@ -760,7 +680,7 @@ export class UserCacheService {
return clonedRecipe;
} catch (error) {
- console.error("Error cloning recipe:", error);
+ void UnifiedLogger.error("offline-cache", "Error cloning recipe:", error);
if (error instanceof OfflineError) {
throw error;
}
@@ -788,7 +708,8 @@ export class UserCacheService {
try {
// Require userId for security - prevent cross-user data access
if (!userId) {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService.getBrewSessionById] User ID is required for security`
);
return null;
@@ -840,7 +761,11 @@ export class UserCacheService {
return sessionItem.data;
} catch (error) {
- console.error(`[UserCacheService.getBrewSessionById] Error:`, error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ `[UserCacheService.getBrewSessionById] Error:`,
+ error
+ );
return null;
}
}
@@ -850,60 +775,13 @@ export class UserCacheService {
*/
static async getBrewSessions(
userId: string,
- userUnitSystem: "imperial" | "metric" = "imperial"
+ userUnitSystem: UnitSystem = "metric"
): Promise {
try {
- await UnifiedLogger.debug(
- "UserCacheService.getBrewSessions",
- `Retrieving brew sessions for user ${userId}`,
- {
- userId,
- unitSystem: userUnitSystem,
- }
- );
-
- console.log(
- `[UserCacheService.getBrewSessions] Getting brew sessions for user ID: "${userId}"`
- );
-
const cached = await this.getCachedBrewSessions(userId);
- // Log detailed info about what we found in cache
- const deletedCount = cached.filter(item => item.isDeleted).length;
- const pendingSyncCount = cached.filter(item => item.needsSync).length;
- const deletedPendingSyncCount = cached.filter(
- item => item.isDeleted && item.needsSync
- ).length;
-
- await UnifiedLogger.debug(
- "UserCacheService.getBrewSessions",
- `Cache contents analysis`,
- {
- userId,
- totalItems: cached.length,
- deletedItems: deletedCount,
- pendingSyncItems: pendingSyncCount,
- deletedPendingSyncItems: deletedPendingSyncCount,
- itemDetails: cached.map(item => ({
- id: item.id,
- dataId: item.data.id,
- name: item.data.name,
- isDeleted: item.isDeleted || false,
- needsSync: item.needsSync,
- syncStatus: item.syncStatus,
- })),
- }
- );
-
- console.log(
- `[UserCacheService.getBrewSessions] getCachedBrewSessions returned ${cached.length} items for user "${userId}"`
- );
-
// If no cached sessions found, try to hydrate from server
if (cached.length === 0) {
- console.log(
- `[UserCacheService.getBrewSessions] No cached sessions found, attempting to hydrate from server...`
- );
try {
await this.hydrateBrewSessionsFromServer(
userId,
@@ -912,13 +790,11 @@ export class UserCacheService {
);
// Try again after hydration
const hydratedCached = await this.getCachedBrewSessions(userId);
- console.log(
- `[UserCacheService.getBrewSessions] After hydration: ${hydratedCached.length} sessions cached`
- );
return this.filterAndSortHydrated(hydratedCached);
} catch (hydrationError) {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService.getBrewSessions] Failed to hydrate from server:`,
hydrationError
);
@@ -928,34 +804,9 @@ export class UserCacheService {
// Filter out deleted items and return data
const filteredSessions = cached.filter(item => !item.isDeleted);
- console.log(
- `[UserCacheService.getBrewSessions] After filtering out deleted: ${filteredSessions.length} sessions`
- );
-
- if (filteredSessions.length > 0) {
- const sessionIds = filteredSessions.map(item => item.data.id);
- console.log(
- `[UserCacheService.getBrewSessions] Session IDs: [${sessionIds.join(", ")}]`
- );
- }
const finalSessions = this.filterAndSortHydrated(filteredSessions);
- await UnifiedLogger.debug(
- "UserCacheService.getBrewSessions",
- `Returning filtered sessions to UI`,
- {
- userId,
- returnedCount: finalSessions.length,
- filteredOutCount: filteredSessions.length - finalSessions.length,
- returnedSessions: finalSessions.map(session => ({
- id: session.id,
- name: session.name,
- status: session.status || "Unknown",
- })),
- }
- );
-
return finalSessions;
} catch (error) {
await UnifiedLogger.error(
@@ -966,7 +817,6 @@ export class UserCacheService {
error: error instanceof Error ? error.message : "Unknown error",
}
);
- console.error("Error getting brew sessions:", error);
throw new OfflineError(
"Failed to get brew sessions",
"SESSIONS_ERROR",
@@ -1080,7 +930,11 @@ export class UserCacheService {
return newSession;
} catch (error) {
- console.error("Error creating brew session:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error creating brew session:",
+ error
+ );
throw new OfflineError(
"Failed to create brew session",
"CREATE_ERROR",
@@ -1185,7 +1039,11 @@ export class UserCacheService {
return updatedSession;
} catch (error) {
- console.error("Error updating brew session:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error updating brew session:",
+ error
+ );
if (error instanceof OfflineError) {
throw error;
}
@@ -1355,7 +1213,11 @@ export class UserCacheService {
// Trigger background sync
this.backgroundSync();
} catch (error) {
- console.error("Error deleting brew session:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error deleting brew session:",
+ error
+ );
if (error instanceof OfflineError) {
throw error;
}
@@ -1461,7 +1323,11 @@ export class UserCacheService {
return updatedSessionData;
} catch (error) {
- console.error("Error adding fermentation entry:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error adding fermentation entry:",
+ error
+ );
throw new OfflineError(
"Failed to add fermentation entry",
"CREATE_ERROR",
@@ -1565,7 +1431,11 @@ export class UserCacheService {
return updatedSessionData;
} catch (error) {
- console.error("Error updating fermentation entry:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error updating fermentation entry:",
+ error
+ );
throw new OfflineError(
"Failed to update fermentation entry",
"UPDATE_ERROR",
@@ -1661,7 +1531,11 @@ export class UserCacheService {
return updatedSessionData;
} catch (error) {
- console.error("Error deleting fermentation entry:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error deleting fermentation entry:",
+ error
+ );
throw new OfflineError(
"Failed to delete fermentation entry",
"DELETE_ERROR",
@@ -1713,16 +1587,6 @@ export class UserCacheService {
// IMPORTANT: Use session.id (real ID) not sessionId parameter (could be temp ID)
const realSessionId = session.id;
- await UnifiedLogger.debug(
- "UserCacheService.addDryHopFromRecipe",
- `Using session ID for operation`,
- {
- paramSessionId: sessionId,
- realSessionId,
- sessionName: session.name,
- }
- );
-
// Create new dry-hop addition with automatic timestamp
const newDryHop: import("@src/types").DryHopAddition = {
addition_date:
@@ -1736,16 +1600,6 @@ export class UserCacheService {
recipe_instance_id: dryHopData.recipe_instance_id, // CRITICAL: Preserve instance ID for uniqueness
};
- await UnifiedLogger.debug(
- "UserCacheService.addDryHopFromRecipe",
- `Created dry-hop addition object`,
- {
- newDryHop,
- hasInstanceId: !!newDryHop.recipe_instance_id,
- instanceId: newDryHop.recipe_instance_id,
- }
- );
-
// Update dry-hop additions array locally (offline-first)
const updatedDryHops = [...(session.dry_hop_additions || []), newDryHop];
@@ -1797,7 +1651,7 @@ export class UserCacheService {
return updatedSessionData;
} catch (error) {
- console.error("Error adding dry-hop:", error);
+ void UnifiedLogger.error("offline-cache", "Error adding dry-hop:", error);
throw new OfflineError("Failed to add dry-hop", "CREATE_ERROR", true);
}
}
@@ -1892,7 +1746,11 @@ export class UserCacheService {
return updatedSessionData;
} catch (error) {
- console.error("Error removing dry-hop:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error removing dry-hop:",
+ error
+ );
throw new OfflineError("Failed to remove dry-hop", "UPDATE_ERROR", true);
}
}
@@ -1978,7 +1836,11 @@ export class UserCacheService {
return updatedSession;
} catch (error) {
- console.error("Error deleting dry-hop:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error deleting dry-hop:",
+ error
+ );
throw new OfflineError("Failed to delete dry-hop", "DELETE_ERROR", true);
}
}
@@ -2003,7 +1865,7 @@ export class UserCacheService {
if (this.syncInProgress && this.syncStartTime) {
const elapsed = Date.now() - this.syncStartTime;
if (elapsed > this.SYNC_TIMEOUT_MS) {
- console.warn("Resetting stuck sync flag");
+ void UnifiedLogger.warn("offline-cache", "Resetting stuck sync flag");
this.syncInProgress = false;
this.syncStartTime = undefined;
}
@@ -2031,9 +1893,6 @@ export class UserCacheService {
try {
let operations = await this.getPendingOperations();
if (operations.length > 0) {
- console.log(
- `[UserCacheService] Starting sync of ${operations.length} pending operations`
- );
}
// Process operations one at a time, reloading after each to catch any updates
@@ -2071,13 +1930,6 @@ export class UserCacheService {
// CRITICAL: Reload operations after ID mapping to get updated recipe_id references
// This ensures subsequent operations (like brew sessions) use the new real IDs
operations = await this.getPendingOperations();
- await UnifiedLogger.debug(
- "UserCacheService.syncPendingOperations",
- `Reloaded pending operations after ID mapping`,
- {
- remainingOperations: operations.length,
- }
- );
continue; // Skip to next iteration with fresh operations list
} else if (operation.type === "update") {
// For update operations, mark the item as synced
@@ -2093,7 +1945,8 @@ export class UserCacheService {
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService] Failed to process operation ${operation.id}:`,
errorMessage
);
@@ -2121,17 +1974,6 @@ export class UserCacheService {
if (isOfflineError) {
// Offline error - don't increment retry count, just stop syncing
// We'll try again next time (when network is back or next background sync)
- await UnifiedLogger.debug(
- "UserCacheService.syncPendingOperations",
- `Stopping sync due to offline error - will retry later`,
- {
- operationId: operation.id,
- operationType: operation.type,
- entityType: operation.entityType,
- error: errorMessage,
- remainingOperations: operations.length,
- }
- );
// Break out of the while loop - don't keep retrying offline operations
break;
} else {
@@ -2186,17 +2028,13 @@ export class UserCacheService {
`Sync failed: ${errorMessage}`,
{ error: errorMessage }
);
- console.error("Sync failed:", errorMessage);
+ void UnifiedLogger.error("offline-cache", "Sync failed:", errorMessage);
result.success = false;
result.errors.push(`Sync process failed: ${errorMessage}`);
return result;
} finally {
this.syncInProgress = false;
this.syncStartTime = undefined;
- await UnifiedLogger.debug(
- "UserCacheService.syncPendingOperations",
- "Sync process completed, flags reset"
- );
}
}
@@ -2215,7 +2053,11 @@ export class UserCacheService {
const operations = await this.getPendingOperations();
return operations.length;
} catch (error) {
- console.error("Error getting pending operations count:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error getting pending operations count:",
+ error
+ );
return 0;
}
}
@@ -2227,7 +2069,11 @@ export class UserCacheService {
try {
await AsyncStorage.removeItem(STORAGE_KEYS_V2.PENDING_OPERATIONS);
} catch (error) {
- console.error("Error clearing sync queue:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error clearing sync queue:",
+ error
+ );
throw new OfflineError("Failed to clear sync queue", "CLEAR_ERROR", true);
}
}
@@ -2280,7 +2126,11 @@ export class UserCacheService {
`Failed to reset retry counts: ${errorMessage}`,
{ error: errorMessage }
);
- console.error("Error resetting retry counts:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error resetting retry counts:",
+ error
+ );
return 0;
}
});
@@ -2312,18 +2162,13 @@ export class UserCacheService {
return hasTempId && (!hasNeedSync || !hasPendingOp);
});
- console.log(
- `[UserCacheService] Found ${stuckRecipes.length} stuck recipes with temp IDs`
- );
- stuckRecipes.forEach(recipe => {
- console.log(
- `[UserCacheService] Stuck recipe: ID="${recipe.id}", tempId="${recipe.tempId}", needsSync="${recipe.needsSync}", syncStatus="${recipe.syncStatus}"`
- );
- });
-
return { stuckRecipes, pendingOperations: pendingOps };
} catch (error) {
- console.error("[UserCacheService] Error finding stuck recipes:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "[UserCacheService] Error finding stuck recipes:",
+ error
+ );
return { stuckRecipes: [], pendingOperations: [] };
}
}
@@ -2446,7 +2291,11 @@ export class UserCacheService {
syncStatus,
};
} catch (error) {
- console.error("[UserCacheService] Error getting debug info:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "[UserCacheService] Error getting debug info:",
+ error
+ );
return { recipe: null, pendingOperations: [], syncStatus: "error" };
}
}
@@ -2458,8 +2307,6 @@ export class UserCacheService {
recipeId: string
): Promise<{ success: boolean; error?: string }> {
try {
- console.log(`[UserCacheService] Force syncing recipe: ${recipeId}`);
-
const debugInfo = await this.getRecipeDebugInfo(recipeId);
if (debugInfo.pendingOperations.length === 0) {
@@ -2478,7 +2325,8 @@ export class UserCacheService {
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService] Error force syncing recipe ${recipeId}:`,
errorMsg
);
@@ -2499,10 +2347,6 @@ export class UserCacheService {
for (const recipe of stuckRecipes) {
try {
- console.log(
- `[UserCacheService] Attempting to fix stuck recipe: ${recipe.id}`
- );
-
// Reset sync status and recreate pending operation
recipe.needsSync = true;
recipe.syncStatus = "pending";
@@ -2526,21 +2370,24 @@ export class UserCacheService {
// Add the pending operation
await this.addPendingOperation(operation);
- console.log(`[UserCacheService] Fixed stuck recipe: ${recipe.id}`);
fixed++;
} catch (error) {
const errorMsg = `Failed to fix recipe ${recipe.id}: ${error instanceof Error ? error.message : "Unknown error"}`;
- console.error(`[UserCacheService] ${errorMsg}`);
+ void UnifiedLogger.error(
+ "offline-cache",
+ `[UserCacheService] ${errorMsg}`
+ );
errors.push(errorMsg);
}
}
- console.log(
- `[UserCacheService] Fixed ${fixed} stuck recipes with ${errors.length} errors`
- );
return { fixed, errors };
} catch (error) {
- console.error("[UserCacheService] Error fixing stuck recipes:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "[UserCacheService] Error fixing stuck recipes:",
+ error
+ );
return {
fixed: 0,
errors: [
@@ -2555,41 +2402,30 @@ export class UserCacheService {
*/
static async refreshRecipesFromServer(
userId: string,
- userUnitSystem: "imperial" | "metric" = "imperial"
+ userUnitSystem: UnitSystem = "metric"
): Promise {
try {
- console.log(
- `[UserCacheService.refreshRecipesFromServer] Refreshing recipes from server for user: "${userId}"`
- );
-
// Always fetch fresh data from server
await this.hydrateRecipesFromServer(userId, true, userUnitSystem);
// Return fresh cached data
const refreshedRecipes = await this.getRecipes(userId, userUnitSystem);
- console.log(
- `[UserCacheService.refreshRecipesFromServer] Refresh completed, returning ${refreshedRecipes.length} recipes`
- );
return refreshedRecipes;
} catch (error) {
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService.refreshRecipesFromServer] Refresh failed:`,
error
);
// When refresh fails, try to return existing cached data instead of throwing
try {
- console.log(
- `[UserCacheService.refreshRecipesFromServer] Attempting to return cached data after refresh failure`
- );
const cachedRecipes = await this.getRecipes(userId, userUnitSystem);
- console.log(
- `[UserCacheService.refreshRecipesFromServer] Returning ${cachedRecipes.length} cached recipes after refresh failure`
- );
return cachedRecipes;
} catch (cacheError) {
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService.refreshRecipesFromServer] Failed to get cached data:`,
cacheError
);
@@ -2605,13 +2441,9 @@ export class UserCacheService {
private static async hydrateRecipesFromServer(
userId: string,
forceRefresh: boolean = false,
- userUnitSystem: "imperial" | "metric" = "imperial"
+ _userUnitSystem: UnitSystem = "metric"
): Promise {
try {
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Fetching recipes from server for user: "${userId}" (forceRefresh: ${forceRefresh})`
- );
-
// Import the API service here to avoid circular dependencies
const { default: ApiService } = await import("@services/api/apiService");
@@ -2624,10 +2456,6 @@ export class UserCacheService {
// If force refresh and we successfully got server data, clear and replace cache
if (forceRefresh && serverRecipes.length >= 0) {
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Force refresh successful - updating cache for user "${userId}"`
- );
-
// Get existing offline-created recipes to preserve before clearing
const cached = await AsyncStorage.getItem(STORAGE_KEYS_V2.USER_RECIPES);
if (cached) {
@@ -2655,10 +2483,6 @@ export class UserCacheService {
return false;
});
-
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Found ${offlineCreatedRecipes.length} V2 offline-created recipes to preserve`
- );
}
// MIGRATION: Also check for legacy offline recipes that need preservation
@@ -2670,21 +2494,6 @@ export class UserCacheService {
await LegacyMigrationService.getLegacyRecipeCount(userId);
if (legacyCount > 0) {
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Found ${legacyCount} legacy recipes - migrating to V2 before force refresh`
- );
-
- // Migrate legacy recipes to V2 before clearing cache
- const migrationResult =
- await LegacyMigrationService.migrateLegacyRecipesToV2(
- userId,
- userUnitSystem
- );
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Legacy migration result:`,
- migrationResult
- );
-
// Re-check for offline recipes after migration
const cachedAfterMigration = await AsyncStorage.getItem(
STORAGE_KEYS_V2.USER_RECIPES
@@ -2700,24 +2509,17 @@ export class UserCacheService {
item.tempId)
);
});
-
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] After migration: ${offlineCreatedRecipes.length} total offline recipes to preserve`
- );
}
}
} catch (migrationError) {
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService.hydrateRecipesFromServer] Legacy migration failed:`,
migrationError
);
// Continue with force refresh even if migration fails
}
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Found ${offlineCreatedRecipes.length} offline-created recipes to preserve`
- );
-
// Clear all recipes for this user
await this.clearUserRecipesFromCache(userId);
@@ -2725,16 +2527,8 @@ export class UserCacheService {
for (const recipe of offlineCreatedRecipes) {
await this.addRecipeToCache(recipe);
}
-
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Preserved ${offlineCreatedRecipes.length} offline-created recipes`
- );
}
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Fetched ${serverRecipes.length} recipes from server`
- );
-
// Only process and cache server recipes if we have them
if (serverRecipes.length > 0) {
// Filter out server recipes that are already preserved (to avoid duplicates)
@@ -2745,10 +2539,6 @@ export class UserCacheService {
recipe => !preservedIds.has(recipe.id)
);
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Filtered out ${serverRecipes.length - filteredServerRecipes.length} duplicate server recipes`
- );
-
// Convert server recipes to syncable items
const now = Date.now();
const syncableRecipes = filteredServerRecipes.map(recipe => ({
@@ -2763,18 +2553,12 @@ export class UserCacheService {
for (const recipe of syncableRecipes) {
await this.addRecipeToCache(recipe);
}
-
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] Successfully cached ${syncableRecipes.length} recipes`
- );
} else if (!forceRefresh) {
// Only log this for non-force refresh (normal hydration)
- console.log(
- `[UserCacheService.hydrateRecipesFromServer] No server recipes found`
- );
}
} catch (error) {
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService.hydrateRecipesFromServer] Failed to hydrate from server:`,
error
);
@@ -2791,20 +2575,20 @@ export class UserCacheService {
static async clearUserData(userId?: string): Promise {
try {
if (userId) {
- console.log(
- `[UserCacheService.clearUserData] Clearing data for user: "${userId}"`
- );
await this.clearUserRecipesFromCache(userId);
await this.clearUserBrewSessionsFromCache(userId);
await this.clearUserPendingOperations(userId);
} else {
- console.log(`[UserCacheService.clearUserData] Clearing all data`);
await AsyncStorage.removeItem(STORAGE_KEYS_V2.USER_RECIPES);
await AsyncStorage.removeItem(STORAGE_KEYS_V2.PENDING_OPERATIONS);
await AsyncStorage.removeItem(STORAGE_KEYS_V2.USER_BREW_SESSIONS);
}
} catch (error) {
- console.error(`[UserCacheService.clearUserData] Error:`, error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ `[UserCacheService.clearUserData] Error:`,
+ error
+ );
throw error;
}
}
@@ -2831,11 +2615,9 @@ export class UserCacheService {
STORAGE_KEYS_V2.PENDING_OPERATIONS,
JSON.stringify(filteredOperations)
);
- console.log(
- `[UserCacheService.clearUserPendingOperations] Cleared pending operations for user "${userId}"`
- );
} catch (error) {
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService.clearUserPendingOperations] Error:`,
error
);
@@ -2863,11 +2645,9 @@ export class UserCacheService {
STORAGE_KEYS_V2.USER_RECIPES,
JSON.stringify(filteredRecipes)
);
- console.log(
- `[UserCacheService.clearUserRecipesFromCache] Cleared recipes for user "${userId}", kept ${filteredRecipes.length} recipes for other users`
- );
} catch (error) {
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService.clearUserRecipesFromCache] Error:`,
error
);
@@ -2886,28 +2666,6 @@ export class UserCacheService {
private static filterAndSortHydrated<
T extends { updated_at?: string; created_at?: string },
>(hydratedCached: SyncableItem[]): T[] {
- const beforeFiltering = hydratedCached.length;
- const deletedItems = hydratedCached.filter(item => item.isDeleted);
-
- // Log what's being filtered out
- if (__DEV__ && deletedItems.length > 0) {
- void UnifiedLogger.debug(
- "UserCacheService.filterAndSortHydrated",
- `Filtering out ${deletedItems.length} deleted items`,
- {
- totalItems: beforeFiltering,
- deletedItems: deletedItems.length,
- deletedItemsDetails: deletedItems.map(item => ({
- id: item.id,
- name: (item.data as any)?.name || "Unknown",
- isDeleted: item.isDeleted,
- needsSync: item.needsSync,
- syncStatus: item.syncStatus,
- })),
- }
- );
- }
-
const filteredItems = hydratedCached
.filter(item => !item.isDeleted)
.map(item => item.data)
@@ -2928,18 +2686,6 @@ export class UserCacheService {
return bTime - aTime; // Newest first
});
- if (__DEV__) {
- void UnifiedLogger.debug(
- "UserCacheService.filterAndSortHydrated",
- `Filtering and sorting completed`,
- {
- beforeFiltering,
- afterFiltering: filteredItems.length,
- filteredOut: beforeFiltering - filteredItems.length,
- }
- );
- }
-
return filteredItems;
}
@@ -2951,49 +2697,23 @@ export class UserCacheService {
): Promise[]> {
return await withKeyQueue(STORAGE_KEYS_V2.USER_RECIPES, async () => {
try {
- console.log(
- `[UserCacheService.getCachedRecipes] Loading cache for user ID: "${userId}"`
- );
-
const cached = await AsyncStorage.getItem(STORAGE_KEYS_V2.USER_RECIPES);
if (!cached) {
- console.log(`[UserCacheService.getCachedRecipes] No cache found`);
return [];
}
const allRecipes: SyncableItem[] = JSON.parse(cached);
- console.log(
- `[UserCacheService.getCachedRecipes] Total cached recipes found: ${allRecipes.length}`
+ const userRecipes = allRecipes.filter(
+ item => item.data.user_id === userId
);
- // Log sample of all cached recipe user IDs for debugging
- if (allRecipes.length > 0) {
- const sampleUserIds = allRecipes.slice(0, 5).map(item => ({
- id: item.data.id,
- user_id: item.data.user_id,
- }));
- console.log(
- `[UserCacheService.getCachedRecipes] Sample cached recipes:`,
- sampleUserIds
- );
- }
-
- const userRecipes = allRecipes.filter(item => {
- const isMatch = item.data.user_id === userId;
- if (!isMatch) {
- console.log(
- `[UserCacheService.getCachedRecipes] Recipe ${item.data.id} user_id "${item.data.user_id}" != target "${userId}"`
- );
- }
- return isMatch;
- });
-
- console.log(
- `[UserCacheService.getCachedRecipes] Filtered to ${userRecipes.length} recipes for user "${userId}"`
- );
return userRecipes;
- } catch (e) {
- console.warn("Corrupt USER_RECIPES cache; resetting", e);
+ } catch (error) {
+ void UnifiedLogger.warn(
+ "offline-cache",
+ "Corrupt USER_RECIPES cache; resetting",
+ { error: error instanceof Error ? error.message : "Unknown error" }
+ );
await AsyncStorage.removeItem(STORAGE_KEYS_V2.USER_RECIPES);
return [];
}
@@ -3018,7 +2738,11 @@ export class UserCacheService {
JSON.stringify(recipes)
);
} catch (error) {
- console.error("Error adding recipe to cache:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error adding recipe to cache:",
+ error
+ );
throw new OfflineError("Failed to cache recipe", "CACHE_ERROR", true);
}
});
@@ -3053,7 +2777,11 @@ export class UserCacheService {
JSON.stringify(recipes)
);
} catch (error) {
- console.error("Error updating recipe in cache:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error updating recipe in cache:",
+ error
+ );
throw new OfflineError(
"Failed to update cached recipe",
"CACHE_ERROR",
@@ -3071,62 +2799,18 @@ export class UserCacheService {
): Promise[]> {
return await withKeyQueue(STORAGE_KEYS_V2.USER_BREW_SESSIONS, async () => {
try {
- // Add stack trace in dev mode to track what's calling this
- if (__DEV__) {
- const stack = new Error().stack;
- const caller = stack?.split("\n")[3]?.trim() || "unknown";
- console.log(`[getCachedBrewSessions] Called by: ${caller}`);
- }
-
- await UnifiedLogger.debug(
- "UserCacheService.getCachedBrewSessions",
- `Loading cache for user ID: "${userId}"`
- );
-
const cached = await AsyncStorage.getItem(
STORAGE_KEYS_V2.USER_BREW_SESSIONS
);
if (!cached) {
- await UnifiedLogger.debug(
- "UserCacheService.getCachedBrewSessions",
- "No cache found"
- );
return [];
}
const allSessions: SyncableItem[] = JSON.parse(cached);
- await UnifiedLogger.debug(
- "UserCacheService.getCachedBrewSessions",
- `Total cached sessions found: ${allSessions.length}`
+ const userSessions = allSessions.filter(
+ item => item.data.user_id === userId
);
- // Log sample of all cached session user IDs for debugging
- if (allSessions.length > 0) {
- const sampleUserIds = allSessions.slice(0, 5).map(item => ({
- id: item.data.id,
- user_id: item.data.user_id,
- }));
- await UnifiedLogger.debug(
- "UserCacheService.getCachedBrewSessions",
- "Sample cached sessions",
- { sampleUserIds }
- );
- }
-
- const userSessions = allSessions.filter(item => {
- const isMatch = item.data.user_id === userId;
- if (!isMatch) {
- console.log(
- `[UserCacheService.getCachedBrewSessions] Session ${item.data.id} user_id "${item.data.user_id}" != target "${userId}"`
- );
- }
- return isMatch;
- });
-
- await UnifiedLogger.debug(
- "UserCacheService.getCachedBrewSessions",
- `Filtered to ${userSessions.length} sessions for user "${userId}"`
- );
return userSessions;
} catch (e) {
await UnifiedLogger.error(
@@ -3148,17 +2832,6 @@ export class UserCacheService {
): Promise {
return await withKeyQueue(STORAGE_KEYS_V2.USER_BREW_SESSIONS, async () => {
try {
- await UnifiedLogger.debug(
- "UserCacheService.addBrewSessionToCache",
- `Adding session to cache`,
- {
- sessionId: item.id,
- sessionName: item.data.name,
- userId: item.data.user_id,
- syncStatus: item.syncStatus,
- }
- );
-
const cached = await AsyncStorage.getItem(
STORAGE_KEYS_V2.USER_BREW_SESSIONS
);
@@ -3172,12 +2845,6 @@ export class UserCacheService {
STORAGE_KEYS_V2.USER_BREW_SESSIONS,
JSON.stringify(sessions)
);
-
- await UnifiedLogger.debug(
- "UserCacheService.addBrewSessionToCache",
- `Successfully added session to cache`,
- { totalSessions: sessions.length }
- );
} catch (error) {
await UnifiedLogger.error(
"UserCacheService.addBrewSessionToCache",
@@ -3201,17 +2868,6 @@ export class UserCacheService {
): Promise {
return await withKeyQueue(STORAGE_KEYS_V2.USER_BREW_SESSIONS, async () => {
try {
- await UnifiedLogger.debug(
- "UserCacheService.updateBrewSessionInCache",
- `Updating session in cache`,
- {
- sessionId: updatedItem.id,
- sessionName: updatedItem.data.name,
- syncStatus: updatedItem.syncStatus,
- needsSync: updatedItem.needsSync,
- }
- );
-
const cached = await AsyncStorage.getItem(
STORAGE_KEYS_V2.USER_BREW_SESSIONS
);
@@ -3226,16 +2882,8 @@ export class UserCacheService {
if (index >= 0) {
sessions[index] = updatedItem;
- await UnifiedLogger.debug(
- "UserCacheService.updateBrewSessionInCache",
- `Updated existing session at index ${index}`
- );
} else {
sessions.push(updatedItem);
- await UnifiedLogger.debug(
- "UserCacheService.updateBrewSessionInCache",
- `Added new session to cache`
- );
}
await AsyncStorage.setItem(
@@ -3265,12 +2913,6 @@ export class UserCacheService {
): Promise {
return await withKeyQueue(STORAGE_KEYS_V2.USER_BREW_SESSIONS, async () => {
try {
- await UnifiedLogger.debug(
- "UserCacheService.removeBrewSessionFromCache",
- `Removing session from cache`,
- { entityId }
- );
-
const cached = await AsyncStorage.getItem(
STORAGE_KEYS_V2.USER_BREW_SESSIONS
);
@@ -3287,11 +2929,6 @@ export class UserCacheService {
STORAGE_KEYS_V2.USER_BREW_SESSIONS,
JSON.stringify(filteredSessions)
);
- await UnifiedLogger.debug(
- "UserCacheService.removeBrewSessionFromCache",
- `Successfully removed session from cache`,
- { removedCount: sessions.length - filteredSessions.length }
- );
} else {
await UnifiedLogger.warn(
"UserCacheService.removeBrewSessionFromCache",
@@ -3336,10 +2973,6 @@ export class UserCacheService {
STORAGE_KEYS_V2.USER_BREW_SESSIONS,
JSON.stringify(filteredSessions)
);
- await UnifiedLogger.debug(
- "UserCacheService.clearUserBrewSessionsFromCache",
- `Cleared sessions for user "${userId}", kept ${filteredSessions.length} sessions for other users`
- );
} catch (error) {
await UnifiedLogger.error(
"UserCacheService.clearUserBrewSessionsFromCache",
@@ -3360,14 +2993,9 @@ export class UserCacheService {
private static async hydrateBrewSessionsFromServer(
userId: string,
forceRefresh: boolean = false,
- _userUnitSystem: "imperial" | "metric" = "imperial"
+ _userUnitSystem: UnitSystem = "metric"
): Promise {
try {
- await UnifiedLogger.debug(
- "UserCacheService.hydrateBrewSessionsFromServer",
- `Fetching sessions from server for user: "${userId}" (forceRefresh: ${forceRefresh})`
- );
-
// Import the API service here to avoid circular dependencies
const { default: ApiService } = await import("@services/api/apiService");
@@ -3375,28 +3003,6 @@ export class UserCacheService {
const response = await ApiService.brewSessions.getAll(1, 100); // Get first 100 sessions
// Log RAW response before any processing
- await UnifiedLogger.debug(
- "UserCacheService.hydrateBrewSessionsFromServer",
- `RAW response from API (before any processing)`,
- {
- hasBrewSessions: "brew_sessions" in (response.data || {}),
- sessionCount: response.data?.brew_sessions?.length || 0,
- firstSessionDryHops:
- response.data?.brew_sessions?.[0]?.dry_hop_additions?.length || 0,
- rawFirstSession: response.data?.brew_sessions?.[0]
- ? {
- id: response.data.brew_sessions[0].id,
- name: response.data.brew_sessions[0].name,
- hasDryHopField:
- "dry_hop_additions" in response.data.brew_sessions[0],
- dryHopCount:
- response.data.brew_sessions[0].dry_hop_additions?.length || 0,
- dryHops:
- response.data.brew_sessions[0].dry_hop_additions || "MISSING",
- }
- : "NO_SESSIONS",
- }
- );
const serverSessions = response.data?.brew_sessions || [];
@@ -3407,11 +3013,6 @@ export class UserCacheService {
// If force refresh and we successfully got server data, clear and replace cache
if (forceRefresh && serverSessions.length >= 0) {
- await UnifiedLogger.debug(
- "UserCacheService.hydrateBrewSessionsFromServer",
- `Force refresh successful - updating cache for user "${userId}"`
- );
-
// Get existing offline-created sessions to preserve before clearing
const cached = await AsyncStorage.getItem(
STORAGE_KEYS_V2.USER_BREW_SESSIONS
@@ -3447,19 +3048,6 @@ export class UserCacheService {
// Don't preserve anything else during force refresh
return false;
});
-
- await UnifiedLogger.debug(
- "UserCacheService.hydrateBrewSessionsFromServer",
- `Found ${offlineCreatedSessions.length} V2 offline-created sessions (with tempId) to preserve`,
- {
- tempIdMappings: Array.from(tempIdToRealIdMap.entries()).map(
- ([tempId, realId]) => ({
- tempId,
- realId,
- })
- ),
- }
- );
}
// Clear all sessions for this user
@@ -3469,11 +3057,6 @@ export class UserCacheService {
for (const session of offlineCreatedSessions) {
await this.addBrewSessionToCache(session);
}
-
- await UnifiedLogger.debug(
- "UserCacheService.hydrateBrewSessionsFromServer",
- `Preserved ${offlineCreatedSessions.length} offline-created sessions`
- );
}
await UnifiedLogger.info(
@@ -3506,11 +3089,6 @@ export class UserCacheService {
session => !preservedIds.has(session.id)
);
- await UnifiedLogger.debug(
- "UserCacheService.hydrateBrewSessionsFromServer",
- `Filtered out ${serverSessions.length - filteredServerSessions.length} duplicate server sessions`
- );
-
// Convert server sessions to syncable items
const now = Date.now();
const syncableSessions = filteredServerSessions.map(session => {
@@ -3555,10 +3133,6 @@ export class UserCacheService {
);
} else if (!forceRefresh) {
// Only log this for non-force refresh (normal hydration)
- await UnifiedLogger.debug(
- "UserCacheService.hydrateBrewSessionsFromServer",
- `No server sessions found`
- );
}
} catch (error) {
await UnifiedLogger.error(
@@ -3578,7 +3152,7 @@ export class UserCacheService {
*/
static async refreshBrewSessionsFromServer(
userId: string,
- userUnitSystem: "imperial" | "metric" = "imperial"
+ userUnitSystem: UnitSystem = "metric"
): Promise {
try {
await UnifiedLogger.info(
@@ -3622,23 +3196,6 @@ export class UserCacheService {
): Partial {
const sanitized = { ...updates };
- // Debug logging to understand the data being sanitized
- if (__DEV__) {
- UnifiedLogger.debug(
- "UserCacheService.sanitizeBrewSessionUpdatesForAPI",
- "Sanitizing brew session data for API",
- {
- originalFields: Object.keys(updates),
- hasFermentationData: !!(
- sanitized.fermentation_data &&
- sanitized.fermentation_data.length > 0
- ),
- fermentationEntryCount: sanitized.fermentation_data?.length || 0,
- fermentationDataSample: sanitized.fermentation_data?.[0] || null,
- }
- );
- }
-
// Remove fields that shouldn't be updated via API
delete sanitized.id;
delete sanitized.created_at;
@@ -3676,21 +3233,133 @@ export class UserCacheService {
Math.floor(Number(sanitized.batch_rating)) || undefined;
}
- // Debug logging for sanitized result
- if (__DEV__) {
- UnifiedLogger.debug(
- "UserCacheService.sanitizeBrewSessionUpdatesForAPI",
- "Sanitization completed",
+ return sanitized;
+ }
+
+ /**
+ * Resolve temporary recipe_id in brew session data
+ *
+ * Validates that recipe_id exists and is a non-empty string before processing.
+ * If recipe_id is a temp ID, resolves it to the real MongoDB ID by looking up
+ * the synced recipe in the cache.
+ *
+ * @param brewSessionData - The brew session data that may contain a temp recipe_id
+ * @param operation - The pending operation being processed
+ * @param pathContext - Context indicating if this is a CREATE or UPDATE operation
+ * @returns Object containing updated brewSessionData (with recipe_id guaranteed to be a string) and flag indicating if temp ID was found
+ * @throws OfflineError if recipe_id is missing/invalid, userId is missing, or recipe not found
+ */
+ private static async resolveTempRecipeId<
+ TempBrewSessionData extends Partial,
+ >(
+ brewSessionData: TempBrewSessionData,
+ operation: PendingOperation,
+ pathContext: "CREATE" | "UPDATE"
+ ): Promise<{
+ brewSessionData: TempBrewSessionData & { recipe_id: string };
+ hadTempRecipeId: boolean;
+ }> {
+ const updatedData = { ...brewSessionData };
+
+ // Validate that recipe_id exists and is a non-empty string
+ if (
+ !updatedData.recipe_id ||
+ typeof updatedData.recipe_id !== "string" ||
+ updatedData.recipe_id.trim().length === 0
+ ) {
+ await UnifiedLogger.error(
+ "UserCacheService.resolveTempRecipeId",
+ `Brew session ${pathContext} has missing or invalid recipe_id`,
{
- sanitizedFields: Object.keys(sanitized),
- removedFields: Object.keys(updates).filter(
- key => !(key in sanitized)
- ),
+ entityId: operation.entityId,
+ recipeId: updatedData.recipe_id,
+ pathContext,
}
);
+ throw new OfflineError(
+ "Invalid brew session - missing or invalid recipe_id",
+ "DEPENDENCY_ERROR",
+ false
+ );
}
- return sanitized;
+ const hasTemporaryRecipeId = updatedData.recipe_id.startsWith("temp_");
+
+ if (!hasTemporaryRecipeId) {
+ // recipe_id exists, is valid, and is not a temp ID, so it's already a real ID
+ return {
+ brewSessionData: updatedData as TempBrewSessionData & {
+ recipe_id: string;
+ },
+ hadTempRecipeId: false,
+ };
+ }
+
+ // Validate operation has userId
+ if (!operation.userId) {
+ await UnifiedLogger.error(
+ "UserCacheService.resolveTempRecipeId",
+ `Cannot resolve temp recipe_id - operation has no userId`,
+ { entityId: operation.entityId, pathContext }
+ );
+ throw new OfflineError(
+ "Invalid operation - missing userId",
+ "DEPENDENCY_ERROR",
+ false
+ );
+ }
+
+ await UnifiedLogger.info(
+ "UserCacheService.resolveTempRecipeId",
+ `Brew session ${pathContext} has temporary recipe_id - looking up real ID`,
+ {
+ tempRecipeId: updatedData.recipe_id,
+ entityId: operation.entityId,
+ pathContext,
+ }
+ );
+
+ // Look up the real recipe ID from the recipe cache
+ const recipes = await this.getCachedRecipes(operation.userId);
+ const matchedRecipe = recipes.find(
+ r =>
+ r.data.id === updatedData.recipe_id ||
+ r.tempId === updatedData.recipe_id
+ );
+
+ if (matchedRecipe) {
+ const realRecipeId = matchedRecipe.data.id;
+ await UnifiedLogger.info(
+ "UserCacheService.resolveTempRecipeId",
+ `Resolved temp recipe_id to real ID${pathContext === "UPDATE" ? " (UPDATE path)" : ""}`,
+ {
+ tempRecipeId: updatedData.recipe_id,
+ realRecipeId: realRecipeId,
+ pathContext,
+ }
+ );
+ updatedData.recipe_id = realRecipeId;
+ } else {
+ // Recipe not found - this means the recipe hasn't synced yet
+ await UnifiedLogger.warn(
+ "UserCacheService.resolveTempRecipeId",
+ `Cannot find recipe for temp ID - skipping brew session ${pathContext} sync`,
+ {
+ tempRecipeId: updatedData.recipe_id,
+ entityId: operation.entityId,
+ pathContext,
+ }
+ );
+ throw new OfflineError("Recipe not synced yet", "DEPENDENCY_ERROR", true);
+ }
+
+ // At this point, recipe_id has been resolved to a real string ID
+ return {
+ brewSessionData: updatedData as TempBrewSessionData & {
+ recipe_id: string;
+ },
+ hadTempRecipeId: true,
+ };
}
/**
@@ -3703,7 +3372,11 @@ export class UserCacheService {
);
return cached ? JSON.parse(cached) : [];
} catch (error) {
- console.error("Error getting pending operations:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error getting pending operations:",
+ error
+ );
return [];
}
}
@@ -3726,7 +3399,11 @@ export class UserCacheService {
JSON.stringify(operations)
);
} catch (error) {
- console.error("Error adding pending operation:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error adding pending operation:",
+ error
+ );
throw new OfflineError(
"Failed to queue operation",
"QUEUE_ERROR",
@@ -3754,7 +3431,11 @@ export class UserCacheService {
JSON.stringify(filtered)
);
} catch (error) {
- console.error("Error removing pending operation:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error removing pending operation:",
+ error
+ );
}
});
}
@@ -3780,7 +3461,11 @@ export class UserCacheService {
);
}
} catch (error) {
- console.error("Error updating pending operation:", error);
+ void UnifiedLogger.error(
+ "offline-cache",
+ "Error updating pending operation:",
+ error
+ );
}
});
}
@@ -3876,19 +3561,28 @@ export class UserCacheService {
}
);
} else if (operation.entityType === "brew_session") {
+ // CRITICAL FIX: Check if brew session has temp recipe_id and resolve it
+ const { brewSessionData, hadTempRecipeId } =
+ await this.resolveTempRecipeId(
+ { ...operation.data },
+ operation,
+ "CREATE"
+ );
+
await UnifiedLogger.info(
"UserCacheService.processPendingOperation",
`Executing CREATE API call for brew session`,
{
entityId: operation.entityId,
operationId: operation.id,
- sessionName: operation.data?.name || "Unknown",
- brewSessionData: operation.data, // Log the actual data being sent
+ sessionName: brewSessionData?.name || "Unknown",
+ recipeId: brewSessionData.recipe_id,
+ hadTempRecipeId: hadTempRecipeId,
+ brewSessionData: brewSessionData, // Log the actual data being sent
}
);
- const response = await ApiService.brewSessions.create(
- operation.data
- );
+ const response =
+ await ApiService.brewSessions.create(brewSessionData);
if (response && response.data && response.data.id) {
await UnifiedLogger.info(
"UserCacheService.processPendingOperation",
@@ -3910,24 +3604,23 @@ export class UserCacheService {
if (isTempId) {
// Convert UPDATE with temp ID to CREATE operation
- if (__DEV__) {
- console.log(
- `[UserCacheService.syncOperation] Converting UPDATE with temp ID ${operation.entityId} to CREATE operation:`,
- JSON.stringify(operation.data, null, 2)
- );
- }
+
+ await UnifiedLogger.info(
+ "UserCacheService.processPendingOperation",
+ `Converting UPDATE with temp ID ${operation.entityId} to CREATE operation for recipe`,
+ {
+ entityId: operation.entityId,
+ recipeName: operation.data?.name || "Unknown",
+ operationId: operation.id,
+ }
+ );
+
const response = await ApiService.recipes.create(operation.data);
if (response && response.data && response.data.id) {
return { realId: response.data.id };
}
} else {
// Normal UPDATE operation for real MongoDB IDs
- if (__DEV__) {
- console.log(
- `[UserCacheService.syncOperation] Sending UPDATE data to API for recipe ${operation.entityId}:`,
- JSON.stringify(operation.data, null, 2)
- );
- }
await ApiService.recipes.update(
operation.entityId,
operation.data
@@ -4008,6 +3701,14 @@ export class UserCacheService {
// Check if this is a temp ID - if so, treat as CREATE instead of UPDATE
const isTempId = operation.entityId.startsWith("temp_");
+ // CRITICAL FIX: Check if brew session has temp recipe_id and resolve it (for both CREATE and UPDATE paths)
+ const { brewSessionData, hadTempRecipeId } =
+ await this.resolveTempRecipeId(
+ { ...operation.data },
+ operation,
+ "UPDATE"
+ );
+
if (isTempId) {
// Convert UPDATE with temp ID to CREATE operation
await UnifiedLogger.info(
@@ -4015,12 +3716,13 @@ export class UserCacheService {
`Converting UPDATE with temp ID ${operation.entityId} to CREATE operation for brew session`,
{
entityId: operation.entityId,
- sessionName: operation.data?.name || "Unknown",
+ sessionName: brewSessionData?.name || "Unknown",
+ recipeId: brewSessionData.recipe_id,
+ hadTempRecipeId: hadTempRecipeId,
}
);
- const response = await ApiService.brewSessions.create(
- operation.data
- );
+ const response =
+ await ApiService.brewSessions.create(brewSessionData);
if (response && response.data && response.data.id) {
return { realId: response.data.id };
}
@@ -4031,13 +3733,15 @@ export class UserCacheService {
`Executing UPDATE API call for brew session ${operation.entityId}`,
{
entityId: operation.entityId,
- updateFields: Object.keys(operation.data || {}),
- sessionName: operation.data?.name || "Unknown",
+ updateFields: Object.keys(brewSessionData || {}),
+ sessionName: brewSessionData?.name || "Unknown",
+ recipeId: brewSessionData.recipe_id,
+ hadTempRecipeId: hadTempRecipeId,
}
);
// Filter out embedded document arrays - they have dedicated sync operations
- const updateData = { ...operation.data };
+ const updateData = { ...brewSessionData };
// Remove fermentation_data - synced via fermentation_entry operations
if (updateData.fermentation_data) {
@@ -4167,7 +3871,8 @@ export class UserCacheService {
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
`[UserCacheService] Error processing ${operation.type} operation for ${operation.entityId}:`,
errorMessage
);
@@ -4205,16 +3910,6 @@ export class UserCacheService {
const jitter = Math.floor(base * 0.1 * Math.random());
const delay = base + jitter;
- await UnifiedLogger.debug(
- "UserCacheService.backgroundSync",
- `Background sync scheduled in ${delay}ms (backoff delay)`,
- {
- delay,
- maxRetry,
- exponential: exp,
- }
- );
-
// Don't wait for sync to complete
setTimeout(async () => {
try {
@@ -4235,7 +3930,11 @@ export class UserCacheService {
`Background sync failed: ${errorMessage}`,
{ error: errorMessage }
);
- console.warn("Background sync failed:", error);
+ void UnifiedLogger.warn(
+ "offline-cache",
+ "Background sync failed:",
+ error
+ );
}
}, delay);
} catch (error) {
@@ -4246,7 +3945,11 @@ export class UserCacheService {
`Failed to start background sync: ${errorMessage}`,
{ error: errorMessage }
);
- console.warn("Failed to start background sync:", error);
+ void UnifiedLogger.warn(
+ "offline-cache",
+ "Failed to start background sync:",
+ error
+ );
}
}
@@ -4291,7 +3994,8 @@ export class UserCacheService {
JSON.stringify(recipes)
);
} else {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService] Recipe with temp ID "${tempId}" not found in cache`
);
}
@@ -4322,17 +4026,9 @@ export class UserCacheService {
STORAGE_KEYS_V2.USER_BREW_SESSIONS,
JSON.stringify(sessions)
);
- await UnifiedLogger.debug(
- "UserCacheService.mapTempIdToRealId",
- `Mapped brew session temp ID to real ID`,
- {
- tempId,
- realId,
- sessionName: sessions[i].data.name,
- }
- );
} else {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService] Brew session with temp ID "${tempId}" not found in cache`
);
}
@@ -4359,16 +4055,6 @@ export class UserCacheService {
session.syncStatus = "pending";
session.lastModified = Date.now();
updatedCount++;
- await UnifiedLogger.debug(
- "UserCacheService.mapTempIdToRealId",
- `Updated recipe_id reference in brew session`,
- {
- sessionId: session.id,
- sessionName: session.data.name,
- oldRecipeId: tempId,
- newRecipeId: realId,
- }
- );
}
}
@@ -4395,29 +4081,22 @@ export class UserCacheService {
STORAGE_KEYS_V2.PENDING_OPERATIONS
);
const operations: PendingOperation[] = cached ? JSON.parse(cached) : [];
- let updated = false;
+ let updatedEntityIds = 0;
+ let updatedBrewSessionRecipeRefs = 0;
+ let updatedParentIds = 0;
+
for (const op of operations) {
// Update entityId if it matches the temp ID
if (op.entityId === tempId) {
op.entityId = realId;
- updated = true;
+ updatedEntityIds++;
}
// Update recipe_id in brew session operation data if it references the temp recipe ID
if (op.entityType === "brew_session" && op.data) {
const brewSessionData = op.data as Partial;
if (brewSessionData.recipe_id === tempId) {
brewSessionData.recipe_id = realId;
- updated = true;
- await UnifiedLogger.debug(
- "UserCacheService.mapTempIdToRealId",
- `Updated recipe_id in pending brew session operation`,
- {
- operationId: op.id,
- operationType: op.type,
- oldRecipeId: tempId,
- newRecipeId: realId,
- }
- );
+ updatedBrewSessionRecipeRefs++;
}
}
// Update parentId for fermentation_entry and dry_hop_addition operations
@@ -4427,25 +4106,29 @@ export class UserCacheService {
op.parentId === tempId
) {
op.parentId = realId;
- updated = true;
- await UnifiedLogger.debug(
- "UserCacheService.mapTempIdToRealId",
- `Updated parentId in pending ${op.entityType} operation`,
- {
- operationId: op.id,
- operationType: op.type,
- entityType: op.entityType,
- oldParentId: tempId,
- newParentId: realId,
- }
- );
+ updatedParentIds++;
}
}
- if (updated) {
+
+ const totalUpdated =
+ updatedEntityIds + updatedBrewSessionRecipeRefs + updatedParentIds;
+
+ if (totalUpdated > 0) {
await AsyncStorage.setItem(
STORAGE_KEYS_V2.PENDING_OPERATIONS,
JSON.stringify(operations)
);
+ await UnifiedLogger.info(
+ "UserCacheService.mapTempIdToRealId",
+ `Updated ${totalUpdated} pending operation(s) with new IDs`,
+ {
+ tempId,
+ realId,
+ updatedEntityIds,
+ updatedBrewSessionRecipeRefs,
+ updatedParentIds,
+ }
+ );
}
});
@@ -4468,7 +4151,8 @@ export class UserCacheService {
error: error instanceof Error ? error.message : "Unknown error",
}
);
- console.error(
+ void UnifiedLogger.error(
+ "offline-cache",
"[UserCacheService] Error mapping temp ID to real ID:",
error
);
@@ -4498,13 +4182,9 @@ export class UserCacheService {
STORAGE_KEYS_V2.USER_RECIPES,
JSON.stringify(recipes)
);
- await UnifiedLogger.debug(
- "UserCacheService.markItemAsSynced",
- `Marked recipe as synced`,
- { entityId, recipeName: recipes[i].data.name }
- );
} else {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService] Recipe with ID "${entityId}" not found in cache for marking as synced`
);
}
@@ -4530,13 +4210,9 @@ export class UserCacheService {
STORAGE_KEYS_V2.USER_BREW_SESSIONS,
JSON.stringify(sessions)
);
- await UnifiedLogger.debug(
- "UserCacheService.markItemAsSynced",
- `Marked brew session as synced`,
- { entityId, sessionName: sessions[i].data.name }
- );
} else {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService] Brew session with ID "${entityId}" not found in cache for marking as synced`
);
}
@@ -4574,13 +4250,9 @@ export class UserCacheService {
STORAGE_KEYS_V2.USER_RECIPES,
JSON.stringify(filteredRecipes)
);
- await UnifiedLogger.debug(
- "UserCacheService.removeItemFromCache",
- `Removed recipe from cache`,
- { entityId, removedCount: recipes.length - filteredRecipes.length }
- );
} else {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService] Recipe with ID "${entityId}" not found in cache for removal`
);
}
@@ -4604,16 +4276,9 @@ export class UserCacheService {
STORAGE_KEYS_V2.USER_BREW_SESSIONS,
JSON.stringify(filteredSessions)
);
- await UnifiedLogger.debug(
- "UserCacheService.removeItemFromCache",
- `Removed brew session from cache`,
- {
- entityId,
- removedCount: sessions.length - filteredSessions.length,
- }
- );
} else {
- console.warn(
+ void UnifiedLogger.warn(
+ "offline-cache",
`[UserCacheService] Brew session with ID "${entityId}" not found in cache for removal`
);
}
@@ -4639,24 +4304,6 @@ export class UserCacheService {
): Partial {
const sanitized = { ...updates };
- // Debug logging to understand the data being sanitized
- if (__DEV__ && sanitized.ingredients) {
- console.log(
- "[UserCacheService.sanitizeRecipeUpdatesForAPI] Original ingredients:",
- JSON.stringify(
- sanitized.ingredients.map(ing => ({
- name: ing.name,
- id: ing.id,
- id_type: typeof ing.id,
- ingredient_id: (ing as any).ingredient_id,
- ingredient_id_type: typeof (ing as any).ingredient_id,
- })),
- null,
- 2
- )
- );
- }
-
// Remove fields that shouldn't be updated via API
delete sanitized.id;
delete sanitized.created_at;
@@ -4773,14 +4420,6 @@ export class UserCacheService {
});
}
- // Debug logging to see the sanitized result
- if (__DEV__ && sanitized.ingredients) {
- console.log(
- "[UserCacheService.sanitizeRecipeUpdatesForAPI] Sanitized ingredients (FULL):",
- JSON.stringify(sanitized.ingredients, null, 2)
- );
- }
-
return sanitized;
}
@@ -4827,18 +4466,6 @@ export class UserCacheService {
STORAGE_KEYS_V2.TEMP_ID_MAPPINGS,
JSON.stringify(filtered)
);
-
- await UnifiedLogger.debug(
- "UserCacheService.saveTempIdMapping",
- `Saved tempId mapping for ${entityType}`,
- {
- tempId,
- realId,
- entityType,
- userId,
- expiresIn: `${ttl / (60 * 60 * 1000)} hours`,
- }
- );
});
} catch (error) {
await UnifiedLogger.error(
@@ -4884,17 +4511,6 @@ export class UserCacheService {
);
if (mapping) {
- await UnifiedLogger.debug(
- "UserCacheService.getRealIdFromTempId",
- `Found tempId mapping`,
- {
- tempId,
- realId: mapping.realId,
- entityType,
- userId,
- age: `${Math.round((now - mapping.timestamp) / 1000)}s`,
- }
- );
return mapping.realId;
}
diff --git a/src/services/storageService.ts b/src/services/storageService.ts
index 736a6df9..1d2d6cf9 100644
--- a/src/services/storageService.ts
+++ b/src/services/storageService.ts
@@ -18,6 +18,7 @@ import { File, Directory, Paths } from "expo-file-system";
import { Platform } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { STORAGE_KEYS } from "@services/config";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
/**
* Android API levels for permission handling
@@ -92,7 +93,11 @@ export class StorageService {
return status === "granted";
}
} catch (error) {
- console.error("Error requesting media permissions:", error);
+ UnifiedLogger.error(
+ "storage",
+ "Error requesting media permissions:",
+ error
+ );
return false;
}
}
@@ -343,7 +348,8 @@ export class BeerXMLService {
// If user cancelled directory selection, fall back to sharing
if (safResult.userCancelled) {
} else {
- console.warn(
+ UnifiedLogger.warn(
+ "storage",
"🍺 BeerXML Export - SAF failed, falling back to sharing:",
safResult.error
);
@@ -423,7 +429,11 @@ export class BeerXMLService {
uri: file.uri,
};
} catch (error) {
- console.error("🍺 BeerXML Export - Directory choice error:", error);
+ UnifiedLogger.error(
+ "storage",
+ "🍺 BeerXML Export - Directory choice error:",
+ error
+ );
return {
success: false,
error: error instanceof Error ? error.message : "File save failed",
@@ -446,7 +456,7 @@ export class OfflineStorageService {
try {
// Validate input
if (!Array.isArray(recipes)) {
- console.error("Invalid recipes data: expected array");
+ UnifiedLogger.error("storage", "Invalid recipes data: expected array");
return false;
}
@@ -454,7 +464,8 @@ export class OfflineStorageService {
const dataSize = JSON.stringify(recipes).length;
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
if (dataSize > MAX_SIZE) {
- console.error(
+ UnifiedLogger.error(
+ "storage",
`Recipe data too large: ${dataSize} bytes exceeds ${MAX_SIZE} bytes limit`
);
return false;
@@ -474,7 +485,7 @@ export class OfflineStorageService {
return true;
} catch (error) {
- console.error("Failed to store offline recipes:", error);
+ UnifiedLogger.error("storage", "Failed to store offline recipes:", error);
return false;
}
}
@@ -523,7 +534,10 @@ export class OfflineStorageService {
try {
// Validate input
if (!Array.isArray(ingredients)) {
- console.error("Invalid ingredients data: expected array");
+ UnifiedLogger.error(
+ "storage",
+ "Invalid ingredients data: expected array"
+ );
return false;
}
@@ -531,7 +545,8 @@ export class OfflineStorageService {
const dataSize = JSON.stringify(ingredients).length;
const MAX_SIZE = 5 * 1024 * 1024; // 5MB for ingredients
if (dataSize > MAX_SIZE) {
- console.error(
+ UnifiedLogger.error(
+ "storage",
`Ingredients data too large: ${dataSize} bytes exceeds ${MAX_SIZE} bytes limit`
);
return false;
@@ -551,7 +566,7 @@ export class OfflineStorageService {
return true;
} catch (error) {
- console.error("Failed to cache ingredients:", error);
+ UnifiedLogger.error("storage", "Failed to cache ingredients:", error);
return false;
}
}
@@ -610,7 +625,11 @@ export class OfflineStorageService {
await AsyncStorage.setItem(STORAGE_KEYS.LAST_SYNC, timestamp.toString());
return true;
} catch (error) {
- console.error("Failed to update last sync timestamp:", error);
+ UnifiedLogger.error(
+ "storage",
+ "Failed to update last sync timestamp:",
+ error
+ );
return false;
}
}
@@ -627,7 +646,11 @@ export class OfflineStorageService {
const value = Number(raw);
return Number.isFinite(value) ? value : null;
} catch (error) {
- console.error("Failed to get last sync timestamp:", error);
+ UnifiedLogger.error(
+ "storage",
+ "Failed to get last sync timestamp:",
+ error
+ );
return null;
}
}
@@ -645,7 +668,7 @@ export class OfflineStorageService {
return true;
} catch (error) {
- console.error("Failed to clear offline data:", error);
+ UnifiedLogger.error("storage", "Failed to clear offline data:", error);
return false;
}
}
@@ -684,7 +707,7 @@ export class OfflineStorageService {
totalStorageSize: recipesSize + ingredientsSize,
};
} catch (error) {
- console.error("Failed to get offline stats:", error);
+ UnifiedLogger.error("storage", "Failed to get offline stats:", error);
return {
recipesCount: 0,
ingredientsCount: 0,
diff --git a/src/styles/components/beerxml/unitConversionModalStyles.ts b/src/styles/components/beerxml/unitConversionModalStyles.ts
new file mode 100644
index 00000000..bcd0db8f
--- /dev/null
+++ b/src/styles/components/beerxml/unitConversionModalStyles.ts
@@ -0,0 +1,100 @@
+/**
+ * Unit Conversion Choice Modal Styles
+ *
+ * Styles for the unit conversion choice modal component.
+ */
+
+import { StyleSheet } from "react-native";
+
+export const unitConversionModalStyles = StyleSheet.create({
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ justifyContent: "center",
+ alignItems: "center",
+ padding: 20,
+ },
+ backdrop: {
+ ...StyleSheet.absoluteFillObject,
+ },
+ modalContent: {
+ borderRadius: 12,
+ padding: 24,
+ width: "100%",
+ maxWidth: 400,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ elevation: 5,
+ },
+ header: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginBottom: 16,
+ gap: 12,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: "600",
+ flex: 1,
+ },
+ messageContainer: {
+ marginBottom: 24,
+ gap: 12,
+ },
+ recipeName: {
+ fontSize: 18,
+ fontWeight: "600",
+ marginBottom: 8,
+ },
+ message: {
+ fontSize: 16,
+ lineHeight: 24,
+ },
+ unitHighlight: {
+ fontWeight: "600",
+ },
+ subMessage: {
+ fontSize: 14,
+ lineHeight: 20,
+ },
+ buttonContainer: {
+ gap: 12,
+ },
+ primaryButton: {
+ paddingVertical: 14,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "center",
+ minHeight: 48, // Touch target accessibility
+ },
+ secondaryButton: {
+ paddingVertical: 14,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ borderWidth: 1,
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "center",
+ minHeight: 48, // Touch target accessibility
+ },
+ buttonText: {
+ fontSize: 16,
+ fontWeight: "600",
+ },
+ buttonSubtext: {
+ fontSize: 12,
+ fontWeight: "400",
+ marginTop: 4,
+ opacity: 0.9,
+ },
+ buttonIcon: {
+ marginRight: 8,
+ },
+ buttonSpinner: {
+ marginRight: 8,
+ },
+});
diff --git a/src/styles/modals/createRecipeStyles.ts b/src/styles/modals/createRecipeStyles.ts
index 11b2874d..a9fe66a4 100644
--- a/src/styles/modals/createRecipeStyles.ts
+++ b/src/styles/modals/createRecipeStyles.ts
@@ -883,9 +883,6 @@ export const createRecipeStyles = (theme: ThemeContextValue) =>
},
ingredientSummary: {
marginTop: 8,
- flexDirection: "row",
- flexWrap: "wrap",
- gap: 8,
},
ingredientTypeCount: {
fontSize: 12,
@@ -1023,6 +1020,7 @@ export const createRecipeStyles = (theme: ThemeContextValue) =>
// Basic BeerXML styles
section: {
marginBottom: 24,
+ padding: 16,
},
button: {
flexDirection: "row" as const,
diff --git a/src/types/ai.ts b/src/types/ai.ts
index 0b22e246..8c7c283e 100644
--- a/src/types/ai.ts
+++ b/src/types/ai.ts
@@ -13,6 +13,7 @@
* @module types/ai
*/
+import { UnitSystem } from "./common";
import { Recipe, RecipeMetrics } from "./recipe";
/**
@@ -21,14 +22,15 @@ import { Recipe, RecipeMetrics } from "./recipe";
* Sends complete recipe data to backend for analysis and optimization
*/
export interface AIAnalysisRequest {
- /** Complete recipe object with all fields (ingredients, parameters, etc.) */
- complete_recipe: Recipe;
+ /** Complete recipe object with all fields (ingredients, parameters, etc.)
+ * Note: For unit_conversion workflow, can be a partial recipe */
+ complete_recipe: Recipe | Partial;
/** Optional beer style guide ID for style-specific analysis */
style_id?: string;
/** Unit system preference for analysis results */
- unit_system?: "metric" | "imperial";
+ unit_system?: UnitSystem;
/** Optional workflow name for specific optimization strategies */
workflow_name?: string;
@@ -53,7 +55,7 @@ export interface AIAnalysisResponse {
analysis_timestamp: string;
/** Unit system used for the analysis */
- unit_system: "metric" | "imperial";
+ unit_system: UnitSystem;
/** User preferences used during analysis */
user_preferences: AIUserPreferences;
diff --git a/src/types/api.ts b/src/types/api.ts
index 86aae2dd..07797e01 100644
--- a/src/types/api.ts
+++ b/src/types/api.ts
@@ -25,7 +25,12 @@ import {
BrewSessionSummary,
} from "./brewSession";
import { User, UserSettings } from "./user";
-import { ApiResponse, PaginatedResponse } from "./common";
+import {
+ ApiResponse,
+ PaginatedResponse,
+ TemperatureUnit,
+ UnitSystem,
+} from "./common";
// Authentication API types
export interface LoginRequest {
@@ -186,7 +191,7 @@ export interface CalculateMetricsPreviewRequest {
boil_time: number;
ingredients: Recipe["ingredients"];
mash_temperature?: number;
- mash_temp_unit?: "F" | "C";
+ mash_temp_unit?: TemperatureUnit;
}
export type CalculateMetricsPreviewResponse = RecipeMetrics;
@@ -401,3 +406,15 @@ export interface DashboardData {
}
export type DashboardResponse = ApiResponse;
+
+// BeerXML API types
+export interface BeerXMLConvertRecipeRequest {
+ recipe: Partial;
+ target_system: UnitSystem;
+ normalize?: boolean;
+}
+
+export interface BeerXMLConvertRecipeResponse {
+ recipe: Partial;
+ warnings?: string[];
+}
diff --git a/src/types/brewSession.ts b/src/types/brewSession.ts
index 4784e906..0cc3662a 100644
--- a/src/types/brewSession.ts
+++ b/src/types/brewSession.ts
@@ -1,4 +1,4 @@
-import { ID } from "./common";
+import { ID, TemperatureUnit } from "./common";
import { Recipe, HopFormat } from "./recipe";
// Brew session status types
@@ -20,9 +20,6 @@ export type FermentationStage =
| "bottled"
| "kegged";
-// Temperature unit
-export type TemperatureUnit = "F" | "C";
-
// Gravity reading interface
export interface GravityReading {
id: ID;
@@ -94,7 +91,7 @@ export interface BrewSession {
mash_temp?: number;
// Additional API fields
- temperature_unit?: "C" | "F";
+ temperature_unit?: TemperatureUnit;
fermentation_data?: FermentationEntry[]; // Backend uses fermentation_data
dry_hop_additions?: DryHopAddition[];
style_database_id?: string; // Android-specific field for style reference. Gets stripped out on API calls.
diff --git a/src/types/common.ts b/src/types/common.ts
index 88c54ddf..cf62e581 100644
--- a/src/types/common.ts
+++ b/src/types/common.ts
@@ -26,6 +26,8 @@ export interface PaginatedResponse {
// Unit system types
export type UnitSystem = "metric" | "imperial";
+export type TemperatureUnit = "C" | "F";
+
export type MeasurementType =
| "weight"
| "hop_weight"
diff --git a/src/types/recipe.ts b/src/types/recipe.ts
index 09b50181..01ef4bfa 100644
--- a/src/types/recipe.ts
+++ b/src/types/recipe.ts
@@ -17,11 +17,11 @@
* - Optional fields: Many type-specific fields are optional (e.g., alpha_acid for hops)
*/
-import { ID } from "./common";
+import { ID, TemperatureUnit, UnitSystem } from "./common";
// Recipe types
export type IngredientType = "grain" | "hop" | "yeast" | "other";
-export type BatchSizeUnit = "gal" | "l";
+export type BatchSizeUnit = "l" | "gal";
export type IngredientUnit =
| "lb"
| "kg"
@@ -42,7 +42,8 @@ export type HopFormat = "Pellet" | "Leaf" | "Plug" | "Whole" | "Extract";
// Recipe ingredient interface
export interface RecipeIngredient {
- id: ID;
+ id?: ID; // Optional - backend generates on creation, present on fetched recipes
+ ingredient_id?: string; // Reference to ingredient database entry (optional for test data and backward compat)
name: string;
type: IngredientType;
amount: number;
@@ -99,11 +100,11 @@ export interface Recipe {
description: string;
batch_size: number;
batch_size_unit: BatchSizeUnit;
- unit_system: "imperial" | "metric";
+ unit_system: UnitSystem;
boil_time: number;
efficiency: number;
mash_temperature: number;
- mash_temp_unit: "F" | "C";
+ mash_temp_unit: TemperatureUnit;
mash_time?: number;
is_public: boolean;
notes: string;
@@ -140,17 +141,39 @@ export interface RecipeFormData {
description: string;
batch_size: number;
batch_size_unit: BatchSizeUnit;
- unit_system: "imperial" | "metric";
+ unit_system: UnitSystem;
boil_time: number;
efficiency: number;
mash_temperature: number;
- mash_temp_unit: "F" | "C";
+ mash_temp_unit: TemperatureUnit;
mash_time?: number;
is_public: boolean;
notes: string;
ingredients: RecipeIngredient[];
}
+/**
+ * Minimal data required for metrics calculation
+ *
+ * Note: This interface intentionally does NOT include unit_system.
+ * The metrics calculator uses specific unit fields (batch_size_unit, mash_temp_unit)
+ * rather than relying on a general unit_system preference, ensuring calculations
+ * work correctly regardless of user's unit system settings.
+ *
+ * RecipeIngredient.id is optional, so this works for both imports and existing recipes.
+ */
+export interface RecipeMetricsInput {
+ batch_size: number;
+ batch_size_unit: BatchSizeUnit;
+ efficiency: number;
+ boil_time: number;
+ mash_temperature?: number;
+ mash_temp_unit?: TemperatureUnit;
+ ingredients: RecipeIngredient[];
+}
+
+// RecipeCreatePayload removed - Partial is sufficient since RecipeIngredient.id is optional
+
// Recipe search filters
export interface RecipeSearchFilters {
style?: string;
@@ -222,14 +245,5 @@ export interface CreateRecipeIngredientData {
notes?: string;
}
-// Strict ingredient input for BeerXML import with required fields for validation
-export interface IngredientInput {
- ingredient_id: string;
- name: string;
- type: IngredientType;
- amount: number;
- unit: IngredientUnit;
- use?: string;
- time?: number;
- instance_id?: string; // Unique instance identifier for React keys and duplicate ingredient handling (optional - backend generates if missing)
-}
+// IngredientInput removed - RecipeIngredient now handles both creation and fetched recipes
+// with optional id field
diff --git a/tests/app/(modals)/(beerxml)/importBeerXML.test.tsx b/tests/app/(modals)/(beerxml)/importBeerXML.test.tsx
index 6137aced..e31dcab3 100644
--- a/tests/app/(modals)/(beerxml)/importBeerXML.test.tsx
+++ b/tests/app/(modals)/(beerxml)/importBeerXML.test.tsx
@@ -10,11 +10,40 @@ import { TEST_IDS } from "@src/constants/testIDs";
// Mock dependencies
jest.mock("@contexts/ThemeContext", () => ({
useTheme: () => ({
- colors: { primary: "#007AFF", text: "#000" },
+ colors: {
+ primary: "#007AFF",
+ text: "#000",
+ background: "#FFF",
+ backgroundSecondary: "#F5F5F5",
+ textSecondary: "#666",
+ border: "#CCC",
+ warning: "#F59E0B",
+ },
fonts: { regular: "System" },
}),
}));
+jest.mock("@contexts/UnitContext", () => ({
+ useUnits: () => ({
+ unitSystem: "imperial",
+ loading: false,
+ error: null,
+ updateUnitSystem: jest.fn(),
+ setError: jest.fn(),
+ getPreferredUnit: jest.fn(),
+ convertUnit: jest.fn(),
+ convertForDisplay: jest.fn(),
+ formatValue: jest.fn(),
+ getTemperatureSymbol: jest.fn(),
+ formatTemperature: jest.fn(),
+ formatCurrentTemperature: jest.fn(),
+ convertTemperature: jest.fn(),
+ getTemperatureAxisConfig: jest.fn(),
+ getUnitSystemLabel: jest.fn(),
+ getCommonUnits: jest.fn(),
+ }),
+}));
+
jest.mock("expo-router", () => ({
router: { back: jest.fn(), push: jest.fn() },
}));
@@ -25,10 +54,17 @@ jest.mock("@services/beerxml/BeerXMLService", () => {
default: {
importBeerXMLFile: jest.fn(),
parseBeerXML: jest.fn(),
+ convertRecipeUnits: jest.fn(recipe =>
+ Promise.resolve({ recipe, warnings: [] })
+ ),
},
};
});
+jest.mock("@src/components/beerxml/UnitConversionChoiceModal", () => ({
+ UnitConversionChoiceModal: () => null,
+}));
+
describe("ImportBeerXMLScreen", () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -162,7 +198,7 @@ describe("ImportBeerXMLScreen - User Interactions", () => {
jest.clearAllMocks();
});
- it("should handle file selection success", async () => {
+ it("should handle file selection success and navigate through unit choice", async () => {
// Mock successful file import and parsing
const mockBeerXMLService =
require("@services/beerxml/BeerXMLService").default;
@@ -177,38 +213,31 @@ describe("ImportBeerXMLScreen - User Interactions", () => {
{
name: "Test Recipe",
style: "IPA",
+ batch_size: 20,
+ batch_size_unit: "l",
ingredients: [
- { name: "Pale Malt", type: "grain", amount: 10, unit: "lb" },
+ { name: "Pale Malt", type: "grain", amount: 4500, unit: "g" },
],
},
]);
- const mockRouter = require("expo-router").router;
-
- const { getByTestId, getByText } = render();
+ const { getByTestId } = render();
const selectButton = getByTestId(TEST_IDS.beerxml.selectFileButton);
await act(async () => {
fireEvent.press(selectButton);
});
- // Wait until parsing is done and recipe preview is shown
+ // Wait until parsing is done
await waitFor(() => {
- expect(getByText("Recipe Preview")).toBeTruthy();
- });
-
- // Press the Import Recipe button
- await act(async () => {
- fireEvent.press(getByText("Import Recipe"));
+ expect(mockBeerXMLService.parseBeerXML).toHaveBeenCalled();
});
expect(mockBeerXMLService.importBeerXMLFile).toHaveBeenCalled();
- expect(mockRouter.push).toHaveBeenCalledWith({
- pathname: "/(modals)/(beerxml)/ingredientMatching",
- params: expect.objectContaining({
- filename: "test_recipe.xml",
- }),
- });
+
+ // After parsing, the component moves to unit_choice step
+ // The modal would be shown but is mocked to return null
+ // We can verify the flow reached this point by checking services were called
});
it("should handle file selection error", async () => {
@@ -428,3 +457,125 @@ it("should retry file selection after error", async () => {
'Retry Recipe'
);
});
+
+describe("ImportBeerXMLScreen - Unit Conversion Workflow", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("should always show unit choice modal after parsing (BeerXML is always metric)", async () => {
+ const mockBeerXMLService =
+ require("@services/beerxml/BeerXMLService").default;
+
+ mockBeerXMLService.importBeerXMLFile = jest.fn().mockResolvedValue({
+ success: true,
+ content:
+ 'Test Recipe',
+ filename: "test_recipe.xml",
+ });
+
+ mockBeerXMLService.parseBeerXML = jest.fn().mockResolvedValue([
+ {
+ name: "Test Recipe",
+ style: "IPA",
+ batch_size: 20,
+ batch_size_unit: "l",
+ ingredients: [
+ { name: "Pale Malt", type: "grain", amount: 4500, unit: "g" },
+ ],
+ },
+ ]);
+
+ const { getByTestId } = render();
+ const selectButton = getByTestId(TEST_IDS.beerxml.selectFileButton);
+
+ await act(async () => {
+ fireEvent.press(selectButton);
+ });
+
+ // Wait for parsing to complete
+ await waitFor(() => {
+ expect(mockBeerXMLService.parseBeerXML).toHaveBeenCalled();
+ });
+
+ // The modal is mocked, but we can verify the state reached unit_choice step
+ // by checking that the modal would be rendered with visible=true
+ expect(mockBeerXMLService.importBeerXMLFile).toHaveBeenCalled();
+ });
+
+ it("should convert recipe to chosen unit system with normalization", async () => {
+ const mockBeerXMLService =
+ require("@services/beerxml/BeerXMLService").default;
+
+ const metricRecipe = {
+ name: "Test Recipe",
+ style: "IPA",
+ batch_size: 20,
+ batch_size_unit: "l",
+ ingredients: [
+ { name: "Pale Malt", type: "grain", amount: 4500, unit: "g" },
+ ],
+ };
+
+ const convertedRecipe = {
+ name: "Test Recipe",
+ style: "IPA",
+ batch_size: 5.28,
+ batch_size_unit: "gal",
+ ingredients: [
+ { name: "Pale Malt", type: "grain", amount: 10, unit: "lb" }, // Normalized from 9.92
+ ],
+ };
+
+ mockBeerXMLService.importBeerXMLFile = jest.fn().mockResolvedValue({
+ success: true,
+ content: '',
+ filename: "test_recipe.xml",
+ });
+
+ mockBeerXMLService.parseBeerXML = jest
+ .fn()
+ .mockResolvedValue([metricRecipe]);
+
+ mockBeerXMLService.convertRecipeUnits = jest
+ .fn()
+ .mockResolvedValue({ recipe: convertedRecipe, warnings: [] });
+
+ render();
+
+ // Note: Since modal is mocked, we can't test the full flow
+ // but the convertRecipeUnits function is set up correctly
+ expect(mockBeerXMLService.convertRecipeUnits).toBeDefined();
+ });
+
+ it("should handle conversion errors gracefully", async () => {
+ const mockBeerXMLService =
+ require("@services/beerxml/BeerXMLService").default;
+
+ mockBeerXMLService.importBeerXMLFile = jest.fn().mockResolvedValue({
+ success: true,
+ content: '',
+ filename: "test_recipe.xml",
+ });
+
+ mockBeerXMLService.parseBeerXML = jest.fn().mockResolvedValue([
+ {
+ name: "Test Recipe",
+ style: "IPA",
+ batch_size: 20,
+ batch_size_unit: "l",
+ ingredients: [],
+ },
+ ]);
+
+ mockBeerXMLService.convertRecipeUnits = jest
+ .fn()
+ .mockRejectedValue(new Error("Conversion failed"));
+
+ render();
+
+ // Conversion errors are handled when user chooses a unit system
+ // Since modal is mocked, we just verify the error handling is set up
+ expect(mockBeerXMLService.convertRecipeUnits).toBeDefined();
+ });
+});
diff --git a/tests/app/(modals)/(beerxml)/importReview.test.tsx b/tests/app/(modals)/(beerxml)/importReview.test.tsx
index dcd76fe2..a1805d0a 100644
--- a/tests/app/(modals)/(beerxml)/importReview.test.tsx
+++ b/tests/app/(modals)/(beerxml)/importReview.test.tsx
@@ -3,18 +3,29 @@
*/
import React from "react";
-import { render } from "@testing-library/react-native";
+import { renderWithProviders, testUtils } from "@/tests/testUtils";
import ImportReviewScreen from "../../../../app/(modals)/(beerxml)/importReview";
import { TEST_IDS } from "@src/constants/testIDs";
-// Mock dependencies
-jest.mock("@contexts/ThemeContext", () => ({
- useTheme: () => ({
- colors: { primary: "#007AFF", text: "#000", background: "#FFF" },
- fonts: { regular: "System" },
- }),
+// Mock Appearance
+jest.mock("react-native/Libraries/Utilities/Appearance", () => ({
+ getColorScheme: jest.fn(() => "light"),
+ addChangeListener: jest.fn(),
+ removeChangeListener: jest.fn(),
}));
+// Mock dependencies
+jest.mock("@contexts/ThemeContext", () => {
+ const React = require("react");
+ return {
+ useTheme: () => ({
+ colors: { primary: "#007AFF", text: "#000", background: "#FFF" },
+ fonts: { regular: "System" },
+ }),
+ ThemeProvider: ({ children }: { children: React.ReactNode }) => children,
+ };
+});
+
jest.mock("expo-router", () => ({
router: { back: jest.fn(), push: jest.fn() },
useLocalSearchParams: jest.fn(() => ({
@@ -29,35 +40,9 @@ jest.mock("expo-router", () => ({
})),
}));
-jest.mock("@tanstack/react-query", () => {
- const actual = jest.requireActual("@tanstack/react-query");
- return {
- ...actual,
- QueryClient: jest.fn().mockImplementation(() => ({
- invalidateQueries: jest.fn(),
- clear: jest.fn(),
- defaultOptions: jest.fn(() => ({ queries: { retry: false } })),
- getQueryCache: jest.fn(() => ({
- getAll: jest.fn(() => []),
- })),
- removeQueries: jest.fn(),
- })),
- useQuery: jest.fn(() => ({
- data: null,
- isLoading: false,
- error: null,
- })),
- useMutation: jest.fn(() => ({
- mutate: jest.fn(),
- mutateAsync: jest.fn(),
- isLoading: false,
- error: null,
- })),
- useQueryClient: jest.fn(() => ({
- invalidateQueries: jest.fn(),
- })),
- };
-});
+// Note: @tanstack/react-query is already mocked in setupTests.js with a more
+// sophisticated implementation that includes Map-backed query storage and cache tracking.
+// No need to override it here.
jest.mock("@services/api/apiService", () => ({
default: {
@@ -68,51 +53,108 @@ jest.mock("@services/api/apiService", () => ({
},
}));
+// Mock auth context
+let mockAuthState = {
+ user: { id: "test-user", username: "testuser", email: "test@example.com" },
+ isLoading: false,
+ isAuthenticated: true,
+ error: null,
+};
+
+const setMockAuthState = (overrides: Partial) => {
+ mockAuthState = { ...mockAuthState, ...overrides };
+};
+
+jest.mock("@contexts/AuthContext", () => {
+ return {
+ useAuth: () => mockAuthState,
+ AuthProvider: ({ children }: { children: React.ReactNode }) => children,
+ };
+});
+
+// Mock other context providers
+jest.mock("@contexts/NetworkContext", () => {
+ return {
+ useNetwork: () => ({ isConnected: true, isInternetReachable: true }),
+ NetworkProvider: ({ children }: { children: React.ReactNode }) => children,
+ };
+});
+
+jest.mock("@contexts/DeveloperContext", () => {
+ return {
+ useDeveloper: () => ({ isDeveloperMode: false }),
+ DeveloperProvider: ({ children }: { children: React.ReactNode }) =>
+ children,
+ };
+});
+
+jest.mock("@contexts/UnitContext", () => {
+ return {
+ useUnits: () => ({ unitSystem: "metric", setUnitSystem: jest.fn() }),
+ UnitProvider: ({ children }: { children: React.ReactNode }) => children,
+ };
+});
+
+jest.mock("@contexts/CalculatorsContext", () => {
+ return {
+ useCalculators: () => ({ state: {}, dispatch: jest.fn() }),
+ CalculatorsProvider: ({ children }: { children: React.ReactNode }) =>
+ children,
+ };
+});
+
describe("ImportReviewScreen", () => {
beforeEach(() => {
jest.clearAllMocks();
+ testUtils.resetCounters();
+ setMockAuthState({
+ user: {
+ id: "test-user",
+ username: "testuser",
+ email: "test@example.com",
+ },
+ isLoading: false,
+ isAuthenticated: true,
+ error: null,
+ });
});
it("should render without crashing", () => {
- expect(() => render()).not.toThrow();
+ expect(() => renderWithProviders()).not.toThrow();
});
it("should display import review title", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("Import Review")).toBeTruthy();
});
it("should display recipe name", () => {
- const { getAllByText } = render();
+ const { getAllByText } = renderWithProviders();
expect(getAllByText("Test Recipe").length).toBeGreaterThan(0);
});
});
describe("ImportReviewScreen - Additional UI Tests", () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
it("should display batch size information", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("Batch Size:")).toBeTruthy();
// The 5.0 and gal are in the same text node together
expect(getByText("5.0 gal")).toBeTruthy();
});
it("should show metrics section", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("Calculated Metrics")).toBeTruthy();
expect(getByText("No metrics calculated")).toBeTruthy();
});
it("should display filename from params", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("test.xml")).toBeTruthy();
});
it("should have proper screen structure", () => {
- const { getByTestId } = render();
+ const { getByTestId } = renderWithProviders();
expect(
getByTestId(TEST_IDS.patterns.scrollAction("import-review"))
).toBeTruthy();
@@ -120,12 +162,8 @@ describe("ImportReviewScreen - Additional UI Tests", () => {
});
describe("ImportReviewScreen - UI Elements", () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
it("should display recipe details section", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("Recipe Details")).toBeTruthy();
expect(getByText("Name:")).toBeTruthy();
@@ -134,7 +172,9 @@ describe("ImportReviewScreen - UI Elements", () => {
});
it("should display import summary section", () => {
- const { getByText, getAllByText } = render();
+ const { getByText, getAllByText } = renderWithProviders(
+
+ );
expect(getByText("Import Summary")).toBeTruthy();
expect(getByText("Source File")).toBeTruthy();
@@ -144,14 +184,14 @@ describe("ImportReviewScreen - UI Elements", () => {
});
it("should display action buttons", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("Review Ingredients")).toBeTruthy();
expect(getByText("Create Recipe")).toBeTruthy();
});
it("should show efficiency and boil time", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("Efficiency:")).toBeTruthy();
expect(getByText("75%")).toBeTruthy(); // efficiency with %
@@ -160,7 +200,9 @@ describe("ImportReviewScreen - UI Elements", () => {
});
it("should show ingredients section", () => {
- const { getAllByText, getByText } = render();
+ const { getAllByText, getByText } = renderWithProviders(
+
+ );
expect(getAllByText("Ingredients").length).toBeGreaterThan(0);
expect(getByText("0 ingredients")).toBeTruthy(); // count with text
@@ -189,18 +231,14 @@ describe("ImportReviewScreen - coerceIngredientTime Function", () => {
createdIngredientsCount: "6",
});
- const { getAllByText } = render();
+ const { getAllByText } = renderWithProviders();
expect(getAllByText("Time Test Recipe").length).toBeGreaterThan(0);
});
});
describe("ImportReviewScreen - Advanced UI Tests", () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
it("should display recipe with no specified style", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("Not specified")).toBeTruthy(); // Style not specified
});
@@ -212,24 +250,62 @@ describe("ImportReviewScreen - Advanced UI Tests", () => {
batch_size: 5,
batch_size_unit: "gal",
ingredients: [
- { name: "Hop 1" },
- { name: "Hop 2" },
- { name: "Hop 3" },
- { name: "Hop 4" },
- { name: "Hop 5" },
- { name: "Hop 6" },
+ {
+ ingredient_id: "1",
+ name: "Hop 1",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ },
+ {
+ ingredient_id: "2",
+ name: "Hop 2",
+ type: "hop",
+ amount: 2,
+ unit: "oz",
+ },
+ {
+ ingredient_id: "3",
+ name: "Hop 3",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ },
+ {
+ ingredient_id: "4",
+ name: "Hop 4",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ },
+ {
+ ingredient_id: "5",
+ name: "Hop 5",
+ type: "hop",
+ amount: 0.5,
+ unit: "oz",
+ },
+ {
+ ingredient_id: "6",
+ name: "Hop 6",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ },
],
}),
filename: "test.xml",
createdIngredientsCount: "6",
});
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("6 ingredients")).toBeTruthy();
});
it("should display multiple UI sections", () => {
- const { getByText, getAllByText } = render();
+ const { getByText, getAllByText } = renderWithProviders(
+
+ );
expect(getByText("Import Summary")).toBeTruthy();
expect(getByText("Recipe Details")).toBeTruthy();
@@ -238,21 +314,23 @@ describe("ImportReviewScreen - Advanced UI Tests", () => {
});
it("should show default recipe values", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("60 minutes")).toBeTruthy(); // Default boil time
expect(getByText("75%")).toBeTruthy(); // Default efficiency
});
it("should display import action buttons", () => {
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("Review Ingredients")).toBeTruthy();
expect(getByText("Create Recipe")).toBeTruthy();
});
it("should render all component parts", () => {
- const { getByTestId, getAllByRole } = render();
+ const { getByTestId, getAllByRole } = renderWithProviders(
+
+ );
expect(
getByTestId(TEST_IDS.patterns.scrollAction("import-review"))
@@ -268,10 +346,6 @@ describe("ImportReviewScreen - Advanced UI Tests", () => {
});
describe("ImportReviewScreen - Recipe Variations", () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
it("should handle recipe with custom values", () => {
const mockParams = jest.requireMock("expo-router").useLocalSearchParams;
mockParams.mockReturnValue({
@@ -291,7 +365,9 @@ describe("ImportReviewScreen - Recipe Variations", () => {
createdIngredientsCount: "2",
});
- const { getAllByText, getByText } = render();
+ const { getAllByText, getByText } = renderWithProviders(
+
+ );
expect(getAllByText("Custom Recipe").length).toBeGreaterThan(0);
expect(getByText("10.0 L")).toBeTruthy(); // Metric batch size (uppercase L)
@@ -315,7 +391,7 @@ describe("ImportReviewScreen - Recipe Variations", () => {
error: null,
});
- const { getByText } = render();
+ const { getByText } = renderWithProviders();
expect(getByText("Calculated Metrics")).toBeTruthy();
// Restore original mock
diff --git a/tests/app/(modals)/(calculators)/hydrometerCorrection.test.tsx b/tests/app/(modals)/(calculators)/hydrometerCorrection.test.tsx
index cad5bc36..c1541220 100644
--- a/tests/app/(modals)/(calculators)/hydrometerCorrection.test.tsx
+++ b/tests/app/(modals)/(calculators)/hydrometerCorrection.test.tsx
@@ -45,7 +45,7 @@ const mockState = {
measuredGravity: "",
wortTemp: "",
calibrationTemp: "68",
- tempUnit: "f",
+ tempUnit: "F",
result: null as any,
},
};
@@ -131,7 +131,7 @@ jest.mock("@components/calculators/UnitToggle", () => {
return (
onChange && onChange(value === "f" ? "c" : "f")}
+ onPress={() => onChange && onChange(value === "F" ? "C" : "F")}
>
{label}
@@ -275,7 +275,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
expect(mockDispatch).toHaveBeenCalledWith({
type: "SET_HYDROMETER_CORRECTION",
payload: {
- tempUnit: "c",
+ tempUnit: "C",
wortTemp: "23.9", // 75°F to °C
calibrationTemp: "20.0", // 68°F to °C
},
@@ -289,7 +289,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "68",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -303,7 +303,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
expect(mockDispatch).toHaveBeenCalledWith({
type: "SET_HYDROMETER_CORRECTION",
payload: {
- tempUnit: "c",
+ tempUnit: "C",
wortTemp: "20.0",
calibrationTemp: "20.0",
},
@@ -315,7 +315,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "20",
calibrationTemp: "20",
- tempUnit: "c" as const,
+ tempUnit: "C" as const,
result: null,
};
@@ -329,7 +329,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
expect(mockDispatch).toHaveBeenCalledWith({
type: "SET_HYDROMETER_CORRECTION",
payload: {
- tempUnit: "f",
+ tempUnit: "F",
wortTemp: "68.0",
calibrationTemp: "68.0",
},
@@ -341,7 +341,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "",
wortTemp: "",
calibrationTemp: "",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -355,7 +355,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
expect(mockDispatch).toHaveBeenCalledWith({
type: "SET_HYDROMETER_CORRECTION",
payload: {
- tempUnit: "c",
+ tempUnit: "C",
wortTemp: "",
calibrationTemp: "",
},
@@ -367,7 +367,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "invalid",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -381,7 +381,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
expect(mockDispatch).toHaveBeenCalledWith({
type: "SET_HYDROMETER_CORRECTION",
payload: {
- tempUnit: "c",
+ tempUnit: "C",
wortTemp: "invalid", // Should remain unchanged
calibrationTemp: "20.0",
},
@@ -395,7 +395,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "",
wortTemp: "",
calibrationTemp: "",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -415,7 +415,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -436,7 +436,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "75",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -448,7 +448,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
1.05,
75,
68,
- "f"
+ "F"
);
expect(mockDispatch).toHaveBeenCalledWith({
@@ -467,7 +467,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "75",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -483,7 +483,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: 1.05,
wortTemp: 75,
calibrationTemp: 68,
- tempUnit: "f",
+ tempUnit: "F",
},
result: {
correctedGravity: 1.048,
@@ -498,7 +498,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "invalid",
wortTemp: "75",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -518,7 +518,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "75",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -550,7 +550,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "75",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: 1.048,
};
@@ -566,7 +566,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "75",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: 1.048,
};
@@ -590,7 +590,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
});
it("should show Celsius placeholders and units when in Celsius mode", () => {
- mockState.hydrometerCorrection.tempUnit = "c";
+ mockState.hydrometerCorrection.tempUnit = "C";
renderWithProviders();
// This is indirectly tested through the NumberInput props
@@ -603,7 +603,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "0",
wortTemp: "0",
calibrationTemp: "0",
- tempUnit: "c" as const,
+ tempUnit: "C" as const,
result: null,
};
@@ -613,7 +613,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
0,
0,
0,
- "c"
+ "C"
);
});
@@ -622,7 +622,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.100",
wortTemp: "212",
calibrationTemp: "32",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -632,7 +632,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
1.1,
212,
32,
- "f"
+ "F"
);
});
@@ -641,7 +641,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.150",
wortTemp: "68",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -651,7 +651,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
1.15,
68,
68,
- "f"
+ "F"
);
});
@@ -660,7 +660,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "68.5",
calibrationTemp: "67.8",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -670,7 +670,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
1.05,
68.5,
67.8,
- "f"
+ "F"
);
});
});
@@ -725,7 +725,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
measuredGravity: "1.050",
wortTemp: "68",
calibrationTemp: "68",
- tempUnit: "f" as const,
+ tempUnit: "F" as const,
result: null,
};
@@ -740,7 +740,7 @@ describe("HydrometerCorrectionCalculatorScreen", () => {
expect(mockDispatch).toHaveBeenCalledWith({
type: "SET_HYDROMETER_CORRECTION",
payload: {
- tempUnit: "c",
+ tempUnit: "C",
wortTemp: "20.0", // 68°F -> 20.0°C
calibrationTemp: "20.0", // 68°F -> 20.0°C
},
diff --git a/tests/app/(modals)/(calculators)/strikeWater.test.tsx b/tests/app/(modals)/(calculators)/strikeWater.test.tsx
index d853e6b1..f64ca1a4 100644
--- a/tests/app/(modals)/(calculators)/strikeWater.test.tsx
+++ b/tests/app/(modals)/(calculators)/strikeWater.test.tsx
@@ -34,7 +34,7 @@ const mockCalculatorsState = {
grainWeightUnit: "lb",
grainTemp: "",
targetMashTemp: "",
- tempUnit: "f",
+ tempUnit: "F",
waterToGrainRatio: "1.25",
result: null as any,
},
@@ -123,9 +123,9 @@ jest.mock("@services/calculators/StrikeWaterCalculator", () => ({
// Mock unit converter
const mockUnitConverter = {
convertTemperature: jest.fn((value, from, to) => {
- if (from === "f" && to === "c") {
+ if (from === "F" && to === "C") {
return (value - 32) * (5 / 9);
- } else if (from === "c" && to === "f") {
+ } else if (from === "C" && to === "F") {
return (value * 9) / 5 + 32;
}
return value;
@@ -251,7 +251,7 @@ describe("StrikeWaterCalculatorScreen", () => {
grainWeightUnit: "lb",
grainTemp: "",
targetMashTemp: "",
- tempUnit: "f",
+ tempUnit: "F",
waterToGrainRatio: "1.25",
result: null,
};
@@ -272,7 +272,7 @@ describe("StrikeWaterCalculatorScreen", () => {
grainWeightUnit: "lb",
grainTemp: "70",
targetMashTemp: "152",
- tempUnit: "f",
+ tempUnit: "F",
waterToGrainRatio: "1.25",
result: null,
};
@@ -293,7 +293,7 @@ describe("StrikeWaterCalculatorScreen", () => {
grainWeightUnit: "lb",
grainTemp: "70",
targetMashTemp: "152",
- tempUnit: "f",
+ tempUnit: "F",
waterToGrainRatio: "1.25",
result: null,
};
@@ -318,7 +318,7 @@ describe("StrikeWaterCalculatorScreen", () => {
grainWeightUnit: "lb",
grainTemp: "70",
targetMashTemp: "152",
- tempUnit: "f",
+ tempUnit: "F",
waterToGrainRatio: "1.25",
result: null,
};
@@ -347,7 +347,7 @@ describe("StrikeWaterCalculatorScreen", () => {
grainWeightUnit: "lb",
grainTemp: "70",
targetMashTemp: "152",
- tempUnit: "f",
+ tempUnit: "F",
waterToGrainRatio: "", // Missing ratio
result: null,
};
@@ -378,7 +378,7 @@ describe("StrikeWaterCalculatorScreen", () => {
grainWeightUnit: "lb",
grainTemp: "70",
targetMashTemp: "152",
- tempUnit: "f",
+ tempUnit: "F",
waterToGrainRatio: "1.25",
result: null,
};
@@ -398,7 +398,7 @@ describe("StrikeWaterCalculatorScreen", () => {
grainWeightUnit: "lb",
grainTemp: "70",
targetMashTemp: "152",
- tempUnit: "f",
+ tempUnit: "F",
waterToGrainRatio: "0",
result: null,
};
diff --git a/tests/services/brewing/OfflineMetricsCalculator.test.ts b/tests/services/brewing/OfflineMetricsCalculator.test.ts
new file mode 100644
index 00000000..c6eeaa0d
--- /dev/null
+++ b/tests/services/brewing/OfflineMetricsCalculator.test.ts
@@ -0,0 +1,572 @@
+/**
+ * Offline Metrics Calculator Tests
+ *
+ * Tests for brewing calculations without API dependency
+ */
+
+import { OfflineMetricsCalculator } from "@services/brewing/OfflineMetricsCalculator";
+import { RecipeMetricsInput, RecipeIngredient } from "@src/types";
+
+describe("OfflineMetricsCalculator", () => {
+ describe("validateRecipeData", () => {
+ const baseRecipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: 10,
+ unit: "lb",
+ },
+ ],
+ };
+
+ it("should validate valid recipe data", () => {
+ const result = OfflineMetricsCalculator.validateRecipeData(baseRecipe);
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors).toHaveLength(0);
+ });
+
+ it("should reject recipe with zero batch size", () => {
+ const invalidRecipe = { ...baseRecipe, batch_size: 0 };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain("Batch size must be greater than 0");
+ });
+
+ it("should reject recipe with negative batch size", () => {
+ const invalidRecipe = { ...baseRecipe, batch_size: -5 };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain("Batch size must be greater than 0");
+ });
+
+ it("should reject recipe with zero efficiency", () => {
+ const invalidRecipe = { ...baseRecipe, efficiency: 0 };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain("Efficiency must be between 1 and 100");
+ });
+
+ it("should reject recipe with efficiency > 100", () => {
+ const invalidRecipe = { ...baseRecipe, efficiency: 101 };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain("Efficiency must be between 1 and 100");
+ });
+
+ it("should reject recipe with negative boil time", () => {
+ const invalidRecipe = { ...baseRecipe, boil_time: -10 };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain("Boil time must be zero or greater");
+ });
+
+ it("should accept recipe with zero boil time", () => {
+ const validRecipe = { ...baseRecipe, boil_time: 0 };
+ const result = OfflineMetricsCalculator.validateRecipeData(validRecipe);
+
+ expect(result.isValid).toBe(true);
+ });
+
+ it("should reject recipe with no ingredients", () => {
+ const invalidRecipe = { ...baseRecipe, ingredients: [] };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain("At least one ingredient is required");
+ });
+
+ it("should reject recipe with no fermentables", () => {
+ const invalidRecipe: RecipeMetricsInput = {
+ ...baseRecipe,
+ ingredients: [
+ {
+ id: "1",
+ name: "Cascade Hops",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ },
+ ] as RecipeIngredient[],
+ };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain(
+ "At least one fermentable (grain or sugar/extract) is required"
+ );
+ });
+
+ it("should reject ingredient with negative amount", () => {
+ const invalidRecipe: RecipeMetricsInput = {
+ ...baseRecipe,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: -5,
+ unit: "lb",
+ },
+ ] as RecipeIngredient[],
+ };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain("Pale Malt amount must be >= 0");
+ });
+
+ it("should reject hop with invalid alpha acid", () => {
+ const invalidRecipe: RecipeMetricsInput = {
+ ...baseRecipe,
+ ingredients: [
+ ...baseRecipe.ingredients,
+ {
+ id: "2",
+ name: "Cascade",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ alpha_acid: 35,
+ },
+ ] as RecipeIngredient[],
+ };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain(
+ "Hop alpha acid must be between 0 and 30"
+ );
+ });
+
+ it("should reject yeast with invalid attenuation", () => {
+ const invalidRecipe: RecipeMetricsInput = {
+ ...baseRecipe,
+ ingredients: [
+ ...baseRecipe.ingredients,
+ {
+ id: "3",
+ name: "US-05",
+ type: "yeast",
+ amount: 1,
+ unit: "pkg",
+ attenuation: 150,
+ },
+ ] as RecipeIngredient[],
+ };
+ const result = OfflineMetricsCalculator.validateRecipeData(invalidRecipe);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain(
+ "Yeast attenuation must be between 0 and 100"
+ );
+ });
+ });
+
+ describe("calculateMetrics", () => {
+ it("should calculate metrics for simple pale ale recipe", () => {
+ const recipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: 10,
+ unit: "lb",
+ potential: 37,
+ color: 2,
+ },
+ {
+ id: "2",
+ name: "Cascade",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ alpha_acid: 5.5,
+ use: "boil",
+ time: 60,
+ },
+ {
+ id: "3",
+ name: "US-05",
+ type: "yeast",
+ amount: 1,
+ unit: "pkg",
+ attenuation: 75,
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(recipe);
+
+ expect(metrics.og).toBeGreaterThan(1.0);
+ expect(metrics.og).toBeLessThan(1.1);
+ expect(metrics.fg).toBeGreaterThan(1.0);
+ expect(metrics.fg).toBeLessThan(metrics.og);
+ expect(metrics.abv).toBeGreaterThan(0);
+ expect(metrics.ibu).toBeGreaterThan(0);
+ expect(metrics.srm).toBeGreaterThan(0);
+ });
+
+ it("should return default metrics for invalid recipe", () => {
+ const invalidRecipe: RecipeMetricsInput = {
+ batch_size: 0,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(invalidRecipe);
+
+ expect(metrics).toEqual({
+ og: 1.0,
+ fg: 1.0,
+ abv: 0.0,
+ ibu: 0.0,
+ srm: 0.0,
+ });
+ });
+
+ it("should handle metric units (liters)", () => {
+ const recipe: RecipeMetricsInput = {
+ batch_size: 20,
+ batch_size_unit: "l",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "C",
+ mash_temperature: 67,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: 4.5,
+ unit: "kg",
+ potential: 37,
+ color: 2,
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(recipe);
+
+ expect(metrics.og).toBeGreaterThan(1.0);
+ expect(metrics.srm).toBeGreaterThan(0);
+ });
+
+ it("should calculate correct FG when no yeast is present (0% attenuation)", () => {
+ const recipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: 10,
+ unit: "lb",
+ potential: 37,
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(recipe);
+
+ // With no yeast, FG should equal OG (0% attenuation)
+ expect(metrics.fg).toBe(metrics.og);
+ expect(metrics.abv).toBe(0);
+ });
+
+ it("should skip dry hop additions in IBU calculation", () => {
+ const recipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: 10,
+ unit: "lb",
+ potential: 37,
+ },
+ {
+ id: "2",
+ name: "Cascade",
+ type: "hop",
+ amount: 2,
+ unit: "oz",
+ alpha_acid: 5.5,
+ use: "dry-hop",
+ time: 7,
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(recipe);
+
+ // Dry hops shouldn't contribute to IBU
+ expect(metrics.ibu).toBe(0);
+ });
+
+ it("should handle whirlpool/flameout hops with specific time", () => {
+ const recipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: 10,
+ unit: "lb",
+ potential: 37,
+ },
+ {
+ id: "2",
+ name: "Cascade",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ alpha_acid: 5.5,
+ use: "whirlpool",
+ time: 20,
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(recipe);
+
+ // Whirlpool hops with time should contribute some IBU
+ expect(metrics.ibu).toBeGreaterThan(0);
+ });
+
+ it("should use default values for missing ingredient properties", () => {
+ const recipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: 10,
+ unit: "lb",
+ // Missing potential, color - should use defaults
+ },
+ {
+ id: "2",
+ name: "Cascade",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ // Missing alpha_acid - should default to 5%
+ use: "boil",
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(recipe);
+
+ expect(metrics.og).toBeGreaterThan(1.0);
+ expect(metrics.ibu).toBeGreaterThan(0);
+ expect(metrics.srm).toBeGreaterThan(0);
+ });
+
+ it("should round metrics to proper decimal places", () => {
+ const recipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: 10,
+ unit: "lb",
+ potential: 37,
+ color: 2,
+ },
+ {
+ id: "2",
+ name: "Cascade",
+ type: "hop",
+ amount: 1,
+ unit: "oz",
+ alpha_acid: 5.5,
+ use: "boil",
+ time: 60,
+ },
+ {
+ id: "3",
+ name: "US-05",
+ type: "yeast",
+ amount: 1,
+ unit: "pkg",
+ attenuation: 75,
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(recipe);
+
+ // OG/FG should be 3 decimal places (e.g., 1.050)
+ expect(metrics.og.toString().split(".")[1]?.length).toBeLessThanOrEqual(
+ 3
+ );
+ expect(metrics.fg.toString().split(".")[1]?.length).toBeLessThanOrEqual(
+ 3
+ );
+
+ // ABV, IBU, SRM should be 1 decimal place
+ expect(metrics.abv.toString().split(".")[1]?.length).toBeLessThanOrEqual(
+ 1
+ );
+ expect(metrics.ibu.toString().split(".")[1]?.length).toBeLessThanOrEqual(
+ 1
+ );
+ expect(metrics.srm.toString().split(".")[1]?.length).toBeLessThanOrEqual(
+ 1
+ );
+ });
+
+ it("should handle recipes with other fermentables (sugars/extracts)", () => {
+ const recipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "DME",
+ type: "other",
+ amount: 5,
+ unit: "lb",
+ potential: 42,
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(recipe);
+
+ expect(metrics.og).toBeGreaterThan(1.0);
+ // SRM should be 2 (default for no grains)
+ expect(metrics.srm).toBe(2);
+ });
+
+ it("should clamp SRM to valid range [1, 60]", () => {
+ const darkRecipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "Chocolate Malt",
+ type: "grain",
+ amount: 20,
+ unit: "lb",
+ potential: 35,
+ color: 350, // Very dark
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(darkRecipe);
+
+ expect(metrics.srm).toBeLessThanOrEqual(60);
+ expect(metrics.srm).toBeGreaterThanOrEqual(1);
+ });
+
+ it("should average attenuation when multiple yeasts are present", () => {
+ const recipe: RecipeMetricsInput = {
+ batch_size: 5,
+ batch_size_unit: "gal",
+ efficiency: 75,
+ boil_time: 60,
+ mash_temp_unit: "F",
+ mash_temperature: 152,
+ ingredients: [
+ {
+ id: "1",
+ name: "Pale Malt",
+ type: "grain",
+ amount: 10,
+ unit: "lb",
+ potential: 37,
+ },
+ {
+ id: "2",
+ name: "US-05",
+ type: "yeast",
+ amount: 1,
+ unit: "pkg",
+ attenuation: 75,
+ },
+ {
+ id: "3",
+ name: "WLP001",
+ type: "yeast",
+ amount: 1,
+ unit: "pkg",
+ attenuation: 77,
+ },
+ ],
+ };
+
+ const metrics = OfflineMetricsCalculator.calculateMetrics(recipe);
+
+ // Should use average attenuation (75 + 77) / 2 = 76
+ expect(metrics.fg).toBeLessThan(metrics.og);
+ expect(metrics.abv).toBeGreaterThan(0);
+ });
+ });
+});
diff --git a/tests/src/components/recipes/RecipeForm/ParametersForm.test.tsx b/tests/src/components/recipes/RecipeForm/ParametersForm.test.tsx
index eb3555c2..1775fb63 100644
--- a/tests/src/components/recipes/RecipeForm/ParametersForm.test.tsx
+++ b/tests/src/components/recipes/RecipeForm/ParametersForm.test.tsx
@@ -1,7 +1,7 @@
import React from "react";
import { render, fireEvent, waitFor } from "@testing-library/react-native";
import { ParametersForm } from "@src/components/recipes/RecipeForm/ParametersForm";
-import { RecipeFormData } from "@src/types";
+import { RecipeFormData, UnitSystem } from "@src/types";
import { TEST_IDS } from "@src/constants/testIDs";
// Comprehensive React Native mocking to avoid ES6 module issues
@@ -42,7 +42,7 @@ jest.mock("@contexts/ThemeContext", () => ({
// Mock unit context
const mockUnitContext = {
- unitSystem: "imperial" as "imperial" | "metric",
+ unitSystem: "imperial" as UnitSystem,
setUnitSystem: jest.fn(),
};
diff --git a/tests/src/contexts/CalculatorsContext.test.tsx b/tests/src/contexts/CalculatorsContext.test.tsx
index 2c9d206c..794c2fa9 100644
--- a/tests/src/contexts/CalculatorsContext.test.tsx
+++ b/tests/src/contexts/CalculatorsContext.test.tsx
@@ -12,8 +12,6 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
import {
CalculatorsProvider,
useCalculators,
- CalculatorState,
- CalculatorAction,
} from "../../../src/contexts/CalculatorsContext";
// Mock AsyncStorage
@@ -58,13 +56,13 @@ describe("CalculatorsContext", () => {
expect(state.abv.result).toBeNull();
// Test Strike Water calculator defaults
- expect(state.strikeWater.tempUnit).toBe("f");
+ expect(state.strikeWater.tempUnit).toBe("F");
expect(state.strikeWater.waterToGrainRatio).toBe("1.25");
expect(state.strikeWater.result).toBeNull();
// Test Hydrometer Correction defaults
expect(state.hydrometerCorrection.calibrationTemp).toBe("60");
- expect(state.hydrometerCorrection.tempUnit).toBe("f");
+ expect(state.hydrometerCorrection.tempUnit).toBe("F");
expect(state.hydrometerCorrection.result).toBeNull();
// Test Boil Timer defaults
@@ -142,7 +140,7 @@ describe("CalculatorsContext", () => {
grainWeightUnit: "kg",
grainTemp: "20",
targetMashTemp: "67",
- tempUnit: "c",
+ tempUnit: "C",
result: { strikeTemp: 72.5, waterVolume: 12.5 },
},
});
@@ -151,7 +149,7 @@ describe("CalculatorsContext", () => {
const { strikeWater } = result.current.state;
expect(strikeWater.grainWeight).toBe("10");
expect(strikeWater.grainWeightUnit).toBe("kg");
- expect(strikeWater.tempUnit).toBe("c");
+ expect(strikeWater.tempUnit).toBe("C");
expect(strikeWater.result).toEqual({
strikeTemp: 72.5,
waterVolume: 12.5,
@@ -170,7 +168,7 @@ describe("CalculatorsContext", () => {
measuredGravity: "1.050",
wortTemp: "80",
calibrationTemp: "68",
- tempUnit: "f",
+ tempUnit: "F",
result: 1.052,
},
});
@@ -458,7 +456,7 @@ describe("CalculatorsContext", () => {
},
preferences: {
defaultUnits: "metric",
- temperatureUnit: "c",
+ temperatureUnit: "C",
saveHistory: true,
},
},
@@ -477,7 +475,7 @@ describe("CalculatorsContext", () => {
expect(state.preferences.defaultUnits).toBe("metric");
// Should preserve other calculator states
- expect(state.strikeWater.tempUnit).toBe("f"); // Default preserved
+ expect(state.strikeWater.tempUnit).toBe("F"); // Default preserved
});
it("should handle partial nested state updates", () => {
@@ -562,7 +560,7 @@ describe("CalculatorsContext", () => {
},
preferences: {
defaultUnits: "metric" as const,
- temperatureUnit: "c" as const,
+ temperatureUnit: "C" as const,
saveHistory: false,
},
};
@@ -581,7 +579,7 @@ describe("CalculatorsContext", () => {
expect(result.current.state.abv.originalGravity).toBe("1.060");
expect(result.current.state.abv.formula).toBe("advanced");
expect(result.current.state.preferences.defaultUnits).toBe("metric");
- expect(result.current.state.preferences.temperatureUnit).toBe("c");
+ expect(result.current.state.preferences.temperatureUnit).toBe("C");
});
it("should handle corrupted persisted state gracefully", async () => {
@@ -595,7 +593,8 @@ describe("CalculatorsContext", () => {
// Should fall back to default state
expect(result.current.state.abv.formula).toBe("simple");
- expect(result.current.state.preferences.defaultUnits).toBe("imperial");
+ expect(result.current.state.preferences.defaultUnits).toBe("metric");
+ expect(result.current.state.preferences.temperatureUnit).toBe("C");
});
it("should persist state changes to AsyncStorage after hydration", async () => {
diff --git a/tests/src/contexts/DeveloperContext.test.tsx b/tests/src/contexts/DeveloperContext.test.tsx
index 3f02cef4..b7a4d4b5 100644
--- a/tests/src/contexts/DeveloperContext.test.tsx
+++ b/tests/src/contexts/DeveloperContext.test.tsx
@@ -11,6 +11,7 @@ import {
NetworkSimulationMode,
} from "@contexts/DeveloperContext";
import { Text, TouchableOpacity } from "react-native";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
// Mock AsyncStorage
jest.mock("@react-native-async-storage/async-storage", () => ({
@@ -26,6 +27,16 @@ jest.mock("@services/config", () => ({
},
}));
+// Mock UnifiedLogger
+jest.mock("@services/logger/UnifiedLogger", () => ({
+ UnifiedLogger: {
+ warn: jest.fn(),
+ error: jest.fn(),
+ info: jest.fn(),
+ debug: jest.fn(),
+ },
+}));
+
const mockAsyncStorage = AsyncStorage as jest.Mocked;
// Test component that uses the developer context
@@ -147,8 +158,6 @@ describe("DeveloperContext", () => {
mockAsyncStorage.getItem.mockRejectedValueOnce(
new Error("Storage error")
);
- const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
-
const { getByTestId } = render(
@@ -157,13 +166,12 @@ describe("DeveloperContext", () => {
await waitFor(() => {
expect(getByTestId("network-mode")).toHaveTextContent("normal");
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.warn).toHaveBeenCalledWith(
+ "developer",
"Failed to load developer settings:",
expect.any(Error)
);
});
-
- consoleSpy.mockRestore();
});
});
@@ -193,8 +201,6 @@ describe("DeveloperContext", () => {
mockAsyncStorage.setItem.mockRejectedValueOnce(
new Error("Storage error")
);
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
-
const { getByTestId } = render(
@@ -206,13 +212,12 @@ describe("DeveloperContext", () => {
});
await waitFor(() => {
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ expect.any(String),
"Failed to set network simulation mode:",
expect.any(Error)
);
});
-
- consoleSpy.mockRestore();
});
});
@@ -309,8 +314,6 @@ describe("DeveloperContext", () => {
mockAsyncStorage.removeItem.mockRejectedValueOnce(
new Error("Reset error")
);
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
-
const { getByTestId } = render(
@@ -322,13 +325,12 @@ describe("DeveloperContext", () => {
});
await waitFor(() => {
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ expect.any(String),
"Failed to reset developer settings:",
expect.any(Error)
);
});
-
- consoleSpy.mockRestore();
});
});
@@ -344,50 +346,4 @@ describe("DeveloperContext", () => {
}).toThrow("useDeveloper must be used within a DeveloperProvider");
});
});
-
- describe("Console Logging", () => {
- it("should log network mode changes", async () => {
- const consoleSpy = jest.spyOn(console, "log").mockImplementation();
-
- const { getByTestId } = render(
-
-
-
- );
-
- await act(async () => {
- getByTestId("set-slow").props.onPress();
- });
-
- await waitFor(() => {
- expect(consoleSpy).toHaveBeenCalledWith(
- 'Developer mode: Network simulation set to "slow"'
- );
- });
-
- consoleSpy.mockRestore();
- });
-
- it("should log settings reset", async () => {
- const consoleSpy = jest.spyOn(console, "log").mockImplementation();
-
- const { getByTestId } = render(
-
-
-
- );
-
- await act(async () => {
- getByTestId("reset-settings").props.onPress();
- });
-
- await waitFor(() => {
- expect(consoleSpy).toHaveBeenCalledWith(
- "Developer settings reset to defaults"
- );
- });
-
- consoleSpy.mockRestore();
- });
- });
});
diff --git a/tests/src/contexts/UnitContext.test.tsx b/tests/src/contexts/UnitContext.test.tsx
index 83db2753..f48b2966 100644
--- a/tests/src/contexts/UnitContext.test.tsx
+++ b/tests/src/contexts/UnitContext.test.tsx
@@ -4,6 +4,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
import { UnitProvider, useUnits } from "@contexts/UnitContext";
import { AuthProvider } from "@contexts/AuthContext";
import { UnitSystem } from "@src/types";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
// Mock AsyncStorage
jest.mock("@react-native-async-storage/async-storage", () => ({
@@ -22,6 +23,16 @@ jest.mock("@services/api/apiService", () => ({
},
}));
+// Mock UnifiedLogger
+jest.mock("@services/logger/UnifiedLogger", () => ({
+ UnifiedLogger: {
+ error: jest.fn(),
+ warn: jest.fn(),
+ info: jest.fn(),
+ debug: jest.fn(),
+ },
+}));
+
const mockAsyncStorage = AsyncStorage as jest.Mocked;
describe("UnitContext", () => {
@@ -31,40 +42,39 @@ describe("UnitContext", () => {
mockAsyncStorage.setItem.mockResolvedValue();
});
- const createWrapper =
- (initialUnitSystem?: UnitSystem, isAuthenticated = false) =>
- ({ children }: { children: React.ReactNode }) =>
- React.createElement(AuthProvider, {
- initialAuthState: {
- user: isAuthenticated
- ? {
- id: "test",
- username: "test",
- email: "test@example.com",
- email_verified: true,
- created_at: "2023-01-01T00:00:00Z",
- updated_at: "2023-01-01T00:00:00Z",
- is_active: true,
- }
- : null,
- },
- children: React.createElement(UnitProvider, {
- initialUnitSystem,
- children,
- }),
- });
+ const createWrapper = (
+ initialUnitSystem?: UnitSystem,
+ isAuthenticated = false
+ ) =>
+ function TestWrapper({ children }: { children: React.ReactNode }) {
+ const initialAuthState = {
+ user: isAuthenticated
+ ? {
+ id: "test",
+ username: "test",
+ email: "test@example.com",
+ email_verified: true,
+ created_at: "2023-01-01T00:00:00Z",
+ updated_at: "2023-01-01T00:00:00Z",
+ is_active: true,
+ }
+ : null,
+ };
+
+ return (
+
+
+ {children}
+
+
+ );
+ };
describe("useUnits hook", () => {
it("should throw error when used outside provider", () => {
- const consoleSpy = jest
- .spyOn(console, "error")
- .mockImplementation(() => {});
-
expect(() => {
renderHook(() => useUnits());
}).toThrow("useUnits must be used within a UnitProvider");
-
- consoleSpy.mockRestore();
});
it("should provide unit context when used within provider", () => {
@@ -220,7 +230,7 @@ describe("UnitContext", () => {
expect(result.current.getPreferredUnit("weight")).toBe("lb");
expect(result.current.getPreferredUnit("volume")).toBe("gal");
- expect(result.current.getPreferredUnit("temperature")).toBe("f");
+ expect(result.current.getPreferredUnit("temperature")).toBe("F");
});
it("should return correct preferred units for metric system", () => {
@@ -229,7 +239,7 @@ describe("UnitContext", () => {
expect(result.current.getPreferredUnit("weight")).toBe("kg");
expect(result.current.getPreferredUnit("volume")).toBe("l");
- expect(result.current.getPreferredUnit("temperature")).toBe("c");
+ expect(result.current.getPreferredUnit("temperature")).toBe("C");
});
});
@@ -296,8 +306,8 @@ describe("UnitContext", () => {
expect(result.current.formatValue(0.5, "lb", "weight")).toBe("0.5 lb");
// Temperature should always use 1 decimal place
- expect(result.current.formatValue(20.555, "c", "temperature")).toBe(
- "20.6 c"
+ expect(result.current.formatValue(20.555, "C", "temperature")).toBe(
+ "20.6 C"
);
// Small volume should use 2 decimal places when < 1
@@ -423,18 +433,13 @@ describe("UnitContext", () => {
const wrapper = createWrapper("metric");
const { result } = renderHook(() => useUnits(), { wrapper });
- const consoleSpy = jest
- .spyOn(console, "warn")
- .mockImplementation(() => {});
-
const result_conv = result.current.convertUnit(100, "unknown", "kg");
expect(result_conv.value).toBe(100);
expect(result_conv.unit).toBe("unknown");
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.warn).toHaveBeenCalledWith(
+ "units",
"No conversion available from unknown to kg"
);
-
- consoleSpy.mockRestore();
});
it("should handle invalid string inputs in convertUnit", () => {
@@ -502,9 +507,6 @@ describe("UnitContext", () => {
});
it("should handle unsupported unit conversions with warning", () => {
- const consoleSpy = jest
- .spyOn(console, "warn")
- .mockImplementation(() => {});
const wrapper = createWrapper("metric");
const { result } = renderHook(() => useUnits(), { wrapper });
@@ -517,11 +519,10 @@ describe("UnitContext", () => {
expect(invalidResult.value).toBe(100);
expect(invalidResult.unit).toBe("unknown_unit");
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.warn).toHaveBeenCalledWith(
+ "units",
"No conversion available from unknown_unit to another_unit"
);
-
- consoleSpy.mockRestore();
});
});
@@ -738,7 +739,7 @@ describe("UnitContext", () => {
const tempUnits = imperialResult.current.getCommonUnits("temperature");
expect(tempUnits.length).toBe(1);
- expect(tempUnits[0].value).toBe("f");
+ expect(tempUnits[0].value).toBe("F");
expect(tempUnits[0].label).toBe("°F");
const metricWrapper = createWrapper("metric");
@@ -749,7 +750,7 @@ describe("UnitContext", () => {
const metricTempUnits =
metricResult.current.getCommonUnits("temperature");
expect(metricTempUnits.length).toBe(1);
- expect(metricTempUnits[0].value).toBe("c");
+ expect(metricTempUnits[0].value).toBe("C");
expect(metricTempUnits[0].label).toBe("°C");
});
@@ -794,8 +795,8 @@ describe("UnitContext", () => {
const wrapper = createWrapper("metric");
const { result } = renderHook(() => useUnits(), { wrapper });
- expect(result.current.formatValue(20.555, "c", "temperature")).toBe(
- "20.6 c"
+ expect(result.current.formatValue(20.555, "C", "temperature")).toBe(
+ "20.6 C"
);
});
@@ -953,9 +954,6 @@ describe("UnitContext", () => {
it("should handle background fetch errors gracefully", async () => {
const ApiService = require("@services/api/apiService").default;
- const consoleSpy = jest
- .spyOn(console, "warn")
- .mockImplementation(() => {});
// Mock cached settings
const cachedSettings = { preferred_units: "imperial" };
@@ -977,12 +975,11 @@ describe("UnitContext", () => {
// Should still use cached settings despite background error
expect(result.current.unitSystem).toBe("imperial");
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.warn).toHaveBeenCalledWith(
+ "units",
"Background settings fetch failed:",
expect.any(Error)
);
-
- consoleSpy.mockRestore();
});
});
@@ -1030,9 +1027,6 @@ describe("UnitContext", () => {
it("should handle updateUnitSystem error and revert", async () => {
const ApiService = require("@services/api/apiService").default;
- const consoleSpy = jest
- .spyOn(console, "error")
- .mockImplementation(() => {});
// Mock AsyncStorage.setItem to fail, which will trigger error for unauthenticated users
mockAsyncStorage.setItem.mockRejectedValue(new Error("Storage Error"));
@@ -1047,12 +1041,11 @@ describe("UnitContext", () => {
// Should revert to original system on error
expect(result.current.unitSystem).toBe("imperial");
expect(result.current.error).toBe("Failed to save unit preference");
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ expect.any(String),
"Failed to update unit system:",
expect.any(Error)
);
-
- consoleSpy.mockRestore();
});
});
@@ -1088,18 +1081,18 @@ describe("UnitContext", () => {
const wrapper = createWrapper("metric");
const { result } = renderHook(() => useUnits(), { wrapper });
- const conversion = result.current.convertUnit(32, "f", "c");
+ const conversion = result.current.convertUnit(32, "F", "C");
expect(conversion.value).toBeCloseTo(0, 1);
- expect(conversion.unit).toBe("c");
+ expect(conversion.unit).toBe("C");
});
it("should handle c to f temperature conversion", () => {
const wrapper = createWrapper("imperial");
const { result } = renderHook(() => useUnits(), { wrapper });
- const conversion = result.current.convertUnit(0, "c", "f");
+ const conversion = result.current.convertUnit(0, "C", "F");
expect(conversion.value).toBeCloseTo(32, 1);
- expect(conversion.unit).toBe("f");
+ expect(conversion.unit).toBe("F");
});
});
});
diff --git a/tests/src/hooks/useRecipeMetrics.test.ts b/tests/src/hooks/useRecipeMetrics.test.ts
index e0232f54..2c0e45e3 100644
--- a/tests/src/hooks/useRecipeMetrics.test.ts
+++ b/tests/src/hooks/useRecipeMetrics.test.ts
@@ -1,14 +1,12 @@
/* eslint-disable import/first */
-import { renderHook, waitFor, act } from "@testing-library/react-native";
+import { renderHook, waitFor } from "@testing-library/react-native";
import React from "react";
-// Mock the API service before importing anything that uses it
-jest.mock("@services/api/apiService", () => ({
- __esModule: true,
- default: {
- recipes: {
- calculateMetricsPreview: jest.fn(),
- },
+// Mock the OfflineMetricsCalculator
+jest.mock("@services/brewing/OfflineMetricsCalculator", () => ({
+ OfflineMetricsCalculator: {
+ validateRecipeData: jest.fn(),
+ calculateMetrics: jest.fn(),
},
}));
@@ -18,18 +16,9 @@ jest.unmock("@tanstack/react-query");
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useRecipeMetrics } from "@src/hooks/useRecipeMetrics";
import { RecipeFormData, RecipeMetrics, RecipeIngredient } from "@src/types";
-import ApiService from "@services/api/apiService";
-
-const mockedApiService = jest.mocked(ApiService);
+import { OfflineMetricsCalculator } from "@services/brewing/OfflineMetricsCalculator";
-// Helper to create mock AxiosResponse
-const createMockAxiosResponse = (data: T) => ({
- data,
- status: 200,
- statusText: "OK",
- headers: {},
- config: {} as any,
-});
+const mockedCalculator = jest.mocked(OfflineMetricsCalculator);
const createTestQueryClient = () =>
new QueryClient({
@@ -109,9 +98,7 @@ describe("useRecipeMetrics - Essential Tests", () => {
// Should remain disabled
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
- expect(
- mockedApiService.recipes.calculateMetricsPreview
- ).not.toHaveBeenCalled();
+ expect(mockedCalculator.calculateMetrics).not.toHaveBeenCalled();
});
it("should not enable query when batch_size is zero", () => {
@@ -125,9 +112,7 @@ describe("useRecipeMetrics - Essential Tests", () => {
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
- expect(
- mockedApiService.recipes.calculateMetricsPreview
- ).not.toHaveBeenCalled();
+ expect(mockedCalculator.calculateMetrics).not.toHaveBeenCalled();
});
it("should respect explicit enabled parameter", () => {
@@ -141,9 +126,7 @@ describe("useRecipeMetrics - Essential Tests", () => {
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
- expect(
- mockedApiService.recipes.calculateMetricsPreview
- ).not.toHaveBeenCalled();
+ expect(mockedCalculator.calculateMetrics).not.toHaveBeenCalled();
});
it("should filter out NaN and invalid numeric values", async () => {
@@ -158,9 +141,11 @@ describe("useRecipeMetrics - Essential Tests", () => {
} as any,
};
- mockedApiService.recipes.calculateMetricsPreview.mockResolvedValue(
- createMockAxiosResponse(mockMetricsResponse.data)
- );
+ mockedCalculator.validateRecipeData.mockReturnValue({
+ isValid: true,
+ errors: [],
+ });
+ mockedCalculator.calculateMetrics.mockReturnValue(mockMetricsResponse.data);
const wrapper = createWrapper(queryClient);
const { result } = renderHook(() => useRecipeMetrics(mockRecipeData), {
@@ -182,13 +167,13 @@ describe("useRecipeMetrics - Essential Tests", () => {
it("should handle empty metrics response gracefully", async () => {
const mockRecipeData = createMockRecipeData();
- const mockMetricsResponse = {
- data: {} as RecipeMetrics,
- };
+ const mockMetricsResponse = {} as RecipeMetrics;
- mockedApiService.recipes.calculateMetricsPreview.mockResolvedValue(
- createMockAxiosResponse(mockMetricsResponse.data)
- );
+ mockedCalculator.validateRecipeData.mockReturnValue({
+ isValid: true,
+ errors: [],
+ });
+ mockedCalculator.calculateMetrics.mockReturnValue(mockMetricsResponse);
const wrapper = createWrapper(queryClient);
const { result } = renderHook(() => useRecipeMetrics(mockRecipeData), {
@@ -240,87 +225,33 @@ describe("useRecipeMetrics - Essential Tests", () => {
],
};
- // Mock successful API response
- const mockResponse = {
- data: {
- og: 1.05,
- fg: 1.012,
- abv: 5.0,
- ibu: 30,
- srm: 6,
- },
- status: 200,
- statusText: "OK",
- headers: {},
- config: {} as any,
- };
- mockedApiService.recipes.calculateMetricsPreview.mockResolvedValue(
- mockResponse
- );
-
- const wrapper = createWrapper(queryClient);
- const { result } = renderHook(() => useRecipeMetrics(mockRecipeData), {
- wrapper,
- });
-
- // Test should complete without errors despite null/undefined id/name fields
- expect(() => result.current).not.toThrow();
- });
-
- it("should handle API validation errors by falling back to offline calculation", async () => {
- const mockRecipeData = createMockRecipeData();
- const validationError = {
- response: { status: 400 },
- message: "Invalid recipe data",
+ // Mock successful metrics calculation
+ const mockMetrics: RecipeMetrics = {
+ og: 1.05,
+ fg: 1.012,
+ abv: 5.0,
+ ibu: 30,
+ srm: 6,
};
- mockedApiService.recipes.calculateMetricsPreview.mockRejectedValue(
- validationError
- );
-
- const wrapper = createWrapper(queryClient);
- const { result } = renderHook(() => useRecipeMetrics(mockRecipeData), {
- wrapper,
- });
- await waitFor(() => {
- expect(result.current.isSuccess).toBe(true);
+ mockedCalculator.validateRecipeData.mockReturnValue({
+ isValid: true,
+ errors: [],
});
-
- // Should have data from offline calculation fallback
- expect(result.current.data).toBeDefined();
- expect(result.current.error).toBeNull();
- // Should only be called once (no retries for 400 errors)
- expect(
- mockedApiService.recipes.calculateMetricsPreview
- ).toHaveBeenCalledTimes(1);
- });
-
- it("should handle network errors by falling back to offline calculation", async () => {
- const mockRecipeData = createMockRecipeData();
- const networkError = {
- response: { status: 500 },
- message: "Network error",
- };
- mockedApiService.recipes.calculateMetricsPreview.mockRejectedValue(
- networkError
- );
+ mockedCalculator.calculateMetrics.mockReturnValue(mockMetrics);
const wrapper = createWrapper(queryClient);
const { result } = renderHook(() => useRecipeMetrics(mockRecipeData), {
wrapper,
});
- await waitFor(() => {
- expect(result.current.isSuccess).toBe(true);
- });
-
- // Should have data from offline calculation fallback
- expect(result.current.data).toBeDefined();
- expect(result.current.error).toBeNull();
- // Should only be called once since query function catches the error and falls back
- expect(
- mockedApiService.recipes.calculateMetricsPreview
- ).toHaveBeenCalledTimes(1);
+ // Test should complete without errors despite null/undefined id/name fields
+ expect(result.current).toBeDefined();
+ expect(result.current.isError).toBe(false);
+ // Verify the hook returns a valid query result object with expected properties
+ expect(result.current).toHaveProperty("data");
+ expect(result.current).toHaveProperty("isLoading");
+ expect(result.current).toHaveProperty("isError");
});
it("should handle complex recipe data with all ingredient types", async () => {
@@ -377,9 +308,11 @@ describe("useRecipeMetrics - Essential Tests", () => {
} as RecipeMetrics,
};
- mockedApiService.recipes.calculateMetricsPreview.mockResolvedValue(
- createMockAxiosResponse(mockMetricsResponse.data)
- );
+ mockedCalculator.validateRecipeData.mockReturnValue({
+ isValid: true,
+ errors: [],
+ });
+ mockedCalculator.calculateMetrics.mockReturnValue(mockMetricsResponse.data);
const wrapper = createWrapper(queryClient);
const { result } = renderHook(() => useRecipeMetrics(complexRecipeData), {
@@ -398,18 +331,10 @@ describe("useRecipeMetrics - Essential Tests", () => {
srm: 12,
});
- // Verify API was called with correct data structure
- expect(
- mockedApiService.recipes.calculateMetricsPreview
- ).toHaveBeenCalledWith({
- batch_size: complexRecipeData.batch_size,
- batch_size_unit: complexRecipeData.batch_size_unit,
- efficiency: complexRecipeData.efficiency,
- boil_time: complexRecipeData.boil_time,
- ingredients: complexRecipeData.ingredients,
- mash_temperature: complexRecipeData.mash_temperature,
- mash_temp_unit: complexRecipeData.mash_temp_unit,
- });
+ // Verify calculator was called with correct data
+ expect(mockedCalculator.calculateMetrics).toHaveBeenCalledWith(
+ complexRecipeData
+ );
});
it("should create correct query key with recipe parameters", async () => {
@@ -432,19 +357,19 @@ describe("useRecipeMetrics - Essential Tests", () => {
],
});
- const mockMetricsResponse = {
- data: {
- og: 1.055,
- fg: 1.012,
- abv: 5.6,
- ibu: 45,
- srm: 8,
- } as RecipeMetrics,
+ const mockMetricsResponse: RecipeMetrics = {
+ og: 1.055,
+ fg: 1.012,
+ abv: 5.6,
+ ibu: 45,
+ srm: 8,
};
- mockedApiService.recipes.calculateMetricsPreview.mockResolvedValue(
- createMockAxiosResponse(mockMetricsResponse.data)
- );
+ mockedCalculator.validateRecipeData.mockReturnValue({
+ isValid: true,
+ errors: [],
+ });
+ mockedCalculator.calculateMetrics.mockReturnValue(mockMetricsResponse);
const wrapper = createWrapper(queryClient);
const { result } = renderHook(() => useRecipeMetrics(mockRecipeData), {
@@ -463,8 +388,17 @@ describe("useRecipeMetrics - Essential Tests", () => {
);
expect(recipeMetricsQueries).toHaveLength(1);
- expect(recipeMetricsQueries[0].queryKey[3]).toBe(5.5); // batch_size
- expect(recipeMetricsQueries[0].queryKey[4]).toBe("gal"); // batch_size_unit
- expect(recipeMetricsQueries[0].queryKey[5]).toBe(72); // efficiency
+
+ // Query key structure from useRecipeMetrics.ts:40-50
+ // [0] = "recipeMetrics", [1] = "offline-first",
+ // [2] = batch_size, [3] = batch_size_unit, [4] = efficiency
+ const [queryName, mode, batchSize, batchSizeUnit, efficiency] =
+ recipeMetricsQueries[0].queryKey;
+
+ expect(queryName).toBe("recipeMetrics");
+ expect(mode).toBe("offline-first");
+ expect(batchSize).toBe(5.5);
+ expect(batchSizeUnit).toBe("gal");
+ expect(efficiency).toBe(72);
});
});
diff --git a/tests/src/services/NotificationService.test.ts b/tests/src/services/NotificationService.test.ts
index 73fb715f..c099986c 100644
--- a/tests/src/services/NotificationService.test.ts
+++ b/tests/src/services/NotificationService.test.ts
@@ -11,6 +11,7 @@ import {
import * as Notifications from "expo-notifications";
import * as Haptics from "expo-haptics";
import { Platform } from "react-native";
+import { UnifiedLogger } from "@services/logger/UnifiedLogger";
// Mock expo-notifications
jest.mock("expo-notifications", () => ({
@@ -52,6 +53,16 @@ jest.mock("react-native", () => ({
},
}));
+// Mock UnifiedLogger
+jest.mock("@/src/services/logger/UnifiedLogger", () => ({
+ UnifiedLogger: {
+ error: jest.fn(),
+ warn: jest.fn(),
+ info: jest.fn(),
+ debug: jest.fn(),
+ },
+}));
+
describe("NotificationService", () => {
const mockGetPermissions = Notifications.getPermissionsAsync as jest.Mock;
const mockRequestPermissions =
@@ -114,14 +125,13 @@ describe("NotificationService", () => {
mockGetPermissions.mockResolvedValue({ status: "denied" });
mockRequestPermissions.mockResolvedValue({ status: "denied" });
- const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
const result = await NotificationService.initialize();
expect(result).toBe(false);
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.warn).toHaveBeenCalledWith(
+ "notifications",
"Notification permissions not granted"
);
- consoleSpy.mockRestore();
});
it("should set up Android notification channel", async () => {
@@ -156,16 +166,14 @@ describe("NotificationService", () => {
it("should handle initialization errors", async () => {
mockGetPermissions.mockRejectedValue(new Error("Permission error"));
-
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
const result = await NotificationService.initialize();
expect(result).toBe(false);
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ "notifications",
"Failed to initialize notifications:",
expect.any(Error)
);
- consoleSpy.mockRestore();
});
});
@@ -221,8 +229,6 @@ describe("NotificationService", () => {
it("should handle scheduling errors", async () => {
mockGetPermissions.mockResolvedValue({ status: "granted" });
mockScheduleNotification.mockRejectedValue(new Error("Scheduling error"));
-
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
const identifier = await NotificationService.scheduleHopAlert(
"Cascade",
1,
@@ -231,11 +237,11 @@ describe("NotificationService", () => {
);
expect(identifier).toBeNull();
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ "notifications",
"Failed to schedule hop alert:",
expect.any(Error)
);
- consoleSpy.mockRestore();
});
});
@@ -372,15 +378,13 @@ describe("NotificationService", () => {
it("should handle cancellation errors", async () => {
mockCancelAll.mockRejectedValue(new Error("Cancel error"));
-
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
await NotificationService.cancelAllAlerts();
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ "notifications",
"Failed to cancel notifications:",
expect.any(Error)
);
- consoleSpy.mockRestore();
});
});
@@ -402,15 +406,13 @@ describe("NotificationService", () => {
it("should handle cancellation errors", async () => {
mockCancelSpecific.mockRejectedValue(new Error("Cancel specific error"));
-
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
await NotificationService.cancelAlert("test-id");
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ "notifications",
"Failed to cancel notification:",
expect.any(Error)
);
- consoleSpy.mockRestore();
});
});
@@ -435,15 +437,13 @@ describe("NotificationService", () => {
it("should handle haptic feedback errors", async () => {
mockHapticImpact.mockRejectedValue(new Error("Haptic error"));
-
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
await NotificationService.triggerHapticFeedback("medium");
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ "notifications",
"Failed to trigger haptic feedback:",
expect.any(Error)
);
- consoleSpy.mockRestore();
});
});
@@ -514,16 +514,14 @@ describe("NotificationService", () => {
it("should handle permission check errors", async () => {
mockGetPermissions.mockRejectedValue(new Error("Permission check error"));
-
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
const result = await NotificationService.areNotificationsEnabled();
expect(result).toBe(false);
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ "notifications",
"Failed to check notification permissions:",
expect.any(Error)
);
- consoleSpy.mockRestore();
});
});
@@ -543,16 +541,14 @@ describe("NotificationService", () => {
it("should return empty array on error", async () => {
mockGetScheduled.mockRejectedValue(new Error("Get scheduled error"));
-
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
const result = await NotificationService.getScheduledNotifications();
expect(result).toEqual([]);
- expect(consoleSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.error).toHaveBeenCalledWith(
+ "notifications",
"Failed to get scheduled notifications:",
expect.any(Error)
);
- consoleSpy.mockRestore();
});
});
@@ -613,25 +609,20 @@ describe("NotificationService", () => {
expect(result.size).toBe(0); // No successful schedules
});
- it("should log debug information in development", async () => {
+ it("should log a warning in development when skipping hop alerts", async () => {
const originalDev = (global as any).__DEV__;
(global as any).__DEV__ = true;
- const consoleSpy = jest.spyOn(console, "log").mockImplementation();
- const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
-
await NotificationService.scheduleHopAlertsForRecipe(
[{ time: 3, name: "Very Late Hop", amount: 0.5, unit: "oz" }],
90 // 1.5 minute boil (90s)
);
// 90s boil - 3min hop (180s) - 30s = -120s (negative, so skipped with warning)
- expect(consoleWarnSpy).toHaveBeenCalledWith(
+ expect(UnifiedLogger.warn).toHaveBeenCalledWith(
+ "notifications",
expect.stringContaining('⚠️ Skipping hop alert for "Very Late Hop"')
);
-
- consoleSpy.mockRestore();
- consoleWarnSpy.mockRestore();
(global as any).__DEV__ = originalDev;
});
});
diff --git a/tests/src/services/calculators/HydrometerCorrectionCalculator.test.ts b/tests/src/services/calculators/HydrometerCorrectionCalculator.test.ts
index c6106aad..d85c8a45 100644
--- a/tests/src/services/calculators/HydrometerCorrectionCalculator.test.ts
+++ b/tests/src/services/calculators/HydrometerCorrectionCalculator.test.ts
@@ -10,10 +10,10 @@ import { HydrometerCorrectionCalculator } from "@services/calculators/Hydrometer
jest.mock("@services/calculators/UnitConverter", () => ({
UnitConverter: {
convertTemperature: jest.fn((value, from, to) => {
- if (from === "c" && to === "f") {
+ if (from === "C" && to === "F") {
return (value * 9) / 5 + 32;
}
- if (from === "f" && to === "c") {
+ if (from === "F" && to === "C") {
return ((value - 32) * 5) / 9;
}
return value;
@@ -28,7 +28,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05, // measured gravity
60, // wort temp (at calibration)
60, // calibration temp
- "f"
+ "F"
);
expect(result.correctedGravity).toBeCloseTo(1.05, 3);
@@ -42,7 +42,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05, // measured gravity
80, // wort temp (higher than calibration)
60, // calibration temp
- "f"
+ "F"
);
expect(result.correctedGravity).toBeGreaterThan(1.05); // Should increase at higher temp
@@ -54,7 +54,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05, // measured gravity
40, // wort temp (lower than calibration)
60, // calibration temp
- "f"
+ "F"
);
expect(result.correctedGravity).toBeLessThan(1.05); // Should decrease at lower temp
@@ -66,7 +66,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05,
20, // 20°C
15.5, // 15.5°C (standard calibration)
- "c"
+ "C"
);
expect(result.correctedGravity).toBeGreaterThan(1.05);
@@ -79,7 +79,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05678,
75,
60,
- "f"
+ "F"
);
// Check that results are properly rounded
@@ -93,31 +93,31 @@ describe("HydrometerCorrectionCalculator", () => {
it("should throw error for invalid gravity", () => {
expect(() => {
- HydrometerCorrectionCalculator.calculateCorrection(0.98, 60, 60, "f");
+ HydrometerCorrectionCalculator.calculateCorrection(0.98, 60, 60, "F");
}).toThrow("Measured gravity must be between 0.990 and 1.200");
expect(() => {
- HydrometerCorrectionCalculator.calculateCorrection(1.25, 60, 60, "f");
+ HydrometerCorrectionCalculator.calculateCorrection(1.25, 60, 60, "F");
}).toThrow("Measured gravity must be between 0.990 and 1.200");
});
it("should throw error for invalid Fahrenheit temperatures", () => {
expect(() => {
- HydrometerCorrectionCalculator.calculateCorrection(1.05, 30, 60, "f"); // Below freezing
+ HydrometerCorrectionCalculator.calculateCorrection(1.05, 30, 60, "F"); // Below freezing
}).toThrow("Wort temperature must be between 32°F and 212°F");
expect(() => {
- HydrometerCorrectionCalculator.calculateCorrection(1.05, 60, 250, "f"); // Above boiling
+ HydrometerCorrectionCalculator.calculateCorrection(1.05, 60, 250, "F"); // Above boiling
}).toThrow("Calibration temperature must be between 32°F and 212°F");
});
it("should throw error for invalid Celsius temperatures", () => {
expect(() => {
- HydrometerCorrectionCalculator.calculateCorrection(1.05, -5, 20, "c"); // Below freezing
+ HydrometerCorrectionCalculator.calculateCorrection(1.05, -5, 20, "C"); // Below freezing
}).toThrow("Wort temperature must be between 0°C and 100°C");
expect(() => {
- HydrometerCorrectionCalculator.calculateCorrection(1.05, 20, 110, "c"); // Above boiling
+ HydrometerCorrectionCalculator.calculateCorrection(1.05, 20, 110, "C"); // Above boiling
}).toThrow("Calibration temperature must be between 0°C and 100°C");
});
});
@@ -127,7 +127,7 @@ describe("HydrometerCorrectionCalculator", () => {
const result = HydrometerCorrectionCalculator.calculateCorrectionDefault(
1.05,
70,
- "f"
+ "F"
);
expect(result.calibrationTemp).toBe(60); // Default F calibration
@@ -138,7 +138,7 @@ describe("HydrometerCorrectionCalculator", () => {
const result = HydrometerCorrectionCalculator.calculateCorrectionDefault(
1.05,
20,
- "c"
+ "C"
);
expect(result.calibrationTemp).toBe(15.5); // Default C calibration
@@ -152,7 +152,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05, // measured
1.052, // target (higher)
60, // calibration
- "f"
+ "F"
);
expect(targetTemp).toBeGreaterThan(60); // Should be higher temp
@@ -163,7 +163,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05,
targetTemp,
60,
- "f"
+ "F"
);
expect(verification.correctedGravity).toBeCloseTo(1.052, 2);
});
@@ -173,7 +173,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05,
1.048, // target (lower)
15.5,
- "c"
+ "C"
);
expect(targetTemp).toBeLessThan(15.5); // Should be lower temp
@@ -186,7 +186,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05,
2.0, // Impossible target
60,
- "f"
+ "F"
);
}).toThrow("Could not converge on solution - check input values");
});
@@ -197,7 +197,7 @@ describe("HydrometerCorrectionCalculator", () => {
const table = HydrometerCorrectionCalculator.getCorrectionTable(
1.05,
60,
- "f"
+ "F"
);
expect(table.length).toBeGreaterThan(10);
@@ -221,7 +221,7 @@ describe("HydrometerCorrectionCalculator", () => {
const table = HydrometerCorrectionCalculator.getCorrectionTable(
1.05,
15.5,
- "c"
+ "C"
);
expect(table.length).toBeGreaterThan(10);
@@ -252,7 +252,7 @@ describe("HydrometerCorrectionCalculator", () => {
HydrometerCorrectionCalculator.isCorrectionSignificant(
60, // wort temp
60, // calibration temp
- "f"
+ "F"
);
expect(isSignificant).toBe(false);
@@ -263,7 +263,7 @@ describe("HydrometerCorrectionCalculator", () => {
HydrometerCorrectionCalculator.isCorrectionSignificant(
80, // wort temp (20F higher)
60, // calibration temp
- "f"
+ "F"
);
expect(isSignificant).toBe(true);
@@ -274,7 +274,7 @@ describe("HydrometerCorrectionCalculator", () => {
HydrometerCorrectionCalculator.isCorrectionSignificant(
62, // wort temp (small difference)
60, // calibration temp
- "f"
+ "F"
);
expect(isSignificant).toBe(false);
@@ -285,7 +285,7 @@ describe("HydrometerCorrectionCalculator", () => {
HydrometerCorrectionCalculator.isCorrectionSignificant(
25, // 10C higher
15.5,
- "c"
+ "C"
);
expect(isSignificant).toBe(true);
@@ -304,17 +304,17 @@ describe("HydrometerCorrectionCalculator", () => {
// Check Standard hydrometer calibration
const standard = calibrationTemps["Standard"];
- expect(standard.f).toBe(60);
- expect(standard.c).toBe(15.5);
+ expect(standard.F).toBe(60);
+ expect(standard.C).toBe(15.5);
// Check that all entries have both F and C values
Object.values(calibrationTemps).forEach(temp => {
- expect(temp).toHaveProperty("f");
- expect(temp).toHaveProperty("c");
- expect(temp.f).toBeGreaterThan(30);
- expect(temp.f).toBeLessThan(80);
- expect(temp.c).toBeGreaterThan(10);
- expect(temp.c).toBeLessThan(30);
+ expect(temp).toHaveProperty("F");
+ expect(temp).toHaveProperty("C");
+ expect(temp.F).toBeGreaterThan(30);
+ expect(temp.F).toBeLessThan(80);
+ expect(temp.C).toBeGreaterThan(10);
+ expect(temp.C).toBeLessThan(30);
});
});
});
@@ -325,7 +325,7 @@ describe("HydrometerCorrectionCalculator", () => {
0.995,
70,
60,
- "f"
+ "F"
);
expect(lowGravity.correctedGravity).toBeGreaterThan(0.995);
@@ -333,7 +333,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.15,
70,
60,
- "f"
+ "F"
);
expect(highGravity.correctedGravity).toBeGreaterThan(1.15);
});
@@ -343,7 +343,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05,
200, // Very hot
60,
- "f"
+ "F"
);
expect(extremeHot.correctedGravity).toBeGreaterThan(1.05);
@@ -351,7 +351,7 @@ describe("HydrometerCorrectionCalculator", () => {
1.05,
35, // Very cold
60,
- "f"
+ "F"
);
expect(extremeCold.correctedGravity).toBeLessThan(1.05);
});
diff --git a/tests/src/services/calculators/PrimingSugarCalculator.test.ts b/tests/src/services/calculators/PrimingSugarCalculator.test.ts
index e94eccc8..15183642 100644
--- a/tests/src/services/calculators/PrimingSugarCalculator.test.ts
+++ b/tests/src/services/calculators/PrimingSugarCalculator.test.ts
@@ -27,7 +27,7 @@ jest.mock("@services/calculators/UnitConverter", () => ({
return value;
}),
convertTemperature: jest.fn((value, from, to) => {
- if (from === "c" && to === "f") {
+ if (from === "C" && to === "F") {
return (value * 9) / 5 + 32;
}
return value;
@@ -146,8 +146,8 @@ describe("PrimingSugarCalculator", () => {
describe("estimateResidualCO2", () => {
it("should estimate residual CO2 for common temperatures", () => {
- const co2At65F = PrimingSugarCalculator.estimateResidualCO2(65, "f");
- const co2At70F = PrimingSugarCalculator.estimateResidualCO2(70, "f");
+ const co2At65F = PrimingSugarCalculator.estimateResidualCO2(65, "F");
+ const co2At70F = PrimingSugarCalculator.estimateResidualCO2(70, "F");
expect(co2At65F).toBe(0.9);
expect(co2At70F).toBe(0.8);
@@ -155,21 +155,21 @@ describe("PrimingSugarCalculator", () => {
});
it("should handle Celsius temperatures", () => {
- const co2 = PrimingSugarCalculator.estimateResidualCO2(18, "c"); // ~65F
+ const co2 = PrimingSugarCalculator.estimateResidualCO2(18, "C"); // ~65F
expect(co2).toBeGreaterThan(0);
expect(co2).toBeLessThan(2);
});
it("should find closest temperature in lookup table", () => {
- const co2At67F = PrimingSugarCalculator.estimateResidualCO2(67, "f"); // Between 65 and 70
+ const co2At67F = PrimingSugarCalculator.estimateResidualCO2(67, "F"); // Between 65 and 70
// Should pick the closest value (65F = 0.9, 70F = 0.8, so 67 should be 0.9)
expect(co2At67F).toBe(0.9);
});
it("should handle extreme temperatures", () => {
- const co2Cold = PrimingSugarCalculator.estimateResidualCO2(20, "f"); // Very cold
- const co2Hot = PrimingSugarCalculator.estimateResidualCO2(90, "f"); // Very hot
+ const co2Cold = PrimingSugarCalculator.estimateResidualCO2(20, "F"); // Very cold
+ const co2Hot = PrimingSugarCalculator.estimateResidualCO2(90, "F"); // Very hot
expect(co2Cold).toBeGreaterThan(0);
expect(co2Hot).toBeGreaterThan(0);
diff --git a/tests/src/services/calculators/StrikeWaterCalculator.test.ts b/tests/src/services/calculators/StrikeWaterCalculator.test.ts
index b16b1f3f..0747da92 100644
--- a/tests/src/services/calculators/StrikeWaterCalculator.test.ts
+++ b/tests/src/services/calculators/StrikeWaterCalculator.test.ts
@@ -33,10 +33,10 @@ jest.mock("@services/calculators/UnitConverter", () => ({
return value;
}),
convertTemperature: jest.fn((value, from, to) => {
- if (from === "c" && to === "f") {
+ if (from === "C" && to === "F") {
return (value * 9) / 5 + 32;
}
- if (from === "f" && to === "c") {
+ if (from === "F" && to === "C") {
return ((value - 32) * 5) / 9;
}
return value;
@@ -53,7 +53,7 @@ describe("StrikeWaterCalculator", () => {
70, // grain temp (F)
152, // target mash temp (F)
1.25, // water-to-grain ratio
- "f", // temp unit
+ "F", // temp unit
10 // tun weight
);
@@ -70,7 +70,7 @@ describe("StrikeWaterCalculator", () => {
50,
152,
1.25,
- "f"
+ "F"
);
const warmGrain = StrikeWaterCalculator.calculateStrikeWater(
10,
@@ -78,7 +78,7 @@ describe("StrikeWaterCalculator", () => {
70,
152,
1.25,
- "f"
+ "F"
);
expect(coolGrain.strikeTemp).toBeGreaterThan(warmGrain.strikeTemp);
@@ -91,7 +91,7 @@ describe("StrikeWaterCalculator", () => {
20, // grain temp (C)
67, // target mash temp (C)
1.25, // water-to-grain ratio
- "c" // temp unit
+ "C" // temp unit
);
expect(result.strikeTemp).toBeGreaterThan(67); // Should be higher than target
@@ -106,7 +106,7 @@ describe("StrikeWaterCalculator", () => {
70,
152,
1.0,
- "f"
+ "F"
);
const thinMash = StrikeWaterCalculator.calculateStrikeWater(
10,
@@ -114,7 +114,7 @@ describe("StrikeWaterCalculator", () => {
70,
152,
1.5,
- "f"
+ "F"
);
expect(thickMash.waterVolume).toBe(10); // 10 * 1.0
@@ -129,7 +129,7 @@ describe("StrikeWaterCalculator", () => {
70,
152,
1.25,
- "f",
+ "F",
5 // light tun
);
const heavyTun = StrikeWaterCalculator.calculateStrikeWater(
@@ -138,7 +138,7 @@ describe("StrikeWaterCalculator", () => {
70,
152,
1.25,
- "f",
+ "F",
20 // heavy tun
);
@@ -166,7 +166,7 @@ describe("StrikeWaterCalculator", () => {
70.456,
152.789,
1.25,
- "f"
+ "F"
);
expect(result.strikeTemp).toBe(Math.round(result.strikeTemp * 10) / 10);
@@ -184,7 +184,7 @@ describe("StrikeWaterCalculator", () => {
158, // target mash temp
12.5, // current mash volume (qt)
180, // infusion water temp
- "f"
+ "F"
);
expect(result.infusionVolume).toBeGreaterThan(0);
@@ -198,7 +198,7 @@ describe("StrikeWaterCalculator", () => {
70, // target mash temp (C)
12, // current mash volume
85, // infusion water temp (C)
- "c"
+ "C"
);
expect(result.infusionVolume).toBeGreaterThan(0);
@@ -213,7 +213,7 @@ describe("StrikeWaterCalculator", () => {
158, // target temp
12, // volume
150, // infusion temp (too low - less than target)
- "f"
+ "F"
);
}).toThrow(
"Infusion water temperature must be higher than target mash temperature"
@@ -226,14 +226,14 @@ describe("StrikeWaterCalculator", () => {
155,
12,
180,
- "f"
+ "F"
);
const bigJump = StrikeWaterCalculator.calculateInfusion(
150,
165,
12,
180,
- "f"
+ "F"
);
expect(bigJump.infusionVolume).toBeGreaterThan(smallJump.infusionVolume);
@@ -245,7 +245,7 @@ describe("StrikeWaterCalculator", () => {
158.456,
12.789,
180.321,
- "f"
+ "F"
);
expect(result.infusionVolume).toBe(
@@ -300,45 +300,45 @@ describe("StrikeWaterCalculator", () => {
describe("validateInputs", () => {
it("should validate grain weight", () => {
expect(() => {
- StrikeWaterCalculator.validateInputs(0, 70, 152, 1.25, "f");
+ StrikeWaterCalculator.validateInputs(0, 70, 152, 1.25, "F");
}).toThrow("Grain weight must be greater than 0");
expect(() => {
- StrikeWaterCalculator.validateInputs(-5, 70, 152, 1.25, "f");
+ StrikeWaterCalculator.validateInputs(-5, 70, 152, 1.25, "F");
}).toThrow("Grain weight must be greater than 0");
});
it("should validate water-to-grain ratio", () => {
expect(() => {
- StrikeWaterCalculator.validateInputs(10, 70, 152, 0, "f");
+ StrikeWaterCalculator.validateInputs(10, 70, 152, 0, "F");
}).toThrow("Water to grain ratio must be between 0 and 10");
expect(() => {
- StrikeWaterCalculator.validateInputs(10, 70, 152, 15, "f");
+ StrikeWaterCalculator.validateInputs(10, 70, 152, 15, "F");
}).toThrow("Water to grain ratio must be between 0 and 10");
});
it("should validate Fahrenheit temperature ranges", () => {
expect(() => {
- StrikeWaterCalculator.validateInputs(10, 25, 152, 1.25, "f"); // Grain too cold
+ StrikeWaterCalculator.validateInputs(10, 25, 152, 1.25, "F"); // Grain too cold
}).toThrow("Grain temperature must be between 32°F and 120°F");
expect(() => {
- StrikeWaterCalculator.validateInputs(10, 70, 130, 1.25, "f"); // Mash too cold
+ StrikeWaterCalculator.validateInputs(10, 70, 130, 1.25, "F"); // Mash too cold
}).toThrow("Target mash temperature must be between 140°F and 170°F");
expect(() => {
- StrikeWaterCalculator.validateInputs(10, 70, 180, 1.25, "f"); // Mash too hot
+ StrikeWaterCalculator.validateInputs(10, 70, 180, 1.25, "F"); // Mash too hot
}).toThrow("Target mash temperature must be between 140°F and 170°F");
});
it("should validate Celsius temperature ranges", () => {
expect(() => {
- StrikeWaterCalculator.validateInputs(10, -5, 67, 1.25, "c"); // Grain too cold
+ StrikeWaterCalculator.validateInputs(10, -5, 67, 1.25, "C"); // Grain too cold
}).toThrow("Grain temperature must be between 0°C and 50°C");
expect(() => {
- StrikeWaterCalculator.validateInputs(10, 20, 55, 1.25, "c"); // Mash too cold
+ StrikeWaterCalculator.validateInputs(10, 20, 55, 1.25, "C"); // Mash too cold
}).toThrow("Target mash temperature must be between 60°C and 77°C");
});
@@ -347,11 +347,11 @@ describe("StrikeWaterCalculator", () => {
it("should pass validation for valid inputs", () => {
expect(() => {
- StrikeWaterCalculator.validateInputs(10, 70, 152, 1.25, "f");
+ StrikeWaterCalculator.validateInputs(10, 70, 152, 1.25, "F");
}).not.toThrow();
expect(() => {
- StrikeWaterCalculator.validateInputs(5, 20, 67, 1.5, "c");
+ StrikeWaterCalculator.validateInputs(5, 20, 67, 1.5, "C");
}).not.toThrow();
});
});
@@ -396,7 +396,7 @@ describe("StrikeWaterCalculator", () => {
70,
152,
1.25,
- "f"
+ "F"
);
expect(result.strikeTemp).toBeGreaterThan(0);
@@ -410,7 +410,7 @@ describe("StrikeWaterCalculator", () => {
70,
152,
1.25,
- "f"
+ "F"
);
expect(result.strikeTemp).toBeGreaterThan(0);
@@ -424,7 +424,7 @@ describe("StrikeWaterCalculator", () => {
150,
152,
1.25,
- "f" // Only 2F difference
+ "F" // Only 2F difference
);
expect(result.strikeTemp).toBeGreaterThan(152);
diff --git a/tests/src/services/calculators/UnitConverter.test.ts b/tests/src/services/calculators/UnitConverter.test.ts
index d868e8cf..c9355199 100644
--- a/tests/src/services/calculators/UnitConverter.test.ts
+++ b/tests/src/services/calculators/UnitConverter.test.ts
@@ -60,19 +60,16 @@ describe("UnitConverter", () => {
describe("temperature conversions", () => {
it("should convert Fahrenheit to Celsius", () => {
- const result = UnitConverter.convertTemperature(212, "f", "c");
+ const result = UnitConverter.convertTemperature(212, "F", "C");
expect(result).toBe(100);
});
it("should convert Celsius to Fahrenheit", () => {
- const result = UnitConverter.convertTemperature(0, "c", "f");
+ const result = UnitConverter.convertTemperature(0, "C", "F");
expect(result).toBe(32);
});
- it("should convert Celsius to Kelvin", () => {
- const result = UnitConverter.convertTemperature(25, "c", "k");
- expect(result).toBeCloseTo(298.15, 2);
- });
+ // Kelvin removed - not used in brewing
it("should handle case insensitive temperature units", () => {
const result1 = UnitConverter.convertTemperature(
@@ -87,8 +84,8 @@ describe("UnitConverter", () => {
it("should throw error for unknown temperature units", () => {
expect(() => {
- UnitConverter.convertTemperature(100, "x", "c");
- }).toThrow("Unknown temperature unit");
+ UnitConverter.convertTemperature(100, "x", "C");
+ }).toThrow("Unsupported temperature unit");
});
});
@@ -128,7 +125,7 @@ describe("UnitConverter", () => {
});
it("should validate temperature units", () => {
- expect(UnitConverter.isValidTemperatureUnit("f")).toBe(true);
+ expect(UnitConverter.isValidTemperatureUnit("F")).toBe(true);
expect(UnitConverter.isValidTemperatureUnit("celsius")).toBe(true);
expect(UnitConverter.isValidTemperatureUnit("invalid")).toBe(false);
});
@@ -146,8 +143,8 @@ describe("UnitConverter", () => {
});
it("should format temperature correctly", () => {
- expect(UnitConverter.formatTemperature(152.4, "f")).toBe("152.4°F");
- expect(UnitConverter.formatTemperature(67.2, "c")).toBe("67.2°C");
+ expect(UnitConverter.formatTemperature(152.4, "F")).toBe("152.4°F");
+ expect(UnitConverter.formatTemperature(67.2, "C")).toBe("67.2°C");
});
});
@@ -172,28 +169,20 @@ describe("UnitConverter", () => {
describe("temperature errors", () => {
it("should throw error for unknown from temperature unit", () => {
expect(() => {
- UnitConverter.convertTemperature(100, "invalid", "c");
- }).toThrow("Unknown temperature unit: invalid");
+ UnitConverter.convertTemperature(100, "invalid", "C");
+ }).toThrow("Unsupported temperature unit");
});
it("should throw error for unknown to temperature unit", () => {
expect(() => {
- UnitConverter.convertTemperature(100, "c", "invalid");
- }).toThrow("Unknown temperature unit: invalid");
- });
-
- it("should handle Kelvin conversion", () => {
- const result = UnitConverter.convertTemperature(273.15, "k", "c");
- expect(result).toBeCloseTo(0, 2);
+ UnitConverter.convertTemperature(100, "C", "invalid");
+ }).toThrow("Unsupported temperature unit");
});
- it("should handle Fahrenheit to Kelvin conversion", () => {
- const result = UnitConverter.convertTemperature(32, "f", "k");
- expect(result).toBeCloseTo(273.15, 2);
- });
+ // Kelvin tests removed - not used in brewing
it("should handle same unit temperature conversion", () => {
- const result = UnitConverter.convertTemperature(100, "c", "c");
+ const result = UnitConverter.convertTemperature(100, "C", "C");
expect(result).toBe(100);
});
});
@@ -258,13 +247,9 @@ describe("UnitConverter", () => {
it("should return all temperature units", () => {
const units = UnitConverter.getTemperatureUnits();
- expect(units).toContain("f");
- expect(units).toContain("c");
- expect(units).toContain("k");
- expect(units).toContain("fahrenheit");
- expect(units).toContain("celsius");
- expect(units).toContain("kelvin");
- expect(units.length).toBe(6);
+ expect(units).toContain("F");
+ expect(units).toContain("C");
+ expect(units.length).toBe(2); // Only C and F (Kelvin not used in brewing)
});
});
});
diff --git a/tests/src/services/offlineV2/StartupHydrationService.test.ts b/tests/src/services/offlineV2/StartupHydrationService.test.ts
index 5c281eed..f7f4ccf9 100644
--- a/tests/src/services/offlineV2/StartupHydrationService.test.ts
+++ b/tests/src/services/offlineV2/StartupHydrationService.test.ts
@@ -70,7 +70,7 @@ describe("StartupHydrationService", () => {
// Verify user data hydration was attempted
expect(mockUserCacheService.getRecipes).toHaveBeenCalledWith(
mockUserId,
- "imperial"
+ "metric"
);
// Verify static data hydration was attempted
@@ -467,12 +467,12 @@ describe("StartupHydrationService", () => {
},
});
- // Call without unit system (should default to imperial)
+ // Call without unit system (should default to metric)
await StartupHydrationService.hydrateOnStartup(mockUserId);
expect(mockUserCacheService.getRecipes).toHaveBeenCalledWith(
mockUserId,
- "imperial"
+ "metric"
);
});
});
diff --git a/tests/src/types/brewSession.test.ts b/tests/src/types/brewSession.test.ts
index 1ddda85b..0f91dbbd 100644
--- a/tests/src/types/brewSession.test.ts
+++ b/tests/src/types/brewSession.test.ts
@@ -1,7 +1,6 @@
import {
BrewSessionStatus,
FermentationStage,
- TemperatureUnit,
GravityReading,
FermentationEntry,
BrewSession,
@@ -11,7 +10,7 @@ import {
FermentationStats,
BrewSessionSummary,
} from "@src/types/brewSession";
-import { ID } from "@src/types/common";
+import { ID, TemperatureUnit } from "@src/types/common";
import { Recipe } from "@src/types/recipe";
describe("Brew Session Types", () => {
diff --git a/tests/src/types/common.test.ts b/tests/src/types/common.test.ts
index 32fb4239..1b600473 100644
--- a/tests/src/types/common.test.ts
+++ b/tests/src/types/common.test.ts
@@ -142,7 +142,7 @@ describe("Common Types", () => {
it("should handle first page pagination", () => {
const firstPageResponse: PaginatedResponse = {
- data: ["a", "b", "c"],
+ data: ["a", "b", "C"],
pagination: {
page: 1,
pages: 5,
diff --git a/tests/testUtils.tsx b/tests/testUtils.tsx
index b6568c02..13ab5cb1 100644
--- a/tests/testUtils.tsx
+++ b/tests/testUtils.tsx
@@ -7,6 +7,7 @@ import { NetworkProvider } from "@contexts/NetworkContext";
import { DeveloperProvider } from "@contexts/DeveloperContext";
import { UnitProvider } from "@contexts/UnitContext";
import { CalculatorsProvider } from "@contexts/CalculatorsContext";
+import { TemperatureUnit } from "@/src/types";
// Note: ScreenDimensionsProvider causes issues with react-native-safe-area-context in tests
// import { ScreenDimensionsProvider } from "@contexts/ScreenDimensionsContext";
@@ -20,7 +21,7 @@ interface CustomRenderOptions extends Omit {
isConnected?: boolean;
};
unitSettings?: {
- temperatureUnit?: "F" | "C";
+ temperatureUnit?: TemperatureUnit;
volumeUnit?: "gal" | "L";
weightUnit?: "lb" | "kg";
};
diff --git a/tests/utils/deviceUtils.test.ts b/tests/utils/deviceUtils.test.ts
new file mode 100644
index 00000000..b1f44c38
--- /dev/null
+++ b/tests/utils/deviceUtils.test.ts
@@ -0,0 +1,282 @@
+/**
+ * Device Utilities Tests
+ *
+ * Tests for device identification and platform detection utilities
+ * Android-only app
+ */
+
+import * as Device from "expo-device";
+import * as SecureStore from "expo-secure-store";
+import * as Crypto from "expo-crypto";
+import { getDeviceId, getDeviceName, getPlatform } from "@utils/deviceUtils";
+import { STORAGE_KEYS } from "@services/config";
+
+// Mutable state for device mock
+let mockModelName: string | null | undefined = null;
+let mockOsName: string | null | undefined = null;
+
+// Mock expo modules with dynamic getters
+jest.mock("expo-device", () => ({
+ get modelName() {
+ return mockModelName;
+ },
+ get osName() {
+ return mockOsName;
+ },
+}));
+jest.mock("expo-secure-store");
+jest.mock("expo-crypto");
+
+describe("deviceUtils", () => {
+ let originalCryptoDescriptor: PropertyDescriptor | undefined;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ originalCryptoDescriptor = Object.getOwnPropertyDescriptor(
+ global,
+ "crypto"
+ );
+ });
+
+ afterEach(() => {
+ if (originalCryptoDescriptor) {
+ Object.defineProperty(global, "crypto", originalCryptoDescriptor);
+ } else {
+ // If crypto wasn’t originally defined, clean up any test-added value
+ delete (global as any).crypto;
+ }
+ });
+
+ describe("getDeviceId", () => {
+ it("should return existing device ID from SecureStore", async () => {
+ const existingId = "existing-device-id-123";
+ (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(existingId);
+
+ const result = await getDeviceId();
+
+ expect(result).toBe(existingId);
+ expect(SecureStore.getItemAsync).toHaveBeenCalledWith(
+ STORAGE_KEYS.BIOMETRIC_DEVICE_ID
+ );
+ expect(SecureStore.setItemAsync).not.toHaveBeenCalled();
+ });
+
+ it("should generate and store new device ID when none exists", async () => {
+ (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null);
+ (SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
+
+ // Mock crypto.randomUUID
+ const mockUUID = "550e8400-e29b-41d4-a716-446655440000";
+ Object.defineProperty(global, "crypto", {
+ value: {
+ randomUUID: jest.fn(() => mockUUID),
+ },
+ writable: true,
+ configurable: true,
+ });
+
+ const result = await getDeviceId();
+
+ expect(result).toBe(mockUUID);
+ expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
+ STORAGE_KEYS.BIOMETRIC_DEVICE_ID,
+ mockUUID
+ );
+ });
+
+ it("should use expo-crypto fallback when crypto.randomUUID is unavailable", async () => {
+ (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null);
+ (SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
+
+ // Remove crypto.randomUUID
+ Object.defineProperty(global, "crypto", {
+ value: undefined,
+ writable: true,
+ configurable: true,
+ });
+
+ // Mock expo-crypto random bytes (16 bytes for UUID)
+ const mockBytes = new Uint8Array([
+ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44,
+ 0x55, 0x66, 0x77, 0x88,
+ ]);
+ (Crypto.getRandomBytesAsync as jest.Mock).mockResolvedValue(mockBytes);
+
+ const result = await getDeviceId();
+
+ // Should be a valid UUID format (with version and variant bits set)
+ expect(result).toMatch(
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
+ );
+ expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
+ STORAGE_KEYS.BIOMETRIC_DEVICE_ID,
+ result
+ );
+ });
+
+ it("should generate temporary ID when SecureStore fails", async () => {
+ (SecureStore.getItemAsync as jest.Mock).mockRejectedValue(
+ new Error("SecureStore unavailable")
+ );
+
+ const mockUUID = "temp-uuid-123";
+ Object.defineProperty(global, "crypto", {
+ value: {
+ randomUUID: jest.fn(() => mockUUID),
+ },
+ writable: true,
+ configurable: true,
+ });
+
+ const consoleWarnSpy = jest
+ .spyOn(console, "warn")
+ .mockImplementation(() => {});
+
+ const result = await getDeviceId();
+
+ expect(result).toBe(mockUUID);
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ expect.stringContaining("Failed to get/store device ID"),
+ expect.any(Error)
+ );
+ expect(SecureStore.setItemAsync).not.toHaveBeenCalled();
+
+ consoleWarnSpy.mockRestore();
+ });
+
+ it("should handle SecureStore.setItemAsync failure gracefully", async () => {
+ (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null);
+ (SecureStore.setItemAsync as jest.Mock).mockRejectedValue(
+ new Error("Storage full")
+ );
+
+ const mockUUID = "new-uuid-456";
+ Object.defineProperty(global, "crypto", {
+ value: {
+ randomUUID: jest.fn(() => mockUUID),
+ },
+ writable: true,
+ configurable: true,
+ });
+
+ const consoleWarnSpy = jest
+ .spyOn(console, "warn")
+ .mockImplementation(() => {});
+
+ const result = await getDeviceId();
+
+ // Should still return the generated UUID even if storage fails
+ expect(result).toBe(mockUUID);
+ expect(SecureStore.setItemAsync).toHaveBeenCalledTimes(1);
+ expect(consoleWarnSpy).toHaveBeenCalled();
+
+ consoleWarnSpy.mockRestore();
+ });
+ });
+
+ describe("getDeviceName", () => {
+ beforeEach(() => {
+ // Reset modelName before each test
+ mockModelName = null;
+ });
+
+ it("should return 'Unknown Device' when modelName is null", async () => {
+ mockModelName = null;
+
+ const result = await getDeviceName();
+
+ expect(result).toBe("Unknown Device");
+ });
+
+ it("should return 'Unknown Device' when modelName is undefined", async () => {
+ mockModelName = undefined;
+
+ const result = await getDeviceName();
+
+ expect(result).toBe("Unknown Device");
+ });
+
+ it("should return the modelName when it is a valid string", async () => {
+ mockModelName = "Pixel 6 Pro";
+
+ const result = await getDeviceName();
+
+ expect(result).toBe("Pixel 6 Pro");
+ });
+ });
+
+ describe("getPlatform", () => {
+ it("should return 'web' for unknown OS (fallback)", () => {
+ mockOsName = "Windows";
+
+ const result = getPlatform();
+
+ expect(result).toBe("web");
+ });
+
+ it("should return 'web' when osName is null", () => {
+ mockOsName = null;
+
+ const result = getPlatform();
+
+ expect(result).toBe("web");
+ });
+ });
+
+ describe("UUID generation", () => {
+ it("should generate RFC4122 v4 compliant UUIDs with expo-crypto", async () => {
+ (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null);
+ (SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
+
+ // Remove crypto.randomUUID to force expo-crypto path
+ Object.defineProperty(global, "crypto", {
+ value: undefined,
+ writable: true,
+ configurable: true,
+ });
+
+ // Mock random bytes that will result in specific UUID
+ const mockBytes = new Uint8Array(16);
+ for (let i = 0; i < 16; i++) {
+ mockBytes[i] = i * 16;
+ }
+ (Crypto.getRandomBytesAsync as jest.Mock).mockResolvedValue(mockBytes);
+
+ const result = await getDeviceId();
+
+ // Verify UUID format
+ expect(result).toMatch(
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
+ );
+
+ // Verify version 4 (4th character of 3rd group should be '4')
+ const versionChar = result.split("-")[2][0];
+ expect(versionChar).toBe("4");
+
+ // Verify variant (first character of 4th group should be 8, 9, a, or b)
+ const variantChar = result.split("-")[3][0].toLowerCase();
+ expect(["8", "9", "a", "b"]).toContain(variantChar);
+ });
+
+ it("should prefer crypto.randomUUID when available", async () => {
+ (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null);
+ (SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
+
+ const mockUUID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee";
+ const randomUUIDMock = jest.fn(() => mockUUID);
+ Object.defineProperty(global, "crypto", {
+ value: {
+ randomUUID: randomUUIDMock,
+ },
+ writable: true,
+ configurable: true,
+ });
+
+ const result = await getDeviceId();
+
+ expect(randomUUIDMock).toHaveBeenCalled();
+ expect(Crypto.getRandomBytesAsync).not.toHaveBeenCalled();
+ expect(result).toBe(mockUUID);
+ });
+ });
+});
diff --git a/tests/utils/unitContextMock.ts b/tests/utils/unitContextMock.ts
index 5aa89dd1..98630029 100644
--- a/tests/utils/unitContextMock.ts
+++ b/tests/utils/unitContextMock.ts
@@ -32,7 +32,7 @@ const defaultMockState = {
case "volume":
return "gal";
case "temperature":
- return "f";
+ return "F";
default:
return "lb";
}
@@ -59,7 +59,7 @@ const defaultMockState = {
? "lb"
: measurementType === "volume"
? "gal"
- : "f",
+ : "F",
})
),
formatValue: jest.fn(