feat(todo): Snooze on a reminder on iOS, macOS and Windows (ADR 0033, D4)

Apple's systems take actions from categories registered at start: one
for each length a snooze can be, its action in the language the app
starts in, not to the foreground, so that iOS snoozes in the plugin's
isolate with the app closed (AppDelegate registers the plugins for it).
Windows hands back only the button's arguments, which carry the
notification's ID and payload. macOS and Windows launch the app for an
action while it is closed, and the gateway snoozes what launched it as
it starts. Not yet tried on any of the three devices.

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-01 21:51:11 +01:00
commit e26ba47fb9
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,5 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_todo_flutter/src/feature/reminder/model/snooze.dart';
import 'package:halkyon_todo_flutter/src/feature/settings/model/todo_settings.dart';
/// A reminder snoozed (ADR 0033, D4): what its notification carries, so
/// that the system can bring it back with the app closed, and what comes
@ -40,4 +41,71 @@ void main() {
expect(isSnoozed(payload), isFalse);
}
});
group('the systems’ ways of saying Snooze was pressed (iOS, macOS, '
'Windows)', () {
final payload = snoozePayload(
title: reminder.title,
body: reminder.body,
snooze: snooze,
);
test('Android’s, Linux’s and Apple’s: the action, with the '
'notification’s ID and payload', () {
expect(
snoozeOf(actionId: snoozeAction, id: 41, payload: payload),
(id: 41, payload: payload),
);
});
test('Windows’: the button’s arguments alone, carrying both', () {
final arguments = windowsSnoozeArguments(id: 41, payload: payload);
expect(
snoozeOf(actionId: arguments, id: null, payload: arguments),
(id: 41, payload: payload),
);
expect(
snoozed(id: 41, payload: payload, now: now)!.notice.id,
snoozed(
id: snoozeOf(actionId: arguments, id: null, payload: arguments)!.id,
payload: payload,
now: now,
)!.notice.id,
reason: 'snoozed under the same ID however it was pressed',
);
});
test('anything else is no snooze: the notification tapped, another '
'action, arguments broken', () {
for (final (actionId, id) in [
(null, 41),
('', 41),
('open', 41),
(payload, null),
('$snoozeAction:', null),
('$snoozeAction:x:$payload', null),
]) {
expect(
snoozeOf(actionId: actionId, id: id, payload: payload),
isNull,
reason: '$actionId',
);
}
});
test('Apple’s categories: one for each length a snooze can be, its '
'action named for it; none for another length', () {
expect(snoozeChoices, contains(const ReminderSettings().snooze));
final categories = snoozeCategories((minutes) => 'Snooze $minutes min');
expect(categories.map((c) => c.id), [
for (final minutes in snoozeChoices) snoozeCategory(minutes),
]);
expect(
categories.firstWhere((c) => c.id == snoozeCategory(10)).label,
'Snooze 10 min',
);
expect(snoozeCategory(10), isNot(snoozeCategory(15)));
expect(snoozeCategory(7), isNull);
});
});
}