diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 19e3d63a35..84a13e618f 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -311,7 +311,7 @@ class PuzzleController extends _$PuzzleController { solution: PuzzleSolution( id: state.puzzle.puzzle.id, win: state.result == PuzzleResult.win, - rated: initialContext.userId != null, + rated: initialContext.userId != null && ref.read(puzzlePreferencesProvider).rated, ), ); diff --git a/lib/src/model/puzzle/puzzle_preferences.dart b/lib/src/model/puzzle/puzzle_preferences.dart index 774fc549bb..6482f1d49a 100644 --- a/lib/src/model/puzzle/puzzle_preferences.dart +++ b/lib/src/model/puzzle/puzzle_preferences.dart @@ -32,6 +32,10 @@ class PuzzlePreferences extends _$PuzzlePreferences with SessionPreferencesStora Future setAutoNext(bool autoNext) async { save(state.copyWith(autoNext: autoNext)); } + + Future setRated(bool rated) async { + save(state.copyWith(rated: rated)); + } } @Freezed(fromJson: true, toJson: true) @@ -44,10 +48,14 @@ class PuzzlePrefs with _$PuzzlePrefs implements Serializable { /// no effect on puzzle streaks, which always show next puzzle. Defaults to /// `false`. @Default(false) bool autoNext, + + /// If `true`, the puzzle will be rated for logged in users. + /// Defaults to `true`. + @Default(true) bool rated, }) = _PuzzlePrefs; factory PuzzlePrefs.defaults({UserId? id}) => - PuzzlePrefs(id: id, difficulty: PuzzleDifficulty.normal, autoNext: false); + PuzzlePrefs(id: id, difficulty: PuzzleDifficulty.normal, autoNext: false, rated: true); factory PuzzlePrefs.fromJson(Map json) => _$PuzzlePrefsFromJson(json); } diff --git a/lib/src/view/puzzle/puzzle_settings_screen.dart b/lib/src/view/puzzle/puzzle_settings_screen.dart index 3725ed8425..0f4a860b29 100644 --- a/lib/src/view/puzzle/puzzle_settings_screen.dart +++ b/lib/src/view/puzzle/puzzle_settings_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; @@ -12,7 +13,9 @@ class PuzzleSettingsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final signedIn = ref.watch(authSessionProvider)?.user.id != null; final autoNext = ref.watch(puzzlePreferencesProvider.select((value) => value.autoNext)); + final rated = ref.watch(puzzlePreferencesProvider.select((value) => value.rated)); return BottomSheetScrollableContainer( children: [ SwitchSettingTile( @@ -22,6 +25,14 @@ class PuzzleSettingsScreen extends ConsumerWidget { ref.read(puzzlePreferencesProvider.notifier).setAutoNext(value); }, ), + if (signedIn) + SwitchSettingTile( + title: Text(context.l10n.rated), + value: rated, + onChanged: (value) { + ref.read(puzzlePreferencesProvider.notifier).setRated(value); + }, + ), PlatformListTile( title: const Text('Board settings'), trailing: const Icon(CupertinoIcons.chevron_right), diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index d80920ac60..eb67c10d3b 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -8,6 +8,8 @@ import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_difficulty.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -16,6 +18,7 @@ import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:mocktail/mocktail.dart'; +import '../../model/auth/fake_session_storage.dart'; import '../../test_helpers.dart'; import '../../test_provider_scope.dart'; import 'example_data.dart'; @@ -24,6 +27,22 @@ class MockPuzzleBatchStorage extends Mock implements PuzzleBatchStorage {} class MockPuzzleStorage extends Mock implements PuzzleStorage {} +class MockPuzzlePreferences extends PuzzlePreferences with Mock { + MockPuzzlePreferences(this._rated); + + final bool _rated; + + @override + PuzzlePrefs build() { + return PuzzlePrefs( + id: fakeSession.user.id, + difficulty: PuzzleDifficulty.normal, + autoNext: false, + rated: _rated, + ); + } +} + void main() { setUpAll(() { registerFallbackValue(PuzzleBatch(solved: IList(const []), unsolved: IList([puzzle]))); @@ -424,6 +443,78 @@ void main() { // called once to save solution and once after fetching a new puzzle verify(saveDBReq).called(2); }); + + for (final isRatedPreference in [true, false]) { + testWidgets( + 'puzzle rating is saved correctly, (isRatedPreference: $isRatedPreference)', + variant: kPlatformVariant, + (WidgetTester tester) async { + final mockClient = MockClient((request) { + if (request.url.path == '/api/puzzle/batch/mix') { + return mockResponse(batchOf1, 200); + } + return mockResponse('', 404); + }); + + final app = await makeTestProviderScopeApp( + tester, + home: PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: puzzle2.puzzle.id, + ), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), + puzzlePreferencesProvider.overrideWith( + () => MockPuzzlePreferences(isRatedPreference), + ), + ], + userSession: fakeSession, + ); + + Future saveDBReq() => mockBatchStorage.save( + userId: fakeSession.user.id, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + data: captureAny(named: 'data'), + ); + when(saveDBReq).thenAnswer((_) async {}); + when( + () => mockBatchStorage.fetch( + userId: fakeSession.user.id, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + ), + ).thenAnswer((_) async => batch); + + when( + () => mockHistoryStorage.save(puzzle: any(named: 'puzzle')), + ).thenAnswer((_) async {}); + + await tester.pumpWidget(app); + + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 200)); + + // await for first move to be played and view solution button to appear + await tester.pump(const Duration(seconds: 5)); + + expect(find.byIcon(Icons.help), findsOneWidget); + await tester.tap(find.byIcon(Icons.help)); + + // wait for solution replay animation to finish + await tester.pump(const Duration(seconds: 1)); + + // check puzzle was saved as isRatedPreference + final captured = verify(saveDBReq).captured; + expect(captured.length, 2); + expect((captured[1] as PuzzleBatch).solved.length, 0); + expect((captured[0] as PuzzleBatch).solved.length, 1); + expect((captured[0] as PuzzleBatch).solved[0].rated, isRatedPreference); + }, + ); + } }); }