Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/design/09-deferred-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
57 changes: 7 additions & 50 deletions lib/features/profile/presentation/settings_screen.dart
Original file line number Diff line number Diff line change
@@ -1,41 +1,25 @@
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';
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});
Expand Down Expand Up @@ -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<UserSettingsRecord> settings;

Future<void> _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);
Expand All @@ -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,
Expand Down
45 changes: 5 additions & 40 deletions test/widget/settings_screen_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<SettingsNavRow>(reminderRow()).value, 'Off');
expect(tester.widget<SettingsNavRow>(reminderRow()).isDimmed, isTrue);

await tester.tap(find.text(SettingsCopy.notificationsRow));
await settleLoaders(tester);

expect(
tester.widget<SettingsNavRow>(reminderRow()).value,
DailyReminder.defaultTime,
);
expect(tester.widget<SettingsNavRow>(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', (
Expand Down
Loading