Skip to content

Commit

Permalink
feat(ion identity client): improve errors for register and login (#229)
Browse files Browse the repository at this point in the history
### What does this PR do?
Improves error handling and displays error messages in the login and
registration forms.

### Changes brought by this PR:
- **IdentityKeyNameInput**: Added `errorText` prop to show validation
errors.
- **LoginForm & SignUpPasskeyForm**: Pass `errorText` to
`IdentityKeyNameInput`.
- **LoginDataSource & RegisterDataSource**: Better network error
handling (e.g., account deactivated, invalid code).
- **Failure types**: Added new failure cases with clearer error
messages.
  • Loading branch information
ice-tychon authored Oct 11, 2024
1 parent 9840dff commit ca562ba
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import 'package:ice/generated/assets.gen.dart';
class IdentityKeyNameInput extends HookWidget {
const IdentityKeyNameInput({
this.textInputAction = TextInputAction.done,
super.key,
this.controller,
this.scrollPadding,
this.notShowInfoIcon = false,
this.errorText,
super.key,
});

final TextEditingController? controller;
Expand All @@ -27,6 +28,8 @@ class IdentityKeyNameInput extends HookWidget {
final EdgeInsets? scrollPadding;
final bool notShowInfoIcon;

final String? errorText;

@override
Widget build(BuildContext context) {
final hideKeyboardAndCallOnce = useHideKeyboardAndCallOnce();
Expand Down Expand Up @@ -67,6 +70,7 @@ class IdentityKeyNameInput extends HookWidget {
},
textInputAction: textInputAction,
scrollPadding: scrollPadding ?? EdgeInsets.only(bottom: 100.0.s),
errorText: errorText,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class LoginForm extends HookConsumerWidget {
child: Column(
children: [
IdentityKeyNameInput(
errorText: loginActionState.error?.toString(),
controller: identityKeyNameController,
scrollPadding: EdgeInsets.only(bottom: 190.0.s),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ class SignUpPasskeyForm extends HookConsumerWidget {
key: formKey.value,
child: Column(
children: [
IdentityKeyNameInput(controller: identityKeyNameController),
IdentityKeyNameInput(
errorText: registerActionState.error?.toString(),
controller: identityKeyNameController,
),
SizedBox(height: 16.0.s),
Button(
disabled: registerActionState.isLoading,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: ice License 1.0

import 'package:dio/dio.dart';
import 'package:ion_identity_client/src/auth/dtos/dtos.dart';
import 'package:ion_identity_client/src/auth/result_types/login_user_result.dart';
import 'package:ion_identity_client/src/core/network/network.dart';
Expand Down Expand Up @@ -28,7 +29,11 @@ class LoginDataSource {
decoder: UserActionChallenge.fromJson,
)
.mapLeft(
(l) => const UnknownLoginUserFailure(),
(l) => switch (l) {
RequestExecutionNetworkFailure(:final error) =>
_handleRequestExecutionNetworkFailure(error),
_ => UnknownLoginUserFailure(l),
},
);
}

Expand All @@ -48,7 +53,24 @@ class LoginDataSource {
decoder: Authentication.fromJson,
)
.mapLeft(
(l) => const UnknownLoginUserFailure(),
(l) => switch (l) {
RequestExecutionNetworkFailure(:final error) =>
_handleRequestExecutionNetworkFailure(error),
_ => UnknownLoginUserFailure(l),
},
);
}

LoginUserFailure _handleRequestExecutionNetworkFailure(Object error) {
switch (error) {
case DioException(:final response) when response?.statusCode == 400:
return const AccountDeactivatedLoginUserFailure();
case DioException(:final response) when response?.statusCode == 401:
return const NoCredentialsLoginUserFailure();
case DioException(:final response) when response?.statusCode == 403:
return const InvalidCodeLoginUserFailure();
default:
return const UnknownLoginUserFailure();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: ice License 1.0

import 'package:dio/dio.dart';
import 'package:ion_identity_client/src/auth/dtos/dtos.dart';
import 'package:ion_identity_client/src/auth/result_types/register_user_result.dart';
import 'package:ion_identity_client/src/core/network/network.dart';
Expand Down Expand Up @@ -29,8 +30,9 @@ class RegisterDataSource {
)
.mapLeft(
(l) => switch (l) {
ResponseFormatNetworkFailure() => UserAlreadyExistsRegisterUserFailure(),
_ => const UnknownRegisterUserFailure(),
RequestExecutionNetworkFailure(:final error) =>
_handleRequestExecutionNetworkFailure(error),
_ => UnknownRegisterUserFailure(l),
},
);
}
Expand All @@ -39,19 +41,30 @@ class RegisterDataSource {
required Fido2Attestation attestation,
required String temporaryAuthenticationToken,
}) {
return networkClient.post(
registerCompletePath,
data: SignedChallenge(firstFactorCredential: attestation).toJson(),
decoder: RegistrationCompleteResponse.fromJson,
headers: {
...RequestHeaders.getAuthorizationHeader(token: temporaryAuthenticationToken),
},
).mapLeft(
// TODO: find a complete list of user registration failures
(l) => switch (l) {
ResponseFormatNetworkFailure() => UserAlreadyExistsRegisterUserFailure(),
_ => const UnknownRegisterUserFailure(),
},
);
return networkClient
.post(
registerCompletePath,
data: SignedChallenge(firstFactorCredential: attestation).toJson(),
decoder: RegistrationCompleteResponse.fromJson,
headers: RequestHeaders.getAuthorizationHeader(token: temporaryAuthenticationToken),
)
.mapLeft(
(l) => switch (l) {
RequestExecutionNetworkFailure(:final error) =>
_handleRequestExecutionNetworkFailure(error),
_ => UnknownRegisterUserFailure(l),
},
);
}

RegisterUserFailure _handleRequestExecutionNetworkFailure(Object error) {
switch (error) {
case DioException(:final response) when response?.statusCode == 400:
return RegistrationCodeExpiredRegisterUserFailure();
case DioException(:final response) when response?.statusCode == 401:
return UserAlreadyExistsRegisterUserFailure();
default:
return const UnknownRegisterUserFailure();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ sealed class LoginUserFailure extends LoginUserResult {

final class PasskeyNotAvailableLoginUserFailure extends LoginUserFailure {
const PasskeyNotAvailableLoginUserFailure();

@override
String toString() => 'Passkey not available';
}

final class PasskeyValidationLoginUserFailure extends LoginUserFailure {
Expand All @@ -24,7 +27,28 @@ final class PasskeyValidationLoginUserFailure extends LoginUserFailure {
final StackTrace? stackTrace;

@override
String toString() => 'PasskeyValidationLoginUserFailure(error: $error, stackTrace: $stackTrace)';
String toString() => 'Passkey validation failed: $error';
}

final class InvalidCodeLoginUserFailure extends LoginUserFailure {
const InvalidCodeLoginUserFailure();

@override
String toString() => 'Invalid code';
}

final class NoCredentialsLoginUserFailure extends LoginUserFailure {
const NoCredentialsLoginUserFailure();

@override
String toString() => 'No credentials found for this user';
}

final class AccountDeactivatedLoginUserFailure extends LoginUserFailure {
const AccountDeactivatedLoginUserFailure();

@override
String toString() => 'Account has been deactivated';
}

final class UnknownLoginUserFailure extends LoginUserFailure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,21 @@ sealed class RegisterUserFailure extends RegisterUserResult {
const RegisterUserFailure();
}

final class UserAlreadyExistsRegisterUserFailure extends RegisterUserFailure {}
final class UserAlreadyExistsRegisterUserFailure extends RegisterUserFailure {
@override
String toString() => 'User already exists';
}

final class RegistrationCodeExpiredRegisterUserFailure extends RegisterUserFailure {
@override
String toString() => 'Registration code expired';
}

final class PasskeyNotAvailableRegisterUserFailure extends RegisterUserFailure {
const PasskeyNotAvailableRegisterUserFailure();

@override
String toString() => 'Passkey not available';
}

final class PasskeyValidationRegisterUserFailure extends RegisterUserFailure {
Expand All @@ -26,8 +37,7 @@ final class PasskeyValidationRegisterUserFailure extends RegisterUserFailure {
final StackTrace? stackTrace;

@override
String toString() =>
'PasskeyValidationRegisterUserFailure(error: $error, stackTrace: $stackTrace)';
String toString() => 'Passkey validation failed: $error';
}

final class UnknownRegisterUserFailure extends RegisterUserFailure {
Expand All @@ -40,5 +50,5 @@ final class UnknownRegisterUserFailure extends RegisterUserFailure {
final StackTrace? stackTrace;

@override
String toString() => 'UnknownRegisterUserFailure(error: $error, stackTrace: $stackTrace)';
String toString() => 'Unknown error: $error';
}

0 comments on commit ca562ba

Please sign in to comment.