Skip to content

Commit b12cd35

Browse files
authored
feat(cat-voices): Send email to cat reviews (#2218)
* feat: send email to cat-reviews * chore: todo for previous transaction ID * feat: send email to cat-reviews * feat: revert updates if not saved * feat: register account and send email * chore: cleanup * chore: cleanup * chore: improve logs * chore: reformat
1 parent 1d22916 commit b12cd35

File tree

16 files changed

+338
-197
lines changed

16 files changed

+338
-197
lines changed

catalyst_voices/apps/voices/lib/dependency/dependencies.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,13 @@ final class Dependencies extends DependencyProvider {
162162

163163
void _registerRepositories() {
164164
this
165+
..registerLazySingleton<UserDataSource>(() {
166+
return ApiUserDataSource(get<ApiServices>());
167+
})
165168
..registerLazySingleton<UserRepository>(() {
166169
return UserRepository(
167170
get<UserStorage>(),
171+
get<UserDataSource>(),
168172
get<KeychainProvider>(),
169173
);
170174
})

catalyst_voices/apps/voices/lib/pages/account/account_page.dart

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22

3+
import 'package:catalyst_voices/common/error_handler.dart';
34
import 'package:catalyst_voices/pages/account/widgets/account_action_tile.dart';
45
import 'package:catalyst_voices/pages/account/widgets/account_email_tile.dart';
56
import 'package:catalyst_voices/pages/account/widgets/account_header_tile.dart';
@@ -23,14 +24,8 @@ final class AccountPage extends StatefulWidget {
2324
State<AccountPage> createState() => _AccountPageState();
2425
}
2526

26-
class _AccountPageState extends State<AccountPage> {
27-
@override
28-
void initState() {
29-
super.initState();
30-
31-
unawaited(context.read<AccountCubit>().loadAccountDetails());
32-
}
33-
27+
class _AccountPageState extends State<AccountPage>
28+
with ErrorHandlerStateMixin<AccountCubit, AccountPage> {
3429
@override
3530
Widget build(BuildContext context) {
3631
return Scaffold(
@@ -98,4 +93,11 @@ class _AccountPageState extends State<AccountPage> {
9893
),
9994
);
10095
}
96+
97+
@override
98+
void initState() {
99+
super.initState();
100+
101+
unawaited(context.read<AccountCubit>().loadAccountDetails());
102+
}
101103
}

catalyst_voices/apps/voices/lib/pages/account/widgets/account_email_tile.dart

Lines changed: 56 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,39 @@ class _AccountEmailTileState extends State<AccountEmailTile> {
2323
Email _email = const Email.pure();
2424
StreamSubscription<Email>? _sub;
2525

26+
@override
27+
Widget build(BuildContext context) {
28+
return EditableTile(
29+
title: context.l10n.emailAddress,
30+
key: const Key('AccountEmailTile'),
31+
onChanged: _onEditModeChange,
32+
isEditMode: _isEditMode,
33+
isSaveEnabled: _email.isValid,
34+
child: VoicesEmailTextField(
35+
key: const Key('AccountEmailTextField'),
36+
controller: _controller,
37+
focusNode: _focusNode,
38+
decoration: VoicesTextFieldDecoration(
39+
hintText: context.l10n.emailAddress,
40+
errorText: _email.displayError?.message(context),
41+
),
42+
onFieldSubmitted: null,
43+
readOnly: !_isEditMode,
44+
maxLength: _isEditMode ? Email.lengthRange.max : null,
45+
),
46+
);
47+
}
48+
49+
@override
50+
void dispose() {
51+
unawaited(_sub?.cancel());
52+
_sub = null;
53+
54+
_focusNode.dispose();
55+
_controller.dispose();
56+
super.dispose();
57+
}
58+
2659
@override
2760
void initState() {
2861
super.initState();
@@ -42,37 +75,23 @@ class _AccountEmailTileState extends State<AccountEmailTile> {
4275
.listen(_handleEmailChange);
4376
}
4477

45-
@override
46-
void dispose() {
47-
unawaited(_sub?.cancel());
48-
_sub = null;
78+
void _handleControllerChange() {
79+
setState(() {
80+
_email = Email.dirty(_controller.text);
81+
});
82+
}
4983

50-
_focusNode.dispose();
51-
_controller.dispose();
52-
super.dispose();
84+
void _handleEmailChange(Email email) {
85+
if (_isEditMode) {
86+
return;
87+
}
88+
89+
_controller.textWithSelection = email.value;
5390
}
5491

55-
@override
56-
Widget build(BuildContext context) {
57-
return EditableTile(
58-
title: context.l10n.emailAddress,
59-
key: const Key('AccountEmailTile'),
60-
onChanged: _onEditModeChange,
61-
isEditMode: _isEditMode,
62-
isSaveEnabled: _email.isValid,
63-
child: VoicesEmailTextField(
64-
key: const Key('AccountEmailTextField'),
65-
controller: _controller,
66-
focusNode: _focusNode,
67-
decoration: VoicesTextFieldDecoration(
68-
hintText: context.l10n.emailAddress,
69-
errorText: _email.displayError?.message(context),
70-
),
71-
onFieldSubmitted: null,
72-
readOnly: !_isEditMode,
73-
maxLength: _isEditMode ? Email.lengthRange.max : null,
74-
),
75-
);
92+
void _onCancel() {
93+
final email = context.read<AccountCubit>().state.email;
94+
_controller.textWithSelection = email.value;
7695
}
7796

7897
void _onEditModeChange(EditableTileChange value) {
@@ -89,31 +108,20 @@ class _AccountEmailTileState extends State<AccountEmailTile> {
89108
_onCancel();
90109
}
91110
case EditableTileChangeSource.save:
92-
_onSave();
111+
unawaited(_onSave());
93112
}
94113
});
95114
}
96115

97-
void _onCancel() {
98-
final email = context.read<AccountCubit>().state.email;
99-
_controller.textWithSelection = email.value;
100-
}
101-
102-
void _onSave() {
103-
unawaited(context.read<AccountCubit>().updateEmail(_email));
104-
}
116+
Future<void> _onSave() async {
117+
final cubit = context.read<AccountCubit>();
118+
final updated = await cubit.updateEmail(_email);
105119

106-
void _handleControllerChange() {
107-
setState(() {
108-
_email = Email.dirty(_controller.text);
109-
});
110-
}
111-
112-
void _handleEmailChange(Email email) {
113-
if (_isEditMode) {
114-
return;
120+
if (!updated && mounted) {
121+
setState(() {
122+
_email = cubit.state.email;
123+
_controller.textWithSelection = _email.value;
124+
});
115125
}
116-
117-
_controller.textWithSelection = email.value;
118126
}
119127
}

catalyst_voices/apps/voices/lib/pages/account/widgets/account_username_tile.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,20 @@ class _AccountUsernameTileState extends State<AccountUsernameTile> {
112112
_onCancel();
113113
}
114114
case EditableTileChangeSource.save:
115-
_onSave();
115+
unawaited(_onSave());
116116
}
117117
});
118118
}
119119

120-
void _onSave() {
121-
unawaited(context.read<AccountCubit>().updateUsername(_username));
120+
Future<void> _onSave() async {
121+
final cubit = context.read<AccountCubit>();
122+
final updated = await cubit.updateUsername(_username);
123+
124+
if (!updated && mounted) {
125+
setState(() {
126+
_username = cubit.state.username;
127+
_controller.textWithSelection = _username.value;
128+
});
129+
}
122130
}
123131
}

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

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import 'dart:async';
22

33
import 'package:catalyst_voices_blocs/src/account/account_state.dart';
4+
import 'package:catalyst_voices_blocs/src/common/bloc_error_emitter_mixin.dart';
45
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
56
import 'package:catalyst_voices_services/catalyst_voices_services.dart';
7+
import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
68
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
79
import 'package:flutter_bloc/flutter_bloc.dart';
810

9-
final class AccountCubit extends Cubit<AccountState> {
11+
final class AccountCubit extends Cubit<AccountState>
12+
with BlocErrorEmitterMixin {
13+
final _logger = Logger('AccountCubit');
1014
final UserService _userService;
1115

1216
StreamSubscription<Account?>? _accountSub;
@@ -39,38 +43,54 @@ final class AccountCubit extends Cubit<AccountState> {
3943
// TODO(damian-molinski): Integration
4044
}
4145

42-
Future<void> updateEmail(Email email) async {
43-
if (email.isNotValid) {
44-
return;
46+
/// Returns true if updated, false otherwise.
47+
Future<bool> updateEmail(Email email) async {
48+
try {
49+
if (email.isNotValid) {
50+
return false;
51+
}
52+
53+
final activeAccount = _userService.user.activeAccount;
54+
if (activeAccount != null) {
55+
await _userService.updateAccount(
56+
id: activeAccount.catalystId,
57+
email: email.value,
58+
);
59+
}
60+
61+
emit(state.copyWith(email: email));
62+
return true;
63+
} catch (error, stackTrace) {
64+
_logger.severe('Update email', error, stackTrace);
65+
emitError(LocalizedException.create(error));
66+
return false;
4567
}
46-
47-
final activeAccount = _userService.user.activeAccount;
48-
if (activeAccount != null) {
49-
await _userService.updateAccount(
50-
id: activeAccount.catalystId,
51-
email: email.value,
52-
);
53-
}
54-
55-
emit(state.copyWith(email: email));
5668
}
5769

58-
Future<void> updateUsername(Username username) async {
59-
if (username.isNotValid) {
60-
return;
70+
/// Returns true if updated, false otherwise.
71+
Future<bool> updateUsername(Username username) async {
72+
try {
73+
if (username.isNotValid) {
74+
return false;
75+
}
76+
77+
final activeAccount = _userService.user.activeAccount;
78+
if (activeAccount != null) {
79+
final value = username.value;
80+
81+
await _userService.updateAccount(
82+
id: activeAccount.catalystId,
83+
username: value.isNotEmpty ? Optional(value) : const Optional.empty(),
84+
);
85+
}
86+
87+
emit(state.copyWith(username: username));
88+
return true;
89+
} catch (error, stackTrace) {
90+
_logger.severe('Update username', error, stackTrace);
91+
emitError(LocalizedException.create(error));
92+
return false;
6193
}
62-
63-
final activeAccount = _userService.user.activeAccount;
64-
if (activeAccount != null) {
65-
final value = username.value;
66-
67-
await _userService.updateAccount(
68-
id: activeAccount.catalystId,
69-
username: value.isNotEmpty ? Optional(value) : const Optional.empty(),
70-
);
71-
}
72-
73-
emit(state.copyWith(username: username));
7494
}
7595

7696
void _handleActiveAccountChange(Account? account) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ final class RegistrationCubit extends Cubit<RegistrationState>
152152
case AccountSubmitFullData():
153153
final account = await _registrationService.register(data: submitData);
154154

155-
await _userService.useAccount(account);
155+
await _userService.registerAccount(account);
156+
156157
case AccountSubmitUpdateData(
157158
:final metadata,
158159
:final accountId,

catalyst_voices/packages/internal/catalyst_voices_blocs/test/session/session_cubit_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ void main() {
4242
);
4343
userRepository = UserRepository(
4444
SecureUserStorage(),
45+
_MockUserDataSource(),
4546
keychainProvider,
4647
);
4748
userObserver = StreamUserObserver();
@@ -338,3 +339,5 @@ class _MockRegistrationService extends Mock implements RegistrationService {
338339
);
339340
}
340341
}
342+
343+
class _MockUserDataSource extends Mock implements UserDataSource {}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ export 'signed_document/signed_document_manager.dart'
2020
show SignedDocumentManager;
2121
export 'signed_document/signed_document_manager_impl.dart'
2222
show SignedDocumentManagerImpl;
23+
export 'user/source/user_data_source.dart';
24+
export 'user/source/user_storage.dart';
2325
export 'user/user_repository.dart' show UserRepository;
24-
export 'user/user_storage.dart';

0 commit comments

Comments
 (0)