Skip to content

Commit 5842594

Browse files
authored
fix(cat-voices): handle duplicate email error (#2587)
* feat: handle resource conflict error * fix: missing props * feat: improve fallback error message * feat: throw EmailAlreadyInUseException * feat: add EmailAlreadyUsed exception * feat: handle email already in use * feat: handle error * chore: reformat
1 parent 103500e commit 5842594

File tree

17 files changed

+172
-15
lines changed

17 files changed

+172
-15
lines changed

catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/account/account_cubit.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ final class AccountCubit extends Cubit<AccountState>
4848
await _userService.resendActiveAccountVerificationEmail();
4949

5050
emitSignal(const AccountVerificationEmailSendSignal());
51+
} on EmailAlreadyUsedException catch (error, stackTrace) {
52+
_logger.severe('Re-send verification email - already used', error, stackTrace);
53+
emitError(const LocalizedEmailAlreadyUsedException());
5154
} catch (error, stackTrace) {
5255
_logger.severe('Re-send verification email', error, stackTrace);
5356
emitError(LocalizedException.create(error));
@@ -82,6 +85,10 @@ final class AccountCubit extends Cubit<AccountState>
8285
emitSignal(const AccountVerificationEmailSendSignal());
8386

8487
return true;
88+
} on EmailAlreadyUsedException {
89+
_logger.info('Email already used');
90+
emitError(const LocalizedEmailAlreadyUsedException());
91+
return false;
8592
} catch (error, stackTrace) {
8693
_logger.severe('Update email', error, stackTrace);
8794
emitError(LocalizedException.create(error));

catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,23 @@ final class RegistrationCubit extends Cubit<RegistrationState> with BlocErrorEmi
184184
isSubmittingTx: false,
185185
),
186186
);
187+
} on EmailAlreadyUsedException {
188+
_logger.info('Email already in use');
189+
190+
emitError(const LocalizedRegistrationEmailAlreadyUsedException());
191+
192+
_onRegistrationStateDataChanged(
193+
_registrationState.copyWith(
194+
isSubmittingTx: false,
195+
),
196+
);
197+
198+
_progressNotifier.clear();
199+
200+
// Since the RBAC registration is done at this point email error
201+
// doesn't prevent the registration from finishing, later the user will
202+
// have to update their email in the account page.
203+
nextStep();
187204
} catch (error, stack) {
188205
_logger.severe('Submit registration failed', error, stack);
189206

catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,10 @@
786786
"@somethingWentWrong": {
787787
"description": "Error description when something goes wrong."
788788
},
789+
"resourceConflictError": "Provided data is not unique.",
790+
"@resourceConflictError": {
791+
"description": "Error description when there's duplicate data (i.e. email or username already taken)."
792+
},
789793
"noWalletFound": "No wallet found.",
790794
"@noWalletFound": {
791795
"description": "A description when no wallet extension was found."
@@ -1462,6 +1466,14 @@
14621466
},
14631467
"errorEmailValidationPattern": "Incorrect email pattern",
14641468
"errorEmailValidationOutOfRange": "Invalid length",
1469+
"errorEmailAlreadyInUseUpdateLater": "This email address is already in use. You can update your email later from your account settings.",
1470+
"@errorEmailAlreadyInUseUpdateLater": {
1471+
"description": "Error when trying to register with email that is taken already (in registration)."
1472+
},
1473+
"errorEmailAlreadyInUse": "This email address is already in use.",
1474+
"@errorEmailAlreadyInUse": {
1475+
"description": "Error when trying to update email that is taken already."
1476+
},
14651477
"categoryDetails": "Category Details",
14661478
"fundsAvailable": "Funds Available",
14671479
"minBudgetRequest": "Min budget request",

catalyst_voices/packages/internal/catalyst_voices_models/lib/src/api/exception/api_error_response_exception.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ final class ApiErrorResponseException extends ApiException {
1616
/// In context of request.
1717
static const int notFound = 404;
1818

19+
/// A request could not be completed due to a conflict
20+
/// with the current state of the target resource.
21+
static const int conflict = 409;
22+
1923
/// The client has not sent valid data in its request, headers, parameters
2024
/// or body.
2125
static const int preconditionFailed = 412;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'package:equatable/equatable.dart';
2+
3+
final class EmailAlreadyUsedException extends Equatable implements Exception {
4+
const EmailAlreadyUsedException();
5+
6+
@override
7+
List<Object?> get props => [];
8+
9+
@override
10+
String toString() => 'EmailAlreadyUsedException';
11+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export 'crypto_exception.dart';
2+
export 'email_already_used_exception.dart';
23
export 'network_error.dart';
34
export 'not_found_exception.dart';
5+
export 'resource_conflict_exception.dart';
46
export 'secure_storage_error.dart';
57
export 'vault_exception.dart';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:equatable/equatable.dart';
2+
3+
final class ResourceConflictException extends Equatable implements Exception {
4+
final String? message;
5+
6+
const ResourceConflictException({this.message});
7+
8+
@override
9+
List<Object?> get props => [message];
10+
11+
@override
12+
String toString() {
13+
if (message != null) {
14+
return 'ResourceConflictException: $message';
15+
}
16+
return 'ResourceConflictException';
17+
}
18+
}

catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/common/response_mapper.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ extension ResponseMapper<T> on chopper.Response<T> {
2323
return bodyBytes;
2424
} else if (statusCode == ApiErrorResponseException.notFound) {
2525
throw NotFoundException(message: error.toString());
26+
} else if (statusCode == ApiErrorResponseException.conflict) {
27+
throw ResourceConflictException(message: _extractErrorMessage(error));
2628
} else {
2729
throw toApiException();
2830
}
@@ -32,7 +34,9 @@ extension ResponseMapper<T> on chopper.Response<T> {
3234
if (isSuccessful) {
3335
return bodyOrThrow;
3436
} else if (statusCode == ApiErrorResponseException.notFound) {
35-
throw NotFoundException(message: error.toString());
37+
throw NotFoundException(message: _extractErrorMessage(error));
38+
} else if (statusCode == ApiErrorResponseException.conflict) {
39+
throw ResourceConflictException(message: _extractErrorMessage(error));
3640
} else {
3741
throw toApiException();
3842
}
@@ -44,4 +48,8 @@ extension ResponseMapper<T> on chopper.Response<T> {
4448
error: error,
4549
);
4650
}
51+
52+
String? _extractErrorMessage(Object? error) {
53+
return error?.toString();
54+
}
4755
}

catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_repository.dart

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ abstract interface class UserRepository {
2828

2929
Future<User> getUser();
3030

31+
/// Throws [EmailAlreadyUsedException] if [email] already taken.
3132
Future<void> publishUserProfile({
3233
required CatalystId catalystId,
3334
required String email,
@@ -91,14 +92,18 @@ final class UserRepositoryImpl implements UserRepository {
9192
required CatalystId catalystId,
9293
required String email,
9394
}) async {
94-
await _apiServices.reviews
95-
.apiCatalystIdsMePost(
96-
body: CatalystIDCreate(
97-
catalystIdUri: catalystId.toUri().toStringWithoutScheme(),
98-
email: email,
99-
),
100-
)
101-
.successBodyOrThrow();
95+
try {
96+
await _apiServices.reviews
97+
.apiCatalystIdsMePost(
98+
body: CatalystIDCreate(
99+
catalystIdUri: catalystId.toUri().toStringWithoutScheme(),
100+
email: email,
101+
),
102+
)
103+
.successBodyOrThrow();
104+
} on ResourceConflictException {
105+
throw const EmailAlreadyUsedException();
106+
}
102107
}
103108

104109
@override

catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/common/response_mapper_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ void main() {
5353
);
5454
});
5555

56+
test('successBodyBytesOrThrow throws $ResourceConflictException for 409', () {
57+
final response = mockBinaryResponse(
58+
statusCode: HttpStatus.conflict,
59+
);
60+
61+
expect(
62+
response.successBodyBytesOrThrow,
63+
throwsA(const ResourceConflictException()),
64+
);
65+
});
66+
5667
test('successBodyBytesOrThrow throws $ApiErrorResponseException otherwise', () {
5768
final response = mockBinaryResponse(
5869
statusCode: HttpStatus.internalServerError,
@@ -82,6 +93,17 @@ void main() {
8293
);
8394
});
8495

96+
test('successBodyOrThrow throws $ResourceConflictException for 409', () {
97+
final response = mockResponse(
98+
statusCode: HttpStatus.conflict,
99+
);
100+
101+
expect(
102+
response.successBodyOrThrow,
103+
throwsA(const ResourceConflictException()),
104+
);
105+
});
106+
85107
test('successBodyOrThrow throws $ApiErrorResponseException otherwise', () {
86108
final response = mockResponse(
87109
statusCode: HttpStatus.internalServerError,

0 commit comments

Comments
 (0)