Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
147f85d
Begin integration of unit conversion + optimisation to BeerXML import…
jackmisner Dec 1, 2025
45dcd97
- Explicitly set unit_system after conversion to avoid callers potent…
jackmisner Dec 2, 2025
6671499
- Match create recipe pattern for import beer xml flow (create offlin…
jackmisner Dec 2, 2025
11e9a10
- Fix: Debug console.log statements in importReview.tsx
jackmisner Dec 3, 2025
5110a15
- Extract metrics fallback logic to reduce duplication
jackmisner Dec 3, 2025
2c17ae8
Fixed all unawaited UnifiedLogger calls and redundant logging across …
jackmisner Dec 3, 2025
6702fbb
- De-duplicate NetworkContext.handleStateChange logs to avoid redunda…
jackmisner Dec 3, 2025
891cf9e
fix: standardize temperature unit casing to uppercase (F/C) across te…
jackmisner Dec 3, 2025
09cd23c
Merge 891cf9e3dc78273ec77562386103c91d46569c2d into d16f3d52b6e2e86ba…
jackmisner Dec 3, 2025
67ff6f3
Auto-format code with Prettier
actions-user Dec 3, 2025
29325f0
fix: streamline temperature unit derivation and improve unit handling…
jackmisner Dec 3, 2025
30ffcd7
feat: enhance recipe unit conversion logic and validation across comp…
jackmisner Dec 3, 2025
fd04bdd
feat: update version to 3.3.8 and enhance recipe creation payload han…
jackmisner Dec 3, 2025
880d0f1
feat: update version to 3.3.9 and refactor recipe handling for improv…
jackmisner Dec 4, 2025
25a4733
chore: update version to 3.3.10 and adjust related files
jackmisner Dec 5, 2025
4ef5a53
- Remove many superfluous debug logs
jackmisner Dec 5, 2025
e4ccc73
- Fix: Dependency Version Conflicts
jackmisner Dec 5, 2025
96d5566
- Include type-specific fields for proper ingredient matching and met…
jackmisner Dec 5, 2025
4fe0592
- Change logger error to warning for ingredients missing ingredient_i…
jackmisner Dec 5, 2025
2222934
- Fix: batch_size_unit and unit_system are overwritten instead of pre…
jackmisner Dec 5, 2025
ab3ed6f
Fix: update unit system logic and enhance ingredient normalization in…
jackmisner Dec 5, 2025
4a891c8
- Align numeric coercion/defaults for batch size & boil time across p…
jackmisner Dec 5, 2025
647bdf6
- Reject clearly invalid ingredient amounts (e.g., negatives) during …
jackmisner Dec 5, 2025
1f91d29
- Added Type-Safe Interfaces to importReview for managing data across…
jackmisner Dec 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'}\""
}
Expand Down
2 changes: 1 addition & 1 deletion android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<resources>
<string name="app_name">BrewTracker</string>
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
<string name="expo_runtime_version">3.2.6</string>
<string name="expo_runtime_version">3.3.15</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
</resources>
6 changes: 3 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -16,7 +16,7 @@
},
"edgeToEdgeEnabled": true,
"package": "com.brewtracker.android",
"versionCode": 191,
"versionCode": 207,
"permissions": [
"CAMERA",
"VIBRATE",
Expand Down Expand Up @@ -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"
}
Expand Down
7 changes: 6 additions & 1 deletion app/(auth)/resetPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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.");
Expand Down
140 changes: 115 additions & 25 deletions app/(modals)/(beerxml)/importBeerXML.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImportState>({
step: "file_selection",
Expand All @@ -65,6 +61,7 @@ export default function ImportBeerXMLScreen() {
selectedFile: null,
parsedRecipes: [],
selectedRecipe: null,
convertingTarget: null,
});

/**
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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;
}
Expand All @@ -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",
},
});
Expand All @@ -167,6 +240,7 @@ export default function ImportBeerXMLScreen() {
selectedFile: null,
parsedRecipes: [],
selectedRecipe: null,
convertingTarget: null,
});
};

Expand Down Expand Up @@ -319,7 +393,7 @@ export default function ImportBeerXMLScreen() {

<TouchableOpacity
style={[styles.button, styles.primaryButton]}
onPress={proceedToIngredientMatching}
onPress={() => proceedToIngredientMatching()}
testID={TEST_IDS.patterns.touchableOpacityAction(
"proceed-to-matching"
)}
Expand Down Expand Up @@ -403,6 +477,22 @@ export default function ImportBeerXMLScreen() {
importState.step === "recipe_selection" &&
renderRecipeSelection()}
</ScrollView>

{/* Unit Conversion Choice Modal */}
<UnitConversionChoiceModal
visible={importState.step === "unit_choice"}
userUnitSystem={unitSystem}
convertingTarget={importState.convertingTarget}
recipeName={importState.selectedRecipe?.name}
onChooseMetric={() => handleUnitSystemChoice("metric")}
onChooseImperial={() => handleUnitSystemChoice("imperial")}
onCancel={() =>
setImportState(prev => ({
...prev,
step: "file_selection",
}))
}
/>
</View>
);
}
Loading