fix(todo): the permission to notify asked wherever a reminder is planned, and Settings saying while it is off (ADR 0033, D31)

On a fresh install Todo's reminders never showed: the permission to notify
(Android 13's POST_NOTIFICATIONS, iOS' and macOS' alert) was asked only
where a task's sheet chose its first reminder, so reminders typed in quick
add, added by the automatic reminder or brought by sync were scheduled and
dropped by the system without a word.

Now NotificationAccess, the device's, reads whether notifications are
allowed whenever the plan holds a reminder and asks where they aren't,
once an app start, so that a refusal isn't met with the dialog again and
again; the task sheet asks through it. While reminders are planned and
notifications are off, Settings' Reminders say so, with Turn on: asked
again, and where that doesn't allow it, the system's notification settings
of the app opened (flutter_local_notifications' own call, nothing added).
Read again whenever the app comes back.

Whats-New: Todo asks to notify as soon as there is a reminder, and Settings › Reminders shows when notifications are off, with Turn on.
Whats-New-De: Todo fragt nach Benachrichtigungen, sobald es eine Erinnerung gibt, und Einstellungen › Erinnerungen zeigt, wenn sie aus sind, mit Einschalten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-04 22:35:06 +01:00
commit 82f1bf2739
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 458 additions and 33 deletions

View file

@ -3926,6 +3926,146 @@ void main() {
);
});
});
group('Notifications asked for wherever a reminder is planned (D31)', () {
/// Whether Settings' Reminders say that notifications are off.
Finder off() => find.text('Notifications are off');
testWidgets('a reminder typed in quick add asks to notify, once an '
'app start, however many follow', (tester) async {
reminders.allowed = false;
await start(tester, usedBefore: true);
expect(reminders.asked, 0, reason: 'nothing to remind of yet');
await goTab(tester, 'Inbox');
await quickAdd(tester, 'Leave !30m');
expect(reminders.scheduled, hasLength(1));
expect(reminders.asked, 1);
await quickAdd(tester, 'Call back !1h');
expect(reminders.scheduled, hasLength(2));
expect(reminders.asked, 1, reason: 'refused once, not asked again');
});
testWidgets('the automatic reminder of a task given a time asks to '
'notify', (tester) async {
await start(tester, usedBefore: true);
await tester.runAsync(
() => SettingsRepository(settingsRows, keys).write(
const TodoSettings(reminders: ReminderSettings(automatic: 15)),
),
);
settingsRows.bringRemoteChanges();
await tester.pumpAndSettle();
await goTab(tester, 'Inbox');
await quickAdd(tester, 'Call tomorrow 9am');
expect(reminders.scheduled, hasLength(1));
expect(reminders.asked, 1);
expect(reminders.enabled, isTrue);
});
testWidgets('a reminder another device synced asks to notify; nothing '
'is asked where none is planned', (tester) async {
final tomorrow = Day.of(DateTime.now()).plus(1);
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(
title: 'Call the bank',
projectId: inbox(),
date: TaskDate(tomorrow, time: 14 * 60),
),
);
await goTab(tester, 'Inbox');
await quickAdd(tester, 'Buy milk');
expect(reminders.asked, 0, reason: 'nothing to remind of');
await openSettings(tester);
expect(off(), findsNothing, reason: 'no reminder needs them');
await tester.pageBack();
await tester.pumpAndSettle();
await seed(
tester,
TaskContent(
title: 'Dentist',
projectId: inbox(),
date: TaskDate(tomorrow, time: 9 * 60),
reminders: const [30],
),
);
expect(reminders.asked, 1);
});
testWidgets("refused, Settings' Reminders say notifications are off; "
"Turn on asks again, then opens the system's settings, and "
'notifications allowed there count once Todo is back', (
tester,
) async {
final tomorrow = Day.of(DateTime.now()).plus(1);
reminders.allowed = false;
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(
title: 'Dentist',
projectId: inbox(),
date: TaskDate(tomorrow, time: 9 * 60),
reminders: const [30],
),
);
expect(reminders.asked, 1);
await openSettings(tester);
expect(off(), findsOneWidget);
await tester.tap(find.widgetWithText(FilledButton, 'Turn on'));
await tester.pumpAndSettle();
expect(reminders.asked, 2);
expect(
reminders.openedSettings,
1,
reason: 'refused again, or the system no longer asks',
);
expect(off(), findsOneWidget);
// Allowed in the system's settings, then back to Todo.
reminders.enabled = true;
tester.binding
..handleAppLifecycleStateChanged(AppLifecycleState.inactive)
..handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pumpAndSettle();
expect(off(), findsNothing);
expect(reminders.asked, 2);
});
testWidgets('Turn on allowed asks no more and opens nothing', (
tester,
) async {
final tomorrow = Day.of(DateTime.now()).plus(1);
reminders.allowed = false;
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(
title: 'Dentist',
projectId: inbox(),
date: TaskDate(tomorrow, time: 9 * 60),
reminders: const [30],
),
);
await openSettings(tester);
reminders.allowed = true;
await tester.tap(find.widgetWithText(FilledButton, 'Turn on'));
await tester.pumpAndSettle();
expect(reminders.asked, 2);
expect(reminders.openedSettings, 0);
expect(off(), findsNothing);
});
});
});
group("Upcoming's strip (ADR 0033, D8, D14)", () {

View file

@ -15,9 +15,17 @@ class MemoryReminderGateway() implements ReminderGateway {
/// What the person answers when asked.
bool allowed = true;
/// Whether the app may notify now: not on a fresh install until the
/// person is asked and allows it (Android 13 on, iOS, macOS), or allows
/// it in the system's settings meanwhile.
bool enabled = false;
/// How often they were asked.
int asked = 0;
/// How often the system's notification settings of the app were opened.
int openedSettings = 0;
@override
Future<void> replace(
List<ReminderNotice> notices, {
@ -29,9 +37,17 @@ class MemoryReminderGateway() implements ReminderGateway {
replaced++;
}
@override
Future<bool> notificationsAllowed() async => enabled;
@override
Future<bool> askPermission() async {
asked++;
return allowed;
return enabled = allowed;
}
@override
Future<void> openSettings() async {
openedSettings++;
}
}