From a30628ab96dd4f1b1b1c755678eb83683c54109c Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 2 Nov 2026 08:09:24 +0100 Subject: [PATCH] Haptic on meal log, VIBRATE permission Fires on the tap before the write; rejected logs stay silent. The dopamine-ux TODO now lists only the remaining work. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WzxdWVUTaKFW8m7YrvWbzV --- app/android/app/src/main/AndroidManifest.xml | 2 + app/lib/screens/log_meal_screen.dart | 3 + app/pubspec.yaml | 2 +- app/test/screens/log_meal_haptic_test.dart | 79 ++++++++++++++++++++ prompts/TODO-dopamine-ux-diet-guard.md | 16 ++++ 5 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 app/test/screens/log_meal_haptic_test.dart diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index a016e7a..74407f9 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + diff --git a/app/lib/screens/log_meal_screen.dart b/app/lib/screens/log_meal_screen.dart index 0f57a0e..4184410 100644 --- a/app/lib/screens/log_meal_screen.dart +++ b/app/lib/screens/log_meal_screen.dart @@ -25,6 +25,7 @@ import 'package:diet_guard_app/widgets/macro_input_row.dart'; import 'package:diet_guard_app/widgets/sync_health_banner.dart'; import 'package:diet_guard_app/widgets/today_progress_card.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; /// Lets the user log one food item, with food-bank autocomplete and @@ -146,6 +147,8 @@ class _LogMealScreenState extends State }); return; } + // On the tap, before the write/sync: waiting for them is just latency. + unawaited(HapticFeedback.mediumImpact()); await LogStorageService.instance.logMeal( desc, nutritionFromControllers(_macros, _source), diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 77ceb18..c60db56 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -2,7 +2,7 @@ name: diet_guard_app description: "Companion phone app for diet_guard: log meals on the go." publish_to: 'none' -version: 1.0.127+1 +version: 1.0.128+1 environment: sdk: ^3.12.2 diff --git a/app/test/screens/log_meal_haptic_test.dart b/app/test/screens/log_meal_haptic_test.dart new file mode 100644 index 0000000..722f7a7 --- /dev/null +++ b/app/test/screens/log_meal_haptic_test.dart @@ -0,0 +1,79 @@ +import 'dart:io'; + +import 'package:diet_guard_app/models/meal_schedule.dart'; +import 'package:diet_guard_app/screens/log_meal_screen.dart'; +import 'package:diet_guard_app/services/app_settings_service.dart'; +import 'package:diet_guard_app/services/budget_history_service.dart'; +import 'package:diet_guard_app/services/document_store_io.dart'; +import 'package:diet_guard_app/services/foodbank_service.dart'; +import 'package:diet_guard_app/services/log_storage_service.dart'; +import 'package:diet_guard_app/services/meal_schedule_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Directory dir; + final haptics = []; + + setUp(() async { + haptics.clear(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (call) async { + if (call.method == 'HapticFeedback.vibrate') { + haptics.add(call.arguments as String); + } + return null; + }); + dir = await Directory.systemTemp.createTemp('diet_guard_haptic_'); + LogStorageService.resetForTesting(store: FileDocumentStore(dir)); + FoodBankService.resetForTesting(store: FileDocumentStore(dir)); + AppSettingsService.resetForTesting(store: FileDocumentStore(dir)); + BudgetHistoryService.resetForTesting(store: FileDocumentStore(dir)); + await MealScheduleService.initForTesting(FileDocumentStore(dir)); + }); + + tearDown(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + LogStorageService.resetForTesting(); + FoodBankService.resetForTesting(); + AppSettingsService.resetForTesting(); + BudgetHistoryService.resetForTesting(); + MealScheduleService.resetForTesting(); + await dir.delete(recursive: true); + }); + + Future settle(WidgetTester tester) async { + await Future.delayed(const Duration(milliseconds: 200)); + await tester.pumpAndSettle(); + } + + testWidgets('logging a meal buzzes once, on the tap', (tester) async { + await tester.runAsync(() async { + await tester.pumpWidget(const MaterialApp(home: LogMealScreen())); + await settle(tester); + await tester.enterText(find.byType(TextField).at(0), 'toast'); + await settle(tester); + await tester.tap(find.byTooltip('Log meal')); + // One frame, no I/O wait: the haptic must not wait for the write. + await tester.pump(); + expect(haptics, ['HapticFeedbackType.mediumImpact']); + await settle(tester); + expect(haptics, hasLength(1)); + }); + }); + + testWidgets('a rejected log (empty description) does not buzz', ( + tester, + ) async { + await tester.runAsync(() async { + await tester.pumpWidget(const MaterialApp(home: LogMealScreen())); + await settle(tester); + await tester.tap(find.byTooltip('Log meal')); + await settle(tester); + expect(find.text('Type what you ate first.'), findsOneWidget); + expect(haptics, isEmpty); + }); + }); +} diff --git a/prompts/TODO-dopamine-ux-diet-guard.md b/prompts/TODO-dopamine-ux-diet-guard.md index e851389..3fde488 100644 --- a/prompts/TODO-dopamine-ux-diet-guard.md +++ b/prompts/TODO-dopamine-ux-diet-guard.md @@ -36,6 +36,22 @@ from the true state. Partial completion must never be recorded as DONE. --- +## status 2026-11-02 (audit session) + +DONE and committed: the haptic (`HapticFeedback.mediumImpact()` fired on the +tap, before the write, only for a log that passed validation; test +`app/test/screens/log_meal_haptic_test.dart`) and the VIBRATE permission. + +REMAINING (everything below this block that is not the haptic/VIBRATE): +- needs kuhy: approved text for the explicit confirmation (the old + `Logged "".` line was removed on purpose 2026-08-16, so the wording + and whether to bring it back is his call), and the sound toggle label. +- animated progress card / count-up numbers using motion tokens + (`~/src/utils/unified-design-system/DOCS-motion.md`), honouring + `disableAnimations`. +- step 2 sound: audio dependency, asset, opt-out `SwitchListTile`. +- on-phone check (haptic feel, remove-animations toggle): needs kuhy. + Make logging a meal feel satisfying in diet_guard_app ## findings (4, ranked by value over effort)