From fa4247cd9b682b818f11ed0cc1700e8954661df7 Mon Sep 17 00:00:00 2001 From: airkjw Date: Thu, 18 Jun 2026 12:21:02 +0900 Subject: [PATCH] feat(state): DailyRewardNotifier with injectable clock --- lib/state/daily_reward_notifier.dart | 35 ++++++++++++++++++++++ lib/state/providers.dart | 10 +++++++ test/state/daily_reward_notifier_test.dart | 35 ++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 lib/state/daily_reward_notifier.dart create mode 100644 test/state/daily_reward_notifier_test.dart diff --git a/lib/state/daily_reward_notifier.dart b/lib/state/daily_reward_notifier.dart new file mode 100644 index 0000000..9e36afa --- /dev/null +++ b/lib/state/daily_reward_notifier.dart @@ -0,0 +1,35 @@ +// lib/state/daily_reward_notifier.dart +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../data/save_repository.dart'; +import '../game/daily/daily_reward.dart'; +import 'providers.dart'; + +class DailyRewardNotifier extends Notifier { + static const _cal = DailyRewardCalendar(); + + SaveRepository get _save => ref.read(saveRepositoryProvider); + DateTime Function() get _now => ref.read(dailyNowProvider); + + @override + DailyResolution build() => _resolve(); + + DailyResolution _resolve() => _cal.resolve( + lastClaimedYmd: _save.dailyLastClaimedYmd, + storedDay: _save.dailyCalendarDay, + today: _now(), + ); + + /// Grants the current day's reward (×2 if [doubled]) and records the claim. + Future claim({bool doubled = false}) async { + final r = state; + if (!r.claimable) return; + final reward = _cal.rewardFor(r.day); + final inv = ref.read(boosterInventoryProvider.notifier); + for (final entry in reward.entries) { + await inv.grant(entry.key, entry.value * (doubled ? 2 : 1)); + } + await _save.recordDailyClaim(_cal.ymd(_now()), r.day); + state = _resolve(); + } +} diff --git a/lib/state/providers.dart b/lib/state/providers.dart index 8eb630e..ba999a1 100644 --- a/lib/state/providers.dart +++ b/lib/state/providers.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../data/content_repository.dart'; import '../data/save_repository.dart'; import '../data/streak.dart'; +import '../game/daily/daily_reward.dart'; import '../game/models/booster.dart'; import '../game/models/season.dart'; import '../services/ad_service.dart'; @@ -15,6 +16,7 @@ import '../services/review_service.dart'; import '../services/store_reviewer.dart'; import 'ads_notifier.dart'; import 'booster_inventory_notifier.dart'; +import 'daily_reward_notifier.dart'; import 'endless_best_notifier.dart'; import 'music_notifier.dart'; import 'sound_notifier.dart'; @@ -150,3 +152,11 @@ final boosterInventoryProvider = NotifierProvider>( BoosterInventoryNotifier.new, ); + +/// Injectable clock for the daily calendar (overridden in tests). +final dailyNowProvider = Provider((ref) => DateTime.now); + +final dailyRewardProvider = + NotifierProvider( + DailyRewardNotifier.new, +); diff --git a/test/state/daily_reward_notifier_test.dart b/test/state/daily_reward_notifier_test.dart new file mode 100644 index 0000000..d8dbe8b --- /dev/null +++ b/test/state/daily_reward_notifier_test.dart @@ -0,0 +1,35 @@ +import 'package:block_seasons/data/save_repository.dart'; +import 'package:block_seasons/game/models/booster.dart'; +import 'package:block_seasons/state/providers.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + Future container(DateTime today) async { + SharedPreferences.setMockInitialValues({}); + final repo = SaveRepository(await SharedPreferences.getInstance()); + return ProviderContainer(overrides: [ + saveRepositoryProvider.overrideWithValue(repo), + dailyNowProvider.overrideWithValue(() => today), + ]); + } + + test('first day is claimable as day 1 and claim grants the reward', () async { + final c = await container(DateTime(2026, 6, 18)); + expect(c.read(dailyRewardProvider).claimable, isTrue); + expect(c.read(dailyRewardProvider).day, 1); + + await c.read(dailyRewardProvider.notifier).claim(); + expect(c.read(boosterInventoryProvider)[BoosterType.hammer], 1); // day 1 + expect(c.read(dailyRewardProvider).claimable, isFalse); + }); + + test('doubled claim grants twice the reward', () async { + final c = await container(DateTime(2026, 6, 18)); + await c.read(dailyRewardProvider.notifier).claim(doubled: true); + expect(c.read(boosterInventoryProvider)[BoosterType.hammer], 2); + }); +}