diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 27e0a922..257f4962 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -89,6 +89,10 @@ You can always edit this file by hand instead — the helpers just save effort. longer repeats the kind and title the card already states above it. - **A Coffee Challenge titles itself one step below a lesson.** It is optional, and the design added a ladder step rather than let the two read as equals. +- **The Notifications and Daily reminder rows are out of Settings for now.** They + stored a preference nothing read: no reminder has ever been scheduled. They + come back when reminders are built, which is next on the list. + - **Today's card is the design's.** A surface card with a rule around it: the module as its eyebrow, the title, the picture, one line saying where the lesson sits and how long it takes (`LESSON 1/7 · ~3 MIN`), and a full-width diff --git a/docs/design/09-deferred-v2.md b/docs/design/09-deferred-v2.md index 54b2adae..3738314f 100644 --- a/docs/design/09-deferred-v2.md +++ b/docs/design/09-deferred-v2.md @@ -25,6 +25,11 @@ ## Rewarded ads + timed trials `RewardedAdScreen` (simulated video → 15-min unlock), `RoastyGiftScreen` (perfect-module → 24-h Studio unlock), `TrialBadge` countdown. Needs an ad SDK. +## Daily reminder (`settings.jsx` TimeSheet, `screens.jsx` PRACTICE rows) +- Two Settings rows — a *Notifications* switch and a *Daily reminder* time row with eight slots — and the sheet's promise, *One quiet reminder a day to keep your streak alive*. Nothing in the design says what the notification says or when it fires. +- **Ruled 8 September 2026: not in the v1 launch, and the next thing built after it** ([#443](https://github.com/maximsan/brewpath/issues/443)). Until it ships the two rows are hidden, so no control is drawn that nothing reads. +- **Needs:** a local-notification package with Swift Package Manager support, the OS permission and its refusal path, notification copy, and the fire-or-skip rule for a day already active. + ## Onboarding question flow 6 question screens + expectation + closing. Fully built; nothing consumes the answers yet. diff --git a/lib/features/profile/presentation/settings_screen.dart b/lib/features/profile/presentation/settings_screen.dart index 09062912..113ab1e0 100644 --- a/lib/features/profile/presentation/settings_screen.dart +++ b/lib/features/profile/presentation/settings_screen.dart @@ -1,14 +1,12 @@ import 'package:brew_path/core/constants/app_routes.dart'; import 'package:brew_path/core/widgets/settings_nav_row.dart'; import 'package:brew_path/core/widgets/sub_screen_scaffold.dart'; -import 'package:brew_path/features/profile/domain/daily_reminder.dart'; import 'package:brew_path/features/profile/domain/learner_name.dart'; import 'package:brew_path/features/profile/domain/settings_providers.dart'; import 'package:brew_path/features/profile/presentation/settings/settings_confirmations.dart'; import 'package:brew_path/features/profile/presentation/settings/settings_copy.dart'; import 'package:brew_path/features/profile/presentation/settings/settings_sub_screen.dart'; import 'package:brew_path/features/profile/presentation/widgets/appearance_selector.dart'; -import 'package:brew_path/features/profile/presentation/widgets/daily_reminder_sheet.dart'; import 'package:brew_path/features/profile/presentation/widgets/name_sheet.dart'; import 'package:brew_path/shared/storage/settings_record.dart'; import 'package:brew_path/shared/theme/app_spacing.dart'; @@ -16,26 +14,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -/// Settings, in the design's four sections. +/// Settings, in the design's four sections and their order. /// -/// The order and the grouping are the design's, not the app's: `APPEARANCE` -/// leads, the preference toggles are filed under `PRACTICE` beside the reminder -/// they belong with, `ACCOUNT` and `SUPPORT` are pure navigation, and the -/// destructive block at the foot carries **no label** — a heading over it would -/// announce it before the learner has any reason to look there. -/// -/// The destructive block carries three rows, which is one more than the design -/// and one more than works. Both are the owner's rulings on #395, not this -/// file's: -/// -/// - **`Delete account` is drawn and inert.** The design lists it, and there -/// is nothing to delete — Firebase is gated off and the app keeps everything -/// on the device. Shown rather than omitted so the block is the design's -/// shape from the start; dimmed and unpressable so it cannot promise -/// anything, which is the trade the ruling accepted. -/// - **`Restart onboarding` is kept**, and the design has no such row. It is -/// the app's own, it works, and it is the only way back through the intro -/// #383 built. +/// The foot carries no label, `Delete account` drawn and inert while Firebase +/// is off, and `Restart onboarding`, which the design lacks and #383 needs: +/// the owner's rulings on #395. The design's reminder rows under `PRACTICE` +/// wait for #443. class SettingsScreen extends ConsumerWidget { /// Creates a [SettingsScreen]. const SettingsScreen({super.key}); @@ -80,26 +64,13 @@ class SettingsScreen extends ConsumerWidget { } } -/// The four preference rows the design files under `PRACTICE`. +/// The `PRACTICE` rows: sound and haptics. The design's two reminder rows are +/// hidden until reminders exist (#443). class _PracticeRows extends ConsumerWidget { const _PracticeRows({required this.settings}); final AsyncValue settings; - Future _pickReminder( - BuildContext context, - WidgetRef ref, - UserSettingsRecord current, - ) async { - final picked = await DailyReminderSheet.show( - context, - current: current.dailyReminderTime, - ); - if (picked == null) return; - - await ref.read(settingsControllerProvider.notifier).setReminderTime(picked); - } - @override Widget build(BuildContext context, WidgetRef ref) { final controller = ref.read(settingsControllerProvider.notifier); @@ -110,20 +81,6 @@ class _PracticeRows extends ConsumerWidget { data: (state) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SettingsNavRow( - label: SettingsCopy.notificationsRow, - toggleValue: state.notificationsEnabled, - onToggle: (_) => controller.toggleNotifications(), - ), - SettingsNavRow( - label: SettingsCopy.reminderRow, - value: DailyReminder.rowValue( - enabled: state.notificationsEnabled, - time: state.dailyReminderTime, - ), - isDimmed: !state.notificationsEnabled, - onTap: () => _pickReminder(context, ref, state), - ), SettingsNavRow( label: SettingsCopy.soundRow, toggleValue: state.soundEnabled, diff --git a/test/widget/settings_screen_test.dart b/test/widget/settings_screen_test.dart index a2842291..0a96446e 100644 --- a/test/widget/settings_screen_test.dart +++ b/test/widget/settings_screen_test.dart @@ -3,7 +3,6 @@ import 'package:brew_path/core/icons/app_icon.dart'; import 'package:brew_path/core/widgets/primary_button.dart'; import 'package:brew_path/core/widgets/settings_nav_row.dart'; import 'package:brew_path/core/widgets/smallcaps_label.dart'; -import 'package:brew_path/features/profile/domain/daily_reminder.dart'; import 'package:brew_path/features/profile/domain/learner_name.dart'; import 'package:brew_path/features/profile/domain/settings_providers.dart'; import 'package:brew_path/features/profile/presentation/settings/settings_copy.dart'; @@ -121,49 +120,15 @@ void main() { expect(stored.hapticsEnabled, isFalse); }); - testWidgets('the reminder row reads Off until it is asked for', ( + testWidgets('the reminder rows are not on Settings until reminders exist', ( tester, ) async { - // A time is only a setting while the switch above it is on: a row showing - // 8:00 AM with notifications off promises something that never arrives. + // #443: a switch that stores a preference nothing reads is the inert row + // the repo forbids, so both rows are hidden until a reminder can arrive. await openSettings(tester); - Finder reminderRow() => find.ancestor( - of: find.text(SettingsCopy.reminderRow), - matching: find.byType(SettingsNavRow), - ); - - expect(tester.widget(reminderRow()).value, 'Off'); - expect(tester.widget(reminderRow()).isDimmed, isTrue); - - await tester.tap(find.text(SettingsCopy.notificationsRow)); - await settleLoaders(tester); - - expect( - tester.widget(reminderRow()).value, - DailyReminder.defaultTime, - ); - expect(tester.widget(reminderRow()).isDimmed, isFalse); - }); - - testWidgets('picking a time stores it and turns the reminder on', ( - tester, - ) async { - await openSettings(tester); - - await tester.tap(find.text(SettingsCopy.reminderRow)); - await tester.pumpAndSettle(); - - expect(find.text(DailyReminder.sheetTitle), findsOneWidget); - - await tester.tap(find.text('6:30 AM')); - await tester.pump(); - await tester.tap(find.text(DailyReminder.sheetAction)); - await settleLoaders(tester); - - final stored = await SettingsRepository().getSettings(); - expect(stored.dailyReminderTime, '6:30 AM'); - expect(stored.notificationsEnabled, isTrue); + expect(find.text(SettingsCopy.notificationsRow), findsNothing); + expect(find.text(SettingsCopy.reminderRow), findsNothing); }); testWidgets('the Name row sets, changes and clears what Profile greets by', (