Skip to content

Commit d07b1b0

Browse files
authored
Merge pull request #378 from DevKor-github/codex/add-account-deletion-api
[codex] Support account deletion feedback
2 parents 0531e99 + 2e4b121 commit d07b1b0

9 files changed

Lines changed: 231 additions & 30 deletions

File tree

lib/data/data_sources/authentication_remote_data_source.dart

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ abstract interface class AuthenticationRemoteDataSource {
2323

2424
Future<UserEntity> getUser();
2525

26-
Future<void> deleteGoogleMe();
26+
Future<void> deleteGoogleMe({String? feedbackMessage});
2727

28-
Future<void> deleteAppleMe();
28+
Future<void> deleteAppleMe({String? feedbackMessage});
2929

3030
Future<void> postFeedback(String message);
3131

32-
Future<void> deleteUser();
32+
Future<void> deleteUser({String? feedbackMessage});
3333

3434
Future<String?> getUserSocialType();
3535
}
@@ -143,9 +143,12 @@ class AuthenticationRemoteDataSourceImpl
143143
}
144144

145145
@override
146-
Future<void> deleteGoogleMe() async {
146+
Future<void> deleteGoogleMe({String? feedbackMessage}) async {
147147
try {
148-
final result = await dio.delete(Endpoint.deleteGoogleMe);
148+
final result = await dio.delete(
149+
Endpoint.deleteGoogleMe,
150+
data: _buildDeleteFeedbackData(feedbackMessage),
151+
);
149152
if (result.statusCode == 200) {
150153
return;
151154
} else {
@@ -157,9 +160,12 @@ class AuthenticationRemoteDataSourceImpl
157160
}
158161

159162
@override
160-
Future<void> deleteAppleMe() async {
163+
Future<void> deleteAppleMe({String? feedbackMessage}) async {
161164
try {
162-
final result = await dio.delete(Endpoint.deleteAppleMe);
165+
final result = await dio.delete(
166+
Endpoint.deleteAppleMe,
167+
data: _buildDeleteFeedbackData(feedbackMessage),
168+
);
163169
if (result.statusCode == 200) {
164170
return;
165171
} else {
@@ -189,9 +195,12 @@ class AuthenticationRemoteDataSourceImpl
189195
}
190196

191197
@override
192-
Future<void> deleteUser() async {
198+
Future<void> deleteUser({String? feedbackMessage}) async {
193199
try {
194-
final result = await dio.delete(Endpoint.deleteUser);
200+
final result = await dio.delete(
201+
Endpoint.deleteUser,
202+
data: _buildDeleteFeedbackData(feedbackMessage),
203+
);
195204
if (result.statusCode == 200) {
196205
return;
197206
} else {
@@ -216,4 +225,16 @@ class AuthenticationRemoteDataSourceImpl
216225
rethrow;
217226
}
218227
}
228+
229+
Map<String, dynamic> _buildDeleteFeedbackData(String? feedbackMessage) {
230+
final trimmedMessage = feedbackMessage?.trim();
231+
if (trimmedMessage == null || trimmedMessage.isEmpty) {
232+
return <String, dynamic>{};
233+
}
234+
235+
return <String, dynamic>{
236+
'feedbackId': const Uuid().v4(),
237+
'message': trimmedMessage,
238+
};
239+
}
219240
}

lib/data/repositories/user_repository_impl.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,27 +132,33 @@ class UserRepositoryImpl implements UserRepository {
132132
}
133133

134134
@override
135-
Future<void> deleteUser() async {
135+
Future<void> deleteUser({String? feedbackMessage}) async {
136136
try {
137-
await _authenticationRemoteDataSource.deleteUser();
137+
await _authenticationRemoteDataSource.deleteUser(
138+
feedbackMessage: feedbackMessage,
139+
);
138140
} catch (e) {
139141
rethrow;
140142
}
141143
}
142144

143145
@override
144-
Future<void> deleteGoogleUser() async {
146+
Future<void> deleteGoogleUser({String? feedbackMessage}) async {
145147
try {
146-
await _authenticationRemoteDataSource.deleteGoogleMe();
148+
await _authenticationRemoteDataSource.deleteGoogleMe(
149+
feedbackMessage: feedbackMessage,
150+
);
147151
} catch (e) {
148152
rethrow;
149153
}
150154
}
151155

152156
@override
153-
Future<void> deleteAppleUser() async {
157+
Future<void> deleteAppleUser({String? feedbackMessage}) async {
154158
try {
155-
await _authenticationRemoteDataSource.deleteAppleMe();
159+
await _authenticationRemoteDataSource.deleteAppleMe(
160+
feedbackMessage: feedbackMessage,
161+
);
156162
} catch (e) {
157163
rethrow;
158164
}

lib/domain/repositories/user_repository.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ abstract interface class UserRepository {
2424

2525
Future<void> getUser();
2626

27-
Future<void> deleteUser();
27+
Future<void> deleteUser({String? feedbackMessage});
2828

29-
Future<void> deleteGoogleUser();
29+
Future<void> deleteGoogleUser({String? feedbackMessage});
3030

31-
Future<void> deleteAppleUser();
31+
Future<void> deleteAppleUser({String? feedbackMessage});
3232

3333
Future<void> postFeedback(String message);
3434

lib/domain/use-cases/delete_user_use_case.dart

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,16 @@ class DeleteUserUseCase {
99
DeleteUserUseCase(this._userRepository);
1010

1111
Future<void> call(String feedbackMessage) async {
12-
try {
13-
await _userRepository.postFeedback(feedbackMessage);
14-
} catch (_) {}
15-
1612
final socialTypeString = await _userRepository.getUserSocialType();
1713
final socialType = socialTypeFromString(socialTypeString);
1814

1915
if (socialType == SocialType.google) {
16+
await _userRepository.deleteGoogleUser(feedbackMessage: feedbackMessage);
2017
await _userRepository.disconnectGoogleSignIn();
21-
await _userRepository.deleteGoogleUser();
2218
} else if (socialType == SocialType.apple) {
23-
await _userRepository.deleteAppleUser();
19+
await _userRepository.deleteAppleUser(feedbackMessage: feedbackMessage);
2420
} else {
25-
await _userRepository.deleteUser();
21+
await _userRepository.deleteUser(feedbackMessage: feedbackMessage);
2622
}
2723
}
2824
}

lib/presentation/my_page/my_page_modal/delete_user_modal.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ class DeleteUserModal {
132132
await deleteUserUseCase(controller.text);
133133
} catch (e) {
134134
debugPrint(e.toString());
135+
if (context.mounted) {
136+
ScaffoldMessenger.of(context).showSnackBar(
137+
SnackBar(content: Text(l10n.error)),
138+
);
139+
}
140+
return;
135141
}
136142

137143
try {

lib/presentation/my_page/my_page_screen.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,14 @@ class MyPageScreen extends StatelessWidget {
5656
title: AppLocalizations.of(context)!.deleteAccount,
5757
onTap: () async {
5858
final deleteUserModal = DeleteUserModal();
59-
await deleteUserModal.showDeleteUserModal(context,
60-
onConfirm: () {});
59+
await deleteUserModal.showDeleteUserModal(
60+
context,
61+
onConfirm: () {
62+
if (context.mounted) {
63+
context.go('/signIn');
64+
}
65+
},
66+
);
6167
},
6268
),
6369
],
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:dio/dio.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:mockito/mockito.dart';
4+
import 'package:on_time_front/core/constants/endpoint.dart';
5+
import 'package:on_time_front/data/data_sources/authentication_remote_data_source.dart';
6+
7+
import '../../helpers/mock.mocks.dart';
8+
9+
void main() {
10+
late Dio dio;
11+
late AuthenticationRemoteDataSourceImpl dataSource;
12+
13+
setUp(() {
14+
dio = MockAppDio();
15+
dataSource = AuthenticationRemoteDataSourceImpl(dio);
16+
});
17+
18+
group('deleteUser', () {
19+
test('sends optional feedback in the DELETE request body', () async {
20+
when(dio.delete(Endpoint.deleteUser, data: anyNamed('data'))).thenAnswer(
21+
(_) async => Response(
22+
statusCode: 200,
23+
requestOptions: RequestOptions(path: Endpoint.deleteUser),
24+
),
25+
);
26+
27+
await dataSource.deleteUser(feedbackMessage: ' Not useful anymore. ');
28+
29+
final capturedData = verify(
30+
dio.delete(Endpoint.deleteUser, data: captureAnyNamed('data')),
31+
).captured.single as Map<String, dynamic>;
32+
expect(capturedData['feedbackId'], isA<String>());
33+
expect(capturedData['message'], 'Not useful anymore.');
34+
});
35+
36+
test('sends an empty body when feedback is blank', () async {
37+
when(dio.delete(Endpoint.deleteGoogleMe, data: anyNamed('data')))
38+
.thenAnswer(
39+
(_) async => Response(
40+
statusCode: 200,
41+
requestOptions: RequestOptions(path: Endpoint.deleteGoogleMe),
42+
),
43+
);
44+
45+
await dataSource.deleteGoogleMe(feedbackMessage: ' ');
46+
47+
verify(dio.delete(Endpoint.deleteGoogleMe, data: <String, dynamic>{}))
48+
.called(1);
49+
});
50+
});
51+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:google_sign_in/google_sign_in.dart';
3+
import 'package:on_time_front/domain/entities/user_entity.dart';
4+
import 'package:on_time_front/domain/repositories/user_repository.dart';
5+
import 'package:on_time_front/domain/use-cases/delete_user_use_case.dart';
6+
7+
void main() {
8+
test('deletes normal accounts with feedback', () async {
9+
final repository = _FakeUserRepository(socialType: null);
10+
final useCase = DeleteUserUseCase(repository);
11+
12+
await useCase('Too many notifications');
13+
14+
expect(repository.deletedNormalFeedback, 'Too many notifications');
15+
expect(repository.deletedGoogleFeedback, isNull);
16+
expect(repository.deletedAppleFeedback, isNull);
17+
});
18+
19+
test('deletes Google accounts through the Google revoke endpoint', () async {
20+
final repository = _FakeUserRepository(socialType: 'GOOGLE');
21+
final useCase = DeleteUserUseCase(repository);
22+
23+
await useCase('Switching apps');
24+
25+
expect(repository.deletedGoogleFeedback, 'Switching apps');
26+
expect(repository.didDisconnectGoogleSignIn, isTrue);
27+
expect(repository.deletedNormalFeedback, isNull);
28+
});
29+
30+
test('deletes Apple accounts through the Apple revoke endpoint', () async {
31+
final repository = _FakeUserRepository(socialType: 'apple');
32+
final useCase = DeleteUserUseCase(repository);
33+
34+
await useCase('Fresh start');
35+
36+
expect(repository.deletedAppleFeedback, 'Fresh start');
37+
expect(repository.deletedNormalFeedback, isNull);
38+
});
39+
}
40+
41+
class _FakeUserRepository implements UserRepository {
42+
_FakeUserRepository({required this.socialType});
43+
44+
final String? socialType;
45+
String? deletedNormalFeedback;
46+
String? deletedGoogleFeedback;
47+
String? deletedAppleFeedback;
48+
bool didDisconnectGoogleSignIn = false;
49+
50+
@override
51+
Stream<UserEntity> get userStream => const Stream.empty();
52+
53+
@override
54+
GoogleSignIn get googleSignIn => throw UnimplementedError();
55+
56+
@override
57+
Future<void> deleteAppleUser({String? feedbackMessage}) async {
58+
deletedAppleFeedback = feedbackMessage;
59+
}
60+
61+
@override
62+
Future<void> deleteGoogleUser({String? feedbackMessage}) async {
63+
deletedGoogleFeedback = feedbackMessage;
64+
}
65+
66+
@override
67+
Future<void> deleteUser({String? feedbackMessage}) async {
68+
deletedNormalFeedback = feedbackMessage;
69+
}
70+
71+
@override
72+
Future<void> disconnectGoogleSignIn() async {
73+
didDisconnectGoogleSignIn = true;
74+
}
75+
76+
@override
77+
Future<String?> getUserSocialType() async => socialType;
78+
79+
@override
80+
Future<void> getUser() => throw UnimplementedError();
81+
82+
@override
83+
Future<void> postFeedback(String message) => throw UnimplementedError();
84+
85+
@override
86+
Future<void> signIn({required String email, required String password}) =>
87+
throw UnimplementedError();
88+
89+
@override
90+
Future<void> signInWithApple({
91+
required String idToken,
92+
required String authCode,
93+
required String fullName,
94+
String? email,
95+
}) =>
96+
throw UnimplementedError();
97+
98+
@override
99+
Future<void> signInWithGoogle(GoogleSignInAccount account) =>
100+
throw UnimplementedError();
101+
102+
@override
103+
Future<void> signOut() => throw UnimplementedError();
104+
105+
@override
106+
Future<void> signUp({
107+
required String email,
108+
required String password,
109+
required String name,
110+
}) =>
111+
throw UnimplementedError();
112+
}

test/domain/use-cases/reconcile_alarms_use_case_test.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,16 @@ class FakeUserRepository implements UserRepository {
208208
}
209209

210210
@override
211-
Future<void> deleteAppleUser() => throw UnimplementedError();
211+
Future<void> deleteAppleUser({String? feedbackMessage}) =>
212+
throw UnimplementedError();
212213

213214
@override
214-
Future<void> deleteGoogleUser() => throw UnimplementedError();
215+
Future<void> deleteGoogleUser({String? feedbackMessage}) =>
216+
throw UnimplementedError();
215217

216218
@override
217-
Future<void> deleteUser() => throw UnimplementedError();
219+
Future<void> deleteUser({String? feedbackMessage}) =>
220+
throw UnimplementedError();
218221

219222
@override
220223
Future<void> disconnectGoogleSignIn() => throw UnimplementedError();

0 commit comments

Comments
 (0)