Wire reminder scheduler interface into local repository
This commit is contained in:
@@ -41,6 +41,19 @@ Disable it explicitly:
|
|||||||
flutter run --dart-define=ENABLE_BACKGROUND_SYNC=false
|
flutter run --dart-define=ENABLE_BACKGROUND_SYNC=false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Reminder Delivery Scaffold
|
||||||
|
|
||||||
|
Reminder scheduling is now wired through `ReminderScheduler` and reconciled from
|
||||||
|
`LocalRepository` on startup/state changes.
|
||||||
|
|
||||||
|
Current default implementation is no-op:
|
||||||
|
|
||||||
|
- `NoopReminderScheduler` in
|
||||||
|
`lib/features/reminders/scheduling/reminder_scheduler.dart`
|
||||||
|
|
||||||
|
This keeps behavior stable across all platforms until real local notification
|
||||||
|
integration is added.
|
||||||
|
|
||||||
## Local Persistence Backend
|
## Local Persistence Backend
|
||||||
|
|
||||||
Default local store:
|
Default local store:
|
||||||
|
|||||||
+28
-3
@@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
Updated: 2026-02-15
|
Updated: 2026-02-15
|
||||||
|
|
||||||
|
## Latest Milestone (2026-02-15): Reminder Delivery Interface Wiring
|
||||||
|
|
||||||
|
Implemented the notification scheduling boundary and connected it to local data
|
||||||
|
state transitions:
|
||||||
|
|
||||||
|
- Added reminder scheduler abstraction:
|
||||||
|
- `lib/features/reminders/scheduling/reminder_scheduler.dart`
|
||||||
|
- `ReminderScheduler` + `NoopReminderScheduler`
|
||||||
|
- Added provider wiring:
|
||||||
|
- `lib/features/reminders/scheduling/reminder_scheduler_provider.dart`
|
||||||
|
- Integrated schedule reconciliation with local repository:
|
||||||
|
- `lib/features/local/local_repository.dart`
|
||||||
|
- reconcile runs on repository boot and each persisted state update
|
||||||
|
- failures are swallowed to preserve offline-first behavior
|
||||||
|
- Added tests:
|
||||||
|
- `test/features/local/local_repository_test.dart` now verifies reminder
|
||||||
|
schedule reconciliation calls
|
||||||
|
- Updated setup docs:
|
||||||
|
- `docs/SETUP.md` now documents reminder scheduler scaffold
|
||||||
|
|
||||||
|
Validation for this milestone:
|
||||||
|
|
||||||
|
- `flutter analyze` -> pass
|
||||||
|
- `flutter test` -> pass
|
||||||
|
|
||||||
## Latest Milestone (2026-02-15): Sync Hardening + CI Baseline
|
## Latest Milestone (2026-02-15): Sync Hardening + CI Baseline
|
||||||
|
|
||||||
Implemented the next production-readiness slice:
|
Implemented the next production-readiness slice:
|
||||||
@@ -36,9 +61,9 @@ Validation for this milestone:
|
|||||||
|
|
||||||
### Open Phases (Current)
|
### Open Phases (Current)
|
||||||
|
|
||||||
1. Reminder delivery integration:
|
1. Reminder delivery runtime integration:
|
||||||
- add platform notification scheduler (local notifications) and wire reminder
|
- replace no-op scheduler with platform local notifications implementation
|
||||||
CRUD/state changes to schedule/cancel/update jobs.
|
for Android/iOS/macOS/Windows/Linux/Web-safe fallback.
|
||||||
2. Conflict resolution UX:
|
2. Conflict resolution UX:
|
||||||
- current UX shows and dismisses rejections; still missing guided
|
- current UX shows and dismisses rejections; still missing guided
|
||||||
resolve/retry/inspect flows per entity.
|
resolve/retry/inspect flows per entity.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:relationship_saver/features/local/storage/local_data_store.dart'
|
|||||||
import 'package:relationship_saver/features/local/storage/local_data_store_hive.dart';
|
import 'package:relationship_saver/features/local/storage/local_data_store_hive.dart';
|
||||||
import 'package:relationship_saver/features/local/storage/local_data_store_provider.dart';
|
import 'package:relationship_saver/features/local/storage/local_data_store_provider.dart';
|
||||||
import 'package:relationship_saver/features/local/storage/local_data_store_shared_prefs.dart';
|
import 'package:relationship_saver/features/local/storage/local_data_store_shared_prefs.dart';
|
||||||
|
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart';
|
||||||
import 'package:relationship_saver/features/sync/sync_queue_repository.dart';
|
import 'package:relationship_saver/features/sync/sync_queue_repository.dart';
|
||||||
import 'package:relationship_saver/integrations/backend/models/backend_models.dart';
|
import 'package:relationship_saver/integrations/backend/models/backend_models.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -41,6 +42,7 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
|||||||
if (record == null || record.rawState.isEmpty) {
|
if (record == null || record.rawState.isEmpty) {
|
||||||
final LocalDataState seeded = LocalDataState.seed();
|
final LocalDataState seeded = LocalDataState.seed();
|
||||||
await _persist(seeded);
|
await _persist(seeded);
|
||||||
|
await _reconcileReminderSchedule(seeded);
|
||||||
return seeded;
|
return seeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,10 +50,13 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
|||||||
try {
|
try {
|
||||||
final Map<String, dynamic> json =
|
final Map<String, dynamic> json =
|
||||||
jsonDecode(migrated.rawState) as Map<String, dynamic>;
|
jsonDecode(migrated.rawState) as Map<String, dynamic>;
|
||||||
return LocalDataState.fromJson(json);
|
final LocalDataState loaded = LocalDataState.fromJson(json);
|
||||||
|
await _reconcileReminderSchedule(loaded);
|
||||||
|
return loaded;
|
||||||
} on FormatException {
|
} on FormatException {
|
||||||
final LocalDataState fallback = LocalDataState.seed();
|
final LocalDataState fallback = LocalDataState.seed();
|
||||||
await _persist(fallback);
|
await _persist(fallback);
|
||||||
|
await _reconcileReminderSchedule(fallback);
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -889,6 +894,7 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
|||||||
Future<void> _setState(LocalDataState next) async {
|
Future<void> _setState(LocalDataState next) async {
|
||||||
state = AsyncData<LocalDataState>(next);
|
state = AsyncData<LocalDataState>(next);
|
||||||
await _persist(next);
|
await _persist(next);
|
||||||
|
await _reconcileReminderSchedule(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDataState _requireState() {
|
LocalDataState _requireState() {
|
||||||
@@ -908,6 +914,14 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _reconcileReminderSchedule(LocalDataState state) async {
|
||||||
|
try {
|
||||||
|
await ref.read(reminderSchedulerProvider).reconcile(state.reminders);
|
||||||
|
} catch (_) {
|
||||||
|
// Scheduling failures must not block local-first data updates.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<LocalDataRecord> _migrateIfNeeded(LocalDataRecord record) async {
|
Future<LocalDataRecord> _migrateIfNeeded(LocalDataRecord record) async {
|
||||||
final int currentVersion = record.schemaVersion;
|
final int currentVersion = record.schemaVersion;
|
||||||
if (currentVersion == _schemaVersion) {
|
if (currentVersion == _schemaVersion) {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:relationship_saver/features/local/local_models.dart';
|
||||||
|
|
||||||
|
/// Schedules and reconciles reminder deliveries for current local state.
|
||||||
|
abstract class ReminderScheduler {
|
||||||
|
/// Reconciles platform schedules to match current enabled reminders.
|
||||||
|
Future<void> reconcile(List<ReminderRule> reminders);
|
||||||
|
|
||||||
|
/// Clears all platform-scheduled reminder jobs.
|
||||||
|
Future<void> clearAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default scheduler used until platform notifications are integrated.
|
||||||
|
class NoopReminderScheduler implements ReminderScheduler {
|
||||||
|
const NoopReminderScheduler();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearAll() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> reconcile(List<ReminderRule> reminders) async {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart';
|
||||||
|
|
||||||
|
/// Provides reminder delivery scheduler implementation.
|
||||||
|
final Provider<ReminderScheduler> reminderSchedulerProvider =
|
||||||
|
Provider<ReminderScheduler>((Ref ref) {
|
||||||
|
return const NoopReminderScheduler();
|
||||||
|
});
|
||||||
@@ -4,6 +4,8 @@ import 'package:relationship_saver/features/local/local_models.dart';
|
|||||||
import 'package:relationship_saver/features/local/local_repository.dart';
|
import 'package:relationship_saver/features/local/local_repository.dart';
|
||||||
import 'package:relationship_saver/features/local/storage/local_data_store_in_memory.dart';
|
import 'package:relationship_saver/features/local/storage/local_data_store_in_memory.dart';
|
||||||
import 'package:relationship_saver/features/local/storage/local_data_store_provider.dart';
|
import 'package:relationship_saver/features/local/storage/local_data_store_provider.dart';
|
||||||
|
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart';
|
||||||
|
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart';
|
||||||
import 'package:relationship_saver/features/sync/storage/sync_state_store_in_memory.dart';
|
import 'package:relationship_saver/features/sync/storage/sync_state_store_in_memory.dart';
|
||||||
import 'package:relationship_saver/features/sync/storage/sync_state_store_provider.dart';
|
import 'package:relationship_saver/features/sync/storage/sync_state_store_provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -175,13 +177,66 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(afterDelete.reminders.length, beforeCount);
|
expect(afterDelete.reminders.length, beforeCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('reconciles reminder schedule after state changes', () async {
|
||||||
|
final RecordingReminderScheduler scheduler = RecordingReminderScheduler();
|
||||||
|
final ProviderContainer container = _createContainer(scheduler: scheduler);
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
final LocalDataState initial = await container.read(
|
||||||
|
localRepositoryProvider.future,
|
||||||
|
);
|
||||||
|
final int beforeCount = initial.reminders.length;
|
||||||
|
|
||||||
|
await container
|
||||||
|
.read(localRepositoryProvider.notifier)
|
||||||
|
.addReminder(
|
||||||
|
title: 'Scheduled reminder',
|
||||||
|
cadence: ReminderCadence.daily,
|
||||||
|
nextAt: DateTime.now().add(const Duration(hours: 4)),
|
||||||
|
);
|
||||||
|
final LocalDataState afterAdd = await container.read(
|
||||||
|
localRepositoryProvider.future,
|
||||||
|
);
|
||||||
|
|
||||||
|
await container
|
||||||
|
.read(localRepositoryProvider.notifier)
|
||||||
|
.deleteReminder(afterAdd.reminders.first.id);
|
||||||
|
final LocalDataState afterDelete = await container.read(
|
||||||
|
localRepositoryProvider.future,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(scheduler.snapshots, isNotEmpty);
|
||||||
|
expect(scheduler.snapshots.first.length, beforeCount);
|
||||||
|
expect(
|
||||||
|
scheduler.snapshots.any(
|
||||||
|
(List<ReminderRule> r) => r.length == beforeCount + 1,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(scheduler.snapshots.last.length, afterDelete.reminders.length);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ProviderContainer _createContainer() {
|
ProviderContainer _createContainer({ReminderScheduler? scheduler}) {
|
||||||
return ProviderContainer(
|
return ProviderContainer(
|
||||||
overrides: [
|
overrides: [
|
||||||
localDataStoreProvider.overrideWithValue(InMemoryLocalDataStore()),
|
localDataStoreProvider.overrideWithValue(InMemoryLocalDataStore()),
|
||||||
syncStateStoreProvider.overrideWithValue(InMemorySyncStateStore()),
|
syncStateStoreProvider.overrideWithValue(InMemorySyncStateStore()),
|
||||||
|
if (scheduler != null)
|
||||||
|
reminderSchedulerProvider.overrideWithValue(scheduler),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RecordingReminderScheduler implements ReminderScheduler {
|
||||||
|
final List<List<ReminderRule>> snapshots = <List<ReminderRule>>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearAll() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> reconcile(List<ReminderRule> reminders) async {
|
||||||
|
snapshots.add(List<ReminderRule>.unmodifiable(reminders));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user