mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-19 12:24:53 -04:00
feat: setup/change password in settings page (#7752)
* feat: change password in settings page * feat: add change password api * feat: add password service * feat: add setup password * feat: refacotor account page * chor: update i18n * chore: i18n * chore: i18n * feat: add error message under text field * fix: flutter tests * chore: remove long password test * fix: cloud integration test * fix: cargo clippy * fix: replace border color
This commit is contained in:
parent
80df4955e2
commit
3bb5075a98
42 changed files with 1854 additions and 264 deletions
|
@ -79,7 +79,7 @@ extension AppFlowySettings on WidgetTester {
|
|||
// Enable editing username
|
||||
final editUsernameFinder = find.descendant(
|
||||
of: find.byType(AccountUserProfile),
|
||||
matching: find.byFlowySvg(FlowySvgs.edit_s),
|
||||
matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m),
|
||||
);
|
||||
await tap(editUsernameFinder, warnIfMissed: false);
|
||||
await pumpAndSettle();
|
||||
|
|
|
@ -58,7 +58,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
|
|||
userName: userName,
|
||||
onSubmitted: (value) => context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserName(value)),
|
||||
.add(SettingsUserEvent.updateUserName(name: value)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/user/application/password/password_http_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'password_bloc.freezed.dart';
|
||||
|
||||
class PasswordBloc extends Bloc<PasswordEvent, PasswordState> {
|
||||
PasswordBloc(this.userProfile) : super(PasswordState.initial()) {
|
||||
on<PasswordEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
init: () async => _init(),
|
||||
changePassword: (oldPassword, newPassword) async => _onChangePassword(
|
||||
emit,
|
||||
oldPassword: oldPassword,
|
||||
newPassword: newPassword,
|
||||
),
|
||||
setupPassword: (newPassword) async => _onSetupPassword(
|
||||
emit,
|
||||
newPassword: newPassword,
|
||||
),
|
||||
forgotPassword: (email) async => _onForgotPassword(
|
||||
emit,
|
||||
email: email,
|
||||
),
|
||||
checkHasPassword: () async => _onCheckHasPassword(
|
||||
emit,
|
||||
),
|
||||
cancel: () {},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
late final PasswordHttpService passwordHttpService;
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
Future<void> _init() async {
|
||||
if (userProfile.authenticator == AuthenticatorPB.Local) {
|
||||
Log.debug('PasswordBloc: skip init because user is local authenticator');
|
||||
return;
|
||||
}
|
||||
|
||||
final baseUrl = await getAppFlowyCloudUrl();
|
||||
try {
|
||||
final authToken = jsonDecode(userProfile.token)['access_token'];
|
||||
passwordHttpService = PasswordHttpService(
|
||||
baseUrl: baseUrl,
|
||||
authToken: authToken,
|
||||
);
|
||||
_isInitialized = true;
|
||||
} catch (e) {
|
||||
Log.error('PasswordBloc: _init: error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChangePassword(
|
||||
Emitter<PasswordState> emit, {
|
||||
required String oldPassword,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
Log.info('changePassword: not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isSubmitting) {
|
||||
Log.info('changePassword: already submitting');
|
||||
return;
|
||||
}
|
||||
|
||||
_clearState(emit, true);
|
||||
|
||||
final result = await passwordHttpService.changePassword(
|
||||
currentPassword: oldPassword,
|
||||
newPassword: newPassword,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
changePasswordResult: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSetupPassword(
|
||||
Emitter<PasswordState> emit, {
|
||||
required String newPassword,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
Log.info('setupPassword: not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isSubmitting) {
|
||||
Log.info('setupPassword: already submitting');
|
||||
return;
|
||||
}
|
||||
|
||||
_clearState(emit, true);
|
||||
|
||||
final result = await passwordHttpService.setupPassword(
|
||||
newPassword: newPassword,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
hasPassword: result.fold(
|
||||
(success) => true,
|
||||
(error) => false,
|
||||
),
|
||||
setupPasswordResult: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onForgotPassword(
|
||||
Emitter<PasswordState> emit, {
|
||||
required String email,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
Log.info('forgotPassword: not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isSubmitting) {
|
||||
Log.info('forgotPassword: already submitting');
|
||||
return;
|
||||
}
|
||||
|
||||
_clearState(emit, true);
|
||||
|
||||
final result = await passwordHttpService.forgotPassword(email: email);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
forgotPasswordResult: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onCheckHasPassword(Emitter<PasswordState> emit) async {
|
||||
if (!_isInitialized) {
|
||||
Log.info('checkHasPassword: not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isSubmitting) {
|
||||
Log.info('checkHasPassword: already submitting');
|
||||
return;
|
||||
}
|
||||
|
||||
_clearState(emit, true);
|
||||
|
||||
final result = await passwordHttpService.checkHasPassword();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
hasPassword: result.fold(
|
||||
(success) => success,
|
||||
(error) => false,
|
||||
),
|
||||
checkHasPasswordResult: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _clearState(Emitter<PasswordState> emit, bool isSubmitting) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: isSubmitting,
|
||||
changePasswordResult: null,
|
||||
setupPasswordResult: null,
|
||||
forgotPasswordResult: null,
|
||||
checkHasPasswordResult: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class PasswordEvent with _$PasswordEvent {
|
||||
const factory PasswordEvent.init() = Init;
|
||||
|
||||
// Change password
|
||||
const factory PasswordEvent.changePassword({
|
||||
required String oldPassword,
|
||||
required String newPassword,
|
||||
}) = ChangePassword;
|
||||
|
||||
// Setup password
|
||||
const factory PasswordEvent.setupPassword({
|
||||
required String newPassword,
|
||||
}) = SetupPassword;
|
||||
|
||||
// Forgot password
|
||||
const factory PasswordEvent.forgotPassword({
|
||||
required String email,
|
||||
}) = ForgotPassword;
|
||||
|
||||
// Check has password
|
||||
const factory PasswordEvent.checkHasPassword() = CheckHasPassword;
|
||||
|
||||
// Cancel operation
|
||||
const factory PasswordEvent.cancel() = Cancel;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class PasswordState with _$PasswordState {
|
||||
const factory PasswordState({
|
||||
required bool isSubmitting,
|
||||
required bool hasPassword,
|
||||
required FlowyResult<bool, FlowyError>? changePasswordResult,
|
||||
required FlowyResult<bool, FlowyError>? setupPasswordResult,
|
||||
required FlowyResult<bool, FlowyError>? forgotPasswordResult,
|
||||
required FlowyResult<bool, FlowyError>? checkHasPasswordResult,
|
||||
}) = _PasswordState;
|
||||
|
||||
factory PasswordState.initial() => const PasswordState(
|
||||
isSubmitting: false,
|
||||
hasPassword: false,
|
||||
changePasswordResult: null,
|
||||
setupPasswordResult: null,
|
||||
forgotPasswordResult: null,
|
||||
checkHasPasswordResult: null,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
enum PasswordEndpoint {
|
||||
changePassword,
|
||||
forgotPassword,
|
||||
setupPassword,
|
||||
checkHasPassword;
|
||||
|
||||
String get path {
|
||||
switch (this) {
|
||||
case PasswordEndpoint.changePassword:
|
||||
return '/gotrue/user/change-password';
|
||||
case PasswordEndpoint.forgotPassword:
|
||||
return '/gotrue/user/recover';
|
||||
case PasswordEndpoint.setupPassword:
|
||||
return '/gotrue/user/change-password';
|
||||
case PasswordEndpoint.checkHasPassword:
|
||||
return '/gotrue/user/auth-info';
|
||||
}
|
||||
}
|
||||
|
||||
String get method {
|
||||
switch (this) {
|
||||
case PasswordEndpoint.changePassword:
|
||||
case PasswordEndpoint.setupPassword:
|
||||
case PasswordEndpoint.forgotPassword:
|
||||
return 'POST';
|
||||
case PasswordEndpoint.checkHasPassword:
|
||||
return 'GET';
|
||||
}
|
||||
}
|
||||
|
||||
Uri uri(String baseUrl) => Uri.parse('$baseUrl$path');
|
||||
}
|
||||
|
||||
class PasswordHttpService {
|
||||
PasswordHttpService({
|
||||
required this.baseUrl,
|
||||
required this.authToken,
|
||||
});
|
||||
|
||||
final String baseUrl;
|
||||
final String authToken;
|
||||
|
||||
final http.Client client = http.Client();
|
||||
|
||||
Map<String, String> get headers => {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $authToken',
|
||||
};
|
||||
|
||||
/// Changes the user's password
|
||||
///
|
||||
/// [currentPassword] - The user's current password
|
||||
/// [newPassword] - The new password to set
|
||||
Future<FlowyResult<bool, FlowyError>> changePassword({
|
||||
required String currentPassword,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
final result = await _makeRequest(
|
||||
endpoint: PasswordEndpoint.changePassword,
|
||||
body: {
|
||||
'current_password': currentPassword,
|
||||
'password': newPassword,
|
||||
},
|
||||
errorMessage: 'Failed to change password',
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(data) => FlowyResult.success(true),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends a password reset email to the user
|
||||
///
|
||||
/// [email] - The email address of the user
|
||||
Future<FlowyResult<bool, FlowyError>> forgotPassword({
|
||||
required String email,
|
||||
}) async {
|
||||
final result = await _makeRequest(
|
||||
endpoint: PasswordEndpoint.forgotPassword,
|
||||
body: {'email': email},
|
||||
errorMessage: 'Failed to send password reset email',
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(data) => FlowyResult.success(true),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sets up a password for a user that doesn't have one
|
||||
///
|
||||
/// [newPassword] - The new password to set
|
||||
Future<FlowyResult<bool, FlowyError>> setupPassword({
|
||||
required String newPassword,
|
||||
}) async {
|
||||
final result = await _makeRequest(
|
||||
endpoint: PasswordEndpoint.setupPassword,
|
||||
body: {'password': newPassword},
|
||||
errorMessage: 'Failed to setup password',
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(data) => FlowyResult.success(true),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks if the user has a password set
|
||||
Future<FlowyResult<bool, FlowyError>> checkHasPassword() async {
|
||||
final result = await _makeRequest(
|
||||
endpoint: PasswordEndpoint.checkHasPassword,
|
||||
errorMessage: 'Failed to check password status',
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(data) => FlowyResult.success(data['has_password'] ?? false),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
}
|
||||
|
||||
/// Makes a request to the specified endpoint with the given body
|
||||
Future<FlowyResult<dynamic, FlowyError>> _makeRequest({
|
||||
required PasswordEndpoint endpoint,
|
||||
Map<String, dynamic>? body,
|
||||
String errorMessage = 'Request failed',
|
||||
}) async {
|
||||
try {
|
||||
final uri = endpoint.uri(baseUrl);
|
||||
http.Response response;
|
||||
|
||||
if (endpoint.method == 'POST') {
|
||||
response = await client.post(
|
||||
uri,
|
||||
headers: headers,
|
||||
body: body != null ? jsonEncode(body) : null,
|
||||
);
|
||||
} else if (endpoint.method == 'GET') {
|
||||
response = await client.get(
|
||||
uri,
|
||||
headers: headers,
|
||||
);
|
||||
} else {
|
||||
return FlowyResult.failure(
|
||||
FlowyError(msg: 'Invalid request method: ${endpoint.method}'),
|
||||
);
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
if (response.body.isNotEmpty) {
|
||||
return FlowyResult.success(jsonDecode(response.body));
|
||||
}
|
||||
return FlowyResult.success(true);
|
||||
} else {
|
||||
final errorBody =
|
||||
response.body.isNotEmpty ? jsonDecode(response.body) : {};
|
||||
|
||||
Log.info(
|
||||
'${endpoint.name} request failed: ${response.statusCode}, $errorBody ',
|
||||
);
|
||||
|
||||
return FlowyResult.failure(
|
||||
FlowyError(
|
||||
msg: errorBody['msg'] ?? errorMessage,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('${endpoint.name} request failed: error: $e');
|
||||
|
||||
return FlowyResult.failure(
|
||||
FlowyError(msg: 'Network error: ${e.toString()}'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -303,6 +303,8 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
|||
msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr();
|
||||
} else if (errorMsg.contains('invalid')) {
|
||||
msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr();
|
||||
} else if (errorMsg.contains('Invalid login credentials')) {
|
||||
msg = LocaleKeys.signIn_invalidLoginCredentials.tr();
|
||||
}
|
||||
return state.copyWith(
|
||||
isSubmitting: false,
|
||||
|
|
|
@ -95,6 +95,17 @@ class UserBackendService implements IUserBackendService {
|
|||
return UserEventPasscodeSignIn(payload).send();
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> signInWithPassword(
|
||||
String email,
|
||||
String password,
|
||||
) {
|
||||
final payload = SignInPayloadPB(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
return UserEventSignInWithEmailPassword(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> signOut() {
|
||||
return UserEventSignOut().send();
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||
import 'package:appflowy/user/application/sign_in_bloc.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
@ -50,11 +52,6 @@ class _ContinueWithEmailAndPasswordState
|
|||
);
|
||||
} else if (successOrFail == null && !state.isSubmitting) {
|
||||
emailKey.currentState?.clearError();
|
||||
|
||||
// _pushContinueWithMagicLinkOrPasscodePage(
|
||||
// context,
|
||||
// controller.text,
|
||||
// );
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
|
@ -76,13 +73,24 @@ class _ContinueWithEmailAndPasswordState
|
|||
controller.text,
|
||||
),
|
||||
),
|
||||
// VSpace(theme.spacing.l),
|
||||
// ContinueWithPassword(
|
||||
// onTap: () => _pushContinueWithPasswordPage(
|
||||
// context,
|
||||
// controller.text,
|
||||
// ),
|
||||
// ),
|
||||
VSpace(theme.spacing.l),
|
||||
ContinueWithPassword(
|
||||
onTap: () {
|
||||
final email = controller.text;
|
||||
|
||||
if (!isEmail(email)) {
|
||||
emailKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.signIn_invalidEmail.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_pushContinueWithPasswordPage(
|
||||
context,
|
||||
email,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -147,31 +155,34 @@ class _ContinueWithEmailAndPasswordState
|
|||
_hasPushedContinueWithMagicLinkOrPasscodePage = true;
|
||||
}
|
||||
|
||||
// void _pushContinueWithPasswordPage(
|
||||
// BuildContext context,
|
||||
// String email,
|
||||
// ) {
|
||||
// final signInBloc = context.read<SignInBloc>();
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => BlocProvider.value(
|
||||
// value: signInBloc,
|
||||
// child: ContinueWithPasswordPage(
|
||||
// email: email,
|
||||
// backToLogin: () => Navigator.pop(context),
|
||||
// onEnterPassword: (password) => signInBloc.add(
|
||||
// SignInEvent.signInWithEmailAndPassword(
|
||||
// email: email,
|
||||
// password: password,
|
||||
// ),
|
||||
// ),
|
||||
// onForgotPassword: () {
|
||||
// // todo: implement forgot password
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
void _pushContinueWithPasswordPage(
|
||||
BuildContext context,
|
||||
String email,
|
||||
) {
|
||||
final signInBloc = context.read<SignInBloc>();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: signInBloc,
|
||||
child: ContinueWithPasswordPage(
|
||||
email: email,
|
||||
backToLogin: () {
|
||||
emailKey.currentState?.clearError();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onEnterPassword: (password) => signInBloc.add(
|
||||
SignInEvent.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
onForgotPassword: () {
|
||||
// todo: implement forgot password
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,7 +167,7 @@ class _ContinueWithMagicLinkOrPasscodePageState
|
|||
// title
|
||||
Text(
|
||||
LocaleKeys.signIn_checkYourEmail.tr(),
|
||||
style: theme.textStyle.heading.h3(
|
||||
style: theme.textStyle.heading3.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
@ -199,7 +199,7 @@ class _ContinueWithMagicLinkOrPasscodePageState
|
|||
// title
|
||||
Text(
|
||||
LocaleKeys.signIn_enterCode.tr(),
|
||||
style: theme.textStyle.heading.h3(
|
||||
style: theme.textStyle.heading3.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/application/sign_in_bloc.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -43,9 +46,16 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
width: 320,
|
||||
child: BlocListener<SignInBloc, SignInState>(
|
||||
listener: (context, state) {
|
||||
if (state.passwordError != null) {
|
||||
final successOrFail = state.successOrFail;
|
||||
if (successOrFail != null && successOrFail.isFailure) {
|
||||
successOrFail.onFailure((error) {
|
||||
inputPasswordKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(),
|
||||
);
|
||||
});
|
||||
} else if (state.passwordError != null) {
|
||||
inputPasswordKey.currentState?.syncError(
|
||||
errorText: 'Incorrect password. Please try again.',
|
||||
errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(),
|
||||
);
|
||||
} else {
|
||||
inputPasswordKey.currentState?.clearError();
|
||||
|
@ -80,8 +90,8 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
|
||||
// title
|
||||
Text(
|
||||
'Enter password',
|
||||
style: theme.textStyle.heading.h3(
|
||||
LocaleKeys.signIn_enterPassword.tr(),
|
||||
style: theme.textStyle.heading3.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
@ -92,13 +102,13 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Login as ',
|
||||
text: LocaleKeys.signIn_loginAs.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: widget.email,
|
||||
text: ' ${widget.email}',
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
|
@ -111,13 +121,26 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
}
|
||||
|
||||
List<Widget> _buildPasswordSection() {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final iconSize = 20.0;
|
||||
return [
|
||||
// Password input
|
||||
AFTextField(
|
||||
key: inputPasswordKey,
|
||||
controller: passwordController,
|
||||
hintText: 'Enter password',
|
||||
hintText: LocaleKeys.signIn_enterPassword.tr(),
|
||||
autoFocus: true,
|
||||
obscureText: true,
|
||||
suffixIconConstraints: BoxConstraints.tightFor(
|
||||
width: iconSize + theme.spacing.m,
|
||||
height: iconSize,
|
||||
),
|
||||
suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(
|
||||
isObscured: isObscured,
|
||||
onTap: () {
|
||||
inputPasswordKey.currentState?.syncObscured(!isObscured);
|
||||
},
|
||||
),
|
||||
onSubmitted: widget.onEnterPassword,
|
||||
),
|
||||
// todo: ask designer to provide the spacing
|
||||
|
@ -127,7 +150,7 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AFGhostTextButton(
|
||||
text: 'Forget password?',
|
||||
text: LocaleKeys.signIn_forgotPassword.tr(),
|
||||
size: AFButtonSize.s,
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: widget.onForgotPassword,
|
||||
|
@ -144,7 +167,7 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
|
||||
// Continue button
|
||||
AFFilledTextButton.primary(
|
||||
text: 'Continue',
|
||||
text: LocaleKeys.web_continue.tr(),
|
||||
onTap: () => widget.onEnterPassword(passwordController.text),
|
||||
size: AFButtonSize.l,
|
||||
alignment: Alignment.center,
|
||||
|
@ -156,7 +179,7 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
List<Widget> _buildBackToLogin() {
|
||||
return [
|
||||
AFGhostTextButton(
|
||||
text: 'Back to Login',
|
||||
text: LocaleKeys.signIn_backToLogin.tr(),
|
||||
size: AFButtonSize.s,
|
||||
onTap: widget.backToLogin,
|
||||
padding: EdgeInsets.zero,
|
||||
|
|
|
@ -25,7 +25,7 @@ class FlowyLogoTitle extends StatelessWidget {
|
|||
const VSpace(20),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textStyle.heading.h3(
|
||||
style: theme.textStyle.heading3.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -54,17 +54,27 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
|
|||
);
|
||||
});
|
||||
},
|
||||
removeUserIcon: () {
|
||||
// Empty Icon URL = No icon
|
||||
_userService.updateUserProfile(iconUrl: "").then((result) {
|
||||
updateUserEmail: (String email) {
|
||||
_userService.updateUserProfile(email: email).then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
},
|
||||
updateUserEmail: (String email) {
|
||||
_userService.updateUserProfile(email: email).then((result) {
|
||||
updateUserPassword: (String oldPassword, String newPassword) {
|
||||
_userService
|
||||
.updateUserProfile(password: newPassword)
|
||||
.then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
},
|
||||
removeUserIcon: () {
|
||||
// Empty Icon URL = No icon
|
||||
_userService.updateUserProfile(iconUrl: "").then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
|
@ -104,10 +114,19 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
|
|||
@freezed
|
||||
class SettingsUserEvent with _$SettingsUserEvent {
|
||||
const factory SettingsUserEvent.initial() = _Initial;
|
||||
const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName;
|
||||
const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail;
|
||||
const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) =
|
||||
_UpdateUserIcon;
|
||||
const factory SettingsUserEvent.updateUserName({
|
||||
required String name,
|
||||
}) = _UpdateUserName;
|
||||
const factory SettingsUserEvent.updateUserEmail({
|
||||
required String email,
|
||||
}) = _UpdateEmail;
|
||||
const factory SettingsUserEvent.updateUserIcon({
|
||||
required String iconUrl,
|
||||
}) = _UpdateUserIcon;
|
||||
const factory SettingsUserEvent.updateUserPassword({
|
||||
required String oldPassword,
|
||||
required String newPassword,
|
||||
}) = _UpdateUserPassword;
|
||||
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
|
||||
const factory SettingsUserEvent.didReceiveUserProfile(
|
||||
UserProfilePB newUserProfile,
|
||||
|
|
|
@ -105,9 +105,9 @@ class SidebarToast extends StatelessWidget {
|
|||
if (role.isOwner) {
|
||||
showSettingsDialog(
|
||||
context,
|
||||
userProfile,
|
||||
userWorkspaceBloc,
|
||||
SettingsPage.plan,
|
||||
userProfile: userProfile,
|
||||
userWorkspaceBloc: userWorkspaceBloc,
|
||||
initPage: SettingsPage.plan,
|
||||
);
|
||||
} else {
|
||||
final String message;
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/password/password_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';
|
||||
|
@ -33,7 +34,7 @@ HotKeyItem openSettingsHotKey(
|
|||
),
|
||||
keyDownHandler: (_) {
|
||||
if (_settingsDialogKey.currentContext == null) {
|
||||
showSettingsDialog(context, userProfile);
|
||||
showSettingsDialog(context, userProfile: userProfile);
|
||||
} else {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.popUntil((route) => route.isFirst);
|
||||
|
@ -57,37 +58,55 @@ class UserSettingButton extends StatefulWidget {
|
|||
|
||||
class _UserSettingButtonState extends State<UserSettingButton> {
|
||||
late UserWorkspaceBloc _userWorkspaceBloc;
|
||||
late PasswordBloc _passwordBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
_passwordBloc = PasswordBloc(widget.userProfile)
|
||||
..add(PasswordEvent.init())
|
||||
..add(PasswordEvent.checkHasPassword());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_passwordBloc.close();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: 24.0,
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.settings_menu_open.tr(),
|
||||
child: FlowyButton(
|
||||
onTap: () => showSettingsDialog(
|
||||
context,
|
||||
widget.userProfile,
|
||||
_userWorkspaceBloc,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
text: FlowySvg(
|
||||
FlowySvgs.settings_s,
|
||||
color:
|
||||
widget.isHover ? Theme.of(context).colorScheme.onSurface : null,
|
||||
opacity: 0.7,
|
||||
child: BlocProvider.value(
|
||||
value: _passwordBloc,
|
||||
child: FlowyButton(
|
||||
onTap: () => showSettingsDialog(
|
||||
context,
|
||||
userProfile: widget.userProfile,
|
||||
userWorkspaceBloc: _userWorkspaceBloc,
|
||||
passwordBloc: _passwordBloc,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
text: FlowySvg(
|
||||
FlowySvgs.settings_s,
|
||||
color: widget.isHover
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: null,
|
||||
opacity: 0.7,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -96,21 +115,33 @@ class _UserSettingButtonState extends State<UserSettingButton> {
|
|||
}
|
||||
|
||||
void showSettingsDialog(
|
||||
BuildContext context,
|
||||
UserProfilePB userProfile, [
|
||||
UserWorkspaceBloc? bloc,
|
||||
BuildContext context, {
|
||||
required UserProfilePB userProfile,
|
||||
UserWorkspaceBloc? userWorkspaceBloc,
|
||||
PasswordBloc? passwordBloc,
|
||||
SettingsPage? initPage,
|
||||
]) {
|
||||
}) {
|
||||
AFFocusManager.maybeOf(context)?.notifyLoseFocus();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => MultiBlocProvider(
|
||||
key: _settingsDialogKey,
|
||||
providers: [
|
||||
passwordBloc != null
|
||||
? BlocProvider<PasswordBloc>.value(
|
||||
value: passwordBloc,
|
||||
)
|
||||
: BlocProvider(
|
||||
create: (context) => PasswordBloc(userProfile)
|
||||
..add(PasswordEvent.init())
|
||||
..add(PasswordEvent.checkHasPassword()),
|
||||
),
|
||||
BlocProvider<DocumentAppearanceCubit>.value(
|
||||
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
|
||||
),
|
||||
BlocProvider.value(value: bloc ?? context.read<UserWorkspaceBloc>()),
|
||||
BlocProvider.value(
|
||||
value: userWorkspaceBloc ?? context.read<UserWorkspaceBloc>(),
|
||||
),
|
||||
],
|
||||
child: SettingsDialog(
|
||||
userProfile,
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||
import 'package:appflowy/shared/version_checker/version_checker.dart';
|
||||
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -16,28 +17,29 @@ class SettingsAppVersion extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return ApplicationInfo.isUpdateAvailable
|
||||
? const _UpdateAppSection()
|
||||
: _buildIsUpToDate();
|
||||
: _buildIsUpToDate(context);
|
||||
}
|
||||
|
||||
Widget _buildIsUpToDate() {
|
||||
Widget _buildIsUpToDate(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.regular(
|
||||
Text(
|
||||
LocaleKeys.settings_accountPage_isUpToDate.tr(),
|
||||
figmaLineHeight: 17,
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
const VSpace(4),
|
||||
Opacity(
|
||||
opacity: 0.7,
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.settings_accountPage_officialVersion.tr(
|
||||
namedArgs: {
|
||||
'version': ApplicationInfo.applicationVersion,
|
||||
},
|
||||
),
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 13,
|
||||
Text(
|
||||
LocaleKeys.settings_accountPage_officialVersion.tr(
|
||||
namedArgs: {
|
||||
'version': ApplicationInfo.applicationVersion,
|
||||
},
|
||||
),
|
||||
style: theme.textStyle.caption.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -8,8 +8,8 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_w
|
|||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
@ -43,43 +43,36 @@ class _AccountDeletionButtonState extends State<AccountDeletionButton> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0xFF4F4F4F)
|
||||
: const Color(0xFFB0B0B0);
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText(
|
||||
Text(
|
||||
LocaleKeys.button_deleteAccount.tr(),
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
figmaLineHeight: 21.0,
|
||||
color: textColor,
|
||||
style: theme.textStyle.heading4.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
child: Text(
|
||||
LocaleKeys.newSettings_myAccount_deleteAccount_description.tr(),
|
||||
fontSize: 12.0,
|
||||
figmaLineHeight: 13.0,
|
||||
maxLines: 2,
|
||||
color: textColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textStyle.caption.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.button_deleteAccount.tr(),
|
||||
constraints: const BoxConstraints(minHeight: 32),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 10),
|
||||
fillColor: Colors.transparent,
|
||||
radius: Corners.s8Border,
|
||||
hoverColor:
|
||||
Theme.of(context).colorScheme.error.withValues(alpha: 0.1),
|
||||
fontColor: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
isDangerous: true,
|
||||
onPressed: () {
|
||||
AFOutlinedTextButton.destructive(
|
||||
text: LocaleKeys.button_deleteAccount.tr(),
|
||||
textStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.error,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
onTap: () {
|
||||
isCheckedNotifier.value = false;
|
||||
textEditingController.clear();
|
||||
|
||||
|
|
|
@ -3,12 +3,16 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/password/password_bloc.dart';
|
||||
import 'package:appflowy/user/application/prelude.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart';
|
||||
import 'package:appflowy/util/navigator_context_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/password/change_password.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/password/setup_password.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -28,9 +32,15 @@ class AccountSignInOutSection extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.regular(LocaleKeys.settings_accountPage_login_title.tr()),
|
||||
Text(
|
||||
LocaleKeys.settings_accountPage_login_title.tr(),
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AccountSignInOutButton(
|
||||
userProfile: userProfile,
|
||||
|
@ -56,13 +66,10 @@ class AccountSignInOutButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PrimaryRoundedButton(
|
||||
return AFFilledTextButton.primary(
|
||||
text: signIn
|
||||
? LocaleKeys.settings_accountPage_login_loginLabel.tr()
|
||||
: LocaleKeys.settings_accountPage_login_logoutLabel.tr(),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
fontWeight: FontWeight.w500,
|
||||
radius: 8.0,
|
||||
onTap: () =>
|
||||
signIn ? _showSignInDialog(context) : _showLogoutDialog(context),
|
||||
);
|
||||
|
@ -96,6 +103,94 @@ class AccountSignInOutButton extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class ChangePasswordSection extends StatelessWidget {
|
||||
const ChangePasswordSection({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return BlocBuilder<PasswordBloc, PasswordState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
LocaleKeys.newSettings_myAccount_password_title.tr(),
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
state.hasPassword
|
||||
? AFFilledTextButton.primary(
|
||||
text: LocaleKeys
|
||||
.newSettings_myAccount_password_changePassword
|
||||
.tr(),
|
||||
onTap: () => _showChangePasswordDialog(context),
|
||||
)
|
||||
: AFFilledTextButton.primary(
|
||||
text: LocaleKeys
|
||||
.newSettings_myAccount_password_setupPassword
|
||||
.tr(),
|
||||
onTap: () => _showSetPasswordDialog(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showChangePasswordDialog(BuildContext context) async {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<PasswordBloc>.value(
|
||||
value: context.read<PasswordBloc>(),
|
||||
),
|
||||
BlocProvider<SignInBloc>.value(
|
||||
value: getIt<SignInBloc>(),
|
||||
),
|
||||
],
|
||||
child: Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.xl),
|
||||
),
|
||||
child: ChangePasswordDialogContent(
|
||||
userProfile: userProfile,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showSetPasswordDialog(BuildContext context) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<PasswordBloc>.value(
|
||||
value: context.read<PasswordBloc>(),
|
||||
),
|
||||
BlocProvider<SignInBloc>.value(
|
||||
value: getIt<SignInBloc>(),
|
||||
),
|
||||
],
|
||||
child: Dialog(
|
||||
child: SetupPasswordDialogContent(
|
||||
userProfile: userProfile,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SignInDialogContent extends StatelessWidget {
|
||||
const _SignInDialogContent();
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
|||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
|
@ -96,27 +97,29 @@ class _AccountUserProfileState extends State<AccountUserProfile> {
|
|||
}
|
||||
|
||||
Widget _buildNameDisplay() {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FlowyText.medium(
|
||||
child: Text(
|
||||
widget.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
AFGhostButton.normal(
|
||||
size: AFButtonSize.s,
|
||||
padding: EdgeInsets.all(theme.spacing.xs),
|
||||
onTap: () => setState(() => isEditing = true),
|
||||
child: const FlowyHover(
|
||||
resetHoverOnRebuild: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: FlowySvg(FlowySvgs.edit_s),
|
||||
),
|
||||
builder: (context, isHovering, disabled) => FlowySvg(
|
||||
FlowySvgs.toolbar_link_edit_m,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class SettingsEmailSection extends StatelessWidget {
|
||||
const SettingsEmailSection({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
LocaleKeys.settings_accountPage_email_title.tr(),
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.s),
|
||||
Text(
|
||||
userProfile.email,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/application/password/password_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ChangePasswordDialogContent extends StatefulWidget {
|
||||
const ChangePasswordDialogContent({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
State<ChangePasswordDialogContent> createState() =>
|
||||
_ChangePasswordDialogContentState();
|
||||
}
|
||||
|
||||
class _ChangePasswordDialogContentState
|
||||
extends State<ChangePasswordDialogContent> {
|
||||
final currentPasswordTextFieldKey = GlobalKey<AFTextFieldState>();
|
||||
final newPasswordTextFieldKey = GlobalKey<AFTextFieldState>();
|
||||
final confirmPasswordTextFieldKey = GlobalKey<AFTextFieldState>();
|
||||
|
||||
final currentPasswordController = TextEditingController();
|
||||
final newPasswordController = TextEditingController();
|
||||
final confirmPasswordController = TextEditingController();
|
||||
|
||||
final iconSize = 20.0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
currentPasswordController.dispose();
|
||||
newPasswordController.dispose();
|
||||
confirmPasswordController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return BlocListener<PasswordBloc, PasswordState>(
|
||||
listener: _onPasswordStateChanged,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.xl),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTitle(context),
|
||||
VSpace(theme.spacing.l),
|
||||
..._buildCurrentPasswordFields(context),
|
||||
VSpace(theme.spacing.l),
|
||||
..._buildNewPasswordFields(context),
|
||||
VSpace(theme.spacing.l),
|
||||
..._buildConfirmPasswordFields(context),
|
||||
VSpace(theme.spacing.l),
|
||||
_buildSubmitButton(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Change password',
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AFGhostButton.normal(
|
||||
size: AFButtonSize.s,
|
||||
padding: EdgeInsets.all(theme.spacing.xs),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
builder: (context, isHovering, disabled) => FlowySvg(
|
||||
FlowySvgs.password_close_m,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildCurrentPasswordFields(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return [
|
||||
Text(
|
||||
LocaleKeys.newSettings_myAccount_password_currentPassword.tr(),
|
||||
style: theme.textStyle.caption.enhanced(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
AFTextField(
|
||||
key: currentPasswordTextFieldKey,
|
||||
controller: currentPasswordController,
|
||||
hintText: LocaleKeys
|
||||
.newSettings_myAccount_password_hint_enterYourCurrentPassword
|
||||
.tr(),
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
suffixIconConstraints: BoxConstraints.tightFor(
|
||||
width: iconSize + theme.spacing.m,
|
||||
height: iconSize,
|
||||
),
|
||||
suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(
|
||||
isObscured: isObscured,
|
||||
onTap: () {
|
||||
currentPasswordTextFieldKey.currentState?.syncObscured(!isObscured);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildNewPasswordFields(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return [
|
||||
Text(
|
||||
LocaleKeys.newSettings_myAccount_password_newPassword.tr(),
|
||||
style: theme.textStyle.caption.enhanced(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
AFTextField(
|
||||
key: newPasswordTextFieldKey,
|
||||
controller: newPasswordController,
|
||||
hintText: LocaleKeys
|
||||
.newSettings_myAccount_password_hint_enterYourNewPassword
|
||||
.tr(),
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
suffixIconConstraints: BoxConstraints.tightFor(
|
||||
width: iconSize + theme.spacing.m,
|
||||
height: iconSize,
|
||||
),
|
||||
suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(
|
||||
isObscured: isObscured,
|
||||
onTap: () {
|
||||
newPasswordTextFieldKey.currentState?.syncObscured(!isObscured);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildConfirmPasswordFields(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return [
|
||||
Text(
|
||||
LocaleKeys.newSettings_myAccount_password_confirmNewPassword.tr(),
|
||||
style: theme.textStyle.caption.enhanced(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
AFTextField(
|
||||
key: confirmPasswordTextFieldKey,
|
||||
controller: confirmPasswordController,
|
||||
hintText: LocaleKeys
|
||||
.newSettings_myAccount_password_hint_confirmYourNewPassword
|
||||
.tr(),
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
suffixIconConstraints: BoxConstraints.tightFor(
|
||||
width: iconSize + theme.spacing.m,
|
||||
height: iconSize,
|
||||
),
|
||||
suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(
|
||||
isObscured: isObscured,
|
||||
onTap: () {
|
||||
confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildSubmitButton(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
AFOutlinedTextButton.normal(
|
||||
text: LocaleKeys.button_cancel.tr(),
|
||||
textStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
const HSpace(16),
|
||||
AFFilledTextButton.primary(
|
||||
text: LocaleKeys.button_save.tr(),
|
||||
textStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.onFill,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
onTap: () => _save(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _save(BuildContext context) async {
|
||||
_resetError();
|
||||
|
||||
final currentPassword = currentPasswordController.text;
|
||||
final newPassword = newPasswordController.text;
|
||||
final confirmPassword = confirmPasswordController.text;
|
||||
|
||||
if (newPassword.isEmpty) {
|
||||
newPasswordTextFieldKey.currentState?.syncError(
|
||||
errorText: LocaleKeys
|
||||
.newSettings_myAccount_password_error_newPasswordIsRequired
|
||||
.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmPassword.isEmpty) {
|
||||
confirmPasswordTextFieldKey.currentState?.syncError(
|
||||
errorText: LocaleKeys
|
||||
.newSettings_myAccount_password_error_confirmPasswordIsRequired
|
||||
.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword != confirmPassword) {
|
||||
confirmPasswordTextFieldKey.currentState?.syncError(
|
||||
errorText: LocaleKeys
|
||||
.newSettings_myAccount_password_error_passwordsDoNotMatch
|
||||
.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword == currentPassword) {
|
||||
newPasswordTextFieldKey.currentState?.syncError(
|
||||
errorText: LocaleKeys
|
||||
.newSettings_myAccount_password_error_newPasswordIsSameAsCurrent
|
||||
.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// all the verification passed, save the new password
|
||||
context.read<PasswordBloc>().add(
|
||||
PasswordEvent.changePassword(
|
||||
oldPassword: currentPassword,
|
||||
newPassword: newPassword,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetError() {
|
||||
currentPasswordTextFieldKey.currentState?.clearError();
|
||||
newPasswordTextFieldKey.currentState?.clearError();
|
||||
confirmPasswordTextFieldKey.currentState?.clearError();
|
||||
}
|
||||
|
||||
void _onPasswordStateChanged(BuildContext context, PasswordState state) {
|
||||
bool hasError = false;
|
||||
String message = '';
|
||||
String description = '';
|
||||
|
||||
final changePasswordResult = state.changePasswordResult;
|
||||
final setPasswordResult = state.setupPasswordResult;
|
||||
|
||||
if (changePasswordResult != null) {
|
||||
changePasswordResult.fold(
|
||||
(success) {
|
||||
message = LocaleKeys
|
||||
.newSettings_myAccount_password_toast_passwordUpdatedSuccessfully
|
||||
.tr();
|
||||
},
|
||||
(error) {
|
||||
hasError = true;
|
||||
message = LocaleKeys
|
||||
.newSettings_myAccount_password_toast_passwordUpdatedFailed
|
||||
.tr();
|
||||
description = error.msg;
|
||||
},
|
||||
);
|
||||
} else if (setPasswordResult != null) {
|
||||
setPasswordResult.fold(
|
||||
(success) {
|
||||
message = LocaleKeys
|
||||
.newSettings_myAccount_password_toast_passwordSetupSuccessfully
|
||||
.tr();
|
||||
},
|
||||
(error) {
|
||||
hasError = true;
|
||||
message = LocaleKeys
|
||||
.newSettings_myAccount_password_toast_passwordSetupFailed
|
||||
.tr();
|
||||
description = error.msg;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!state.isSubmitting && message.isNotEmpty) {
|
||||
showToastNotification(
|
||||
message: message,
|
||||
description: description,
|
||||
type: hasError ? ToastificationType.error : ToastificationType.success,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PasswordSuffixIcon extends StatelessWidget {
|
||||
const PasswordSuffixIcon({
|
||||
super.key,
|
||||
required this.isObscured,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool isObscured;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: theme.spacing.m),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: FlowySvg(
|
||||
isObscured ? FlowySvgs.show_s : FlowySvgs.hide_s,
|
||||
color: theme.textColorScheme.secondary,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/application/password/password_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SetupPasswordDialogContent extends StatefulWidget {
|
||||
const SetupPasswordDialogContent({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
State<SetupPasswordDialogContent> createState() =>
|
||||
_SetupPasswordDialogContentState();
|
||||
}
|
||||
|
||||
class _SetupPasswordDialogContentState
|
||||
extends State<SetupPasswordDialogContent> {
|
||||
final passwordTextFieldKey = GlobalKey<AFTextFieldState>();
|
||||
final confirmPasswordTextFieldKey = GlobalKey<AFTextFieldState>();
|
||||
|
||||
final passwordController = TextEditingController();
|
||||
final confirmPasswordController = TextEditingController();
|
||||
|
||||
final iconSize = 20.0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
passwordController.dispose();
|
||||
confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return BlocListener<PasswordBloc, PasswordState>(
|
||||
listener: _onPasswordStateChanged,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTitle(context),
|
||||
VSpace(theme.spacing.l),
|
||||
..._buildPasswordFields(context),
|
||||
VSpace(theme.spacing.l),
|
||||
..._buildConfirmPasswordFields(context),
|
||||
VSpace(theme.spacing.l),
|
||||
_buildSubmitButton(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
LocaleKeys.newSettings_myAccount_password_setupPassword.tr(),
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AFGhostButton.normal(
|
||||
size: AFButtonSize.s,
|
||||
padding: EdgeInsets.all(theme.spacing.xs),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
builder: (context, isHovering, disabled) => FlowySvg(
|
||||
FlowySvgs.password_close_m,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildPasswordFields(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return [
|
||||
Text(
|
||||
'Password',
|
||||
style: theme.textStyle.caption.enhanced(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
AFTextField(
|
||||
key: passwordTextFieldKey,
|
||||
controller: passwordController,
|
||||
hintText: 'Enter your password',
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
suffixIconConstraints: BoxConstraints.tightFor(
|
||||
width: iconSize + theme.spacing.m,
|
||||
height: iconSize,
|
||||
),
|
||||
suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(
|
||||
isObscured: isObscured,
|
||||
onTap: () {
|
||||
passwordTextFieldKey.currentState?.syncObscured(!isObscured);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildConfirmPasswordFields(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return [
|
||||
Text(
|
||||
'Confirm password',
|
||||
style: theme.textStyle.caption.enhanced(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
AFTextField(
|
||||
key: confirmPasswordTextFieldKey,
|
||||
controller: confirmPasswordController,
|
||||
hintText: 'Confirm your password',
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
suffixIconConstraints: BoxConstraints.tightFor(
|
||||
width: iconSize + theme.spacing.m,
|
||||
height: iconSize,
|
||||
),
|
||||
suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(
|
||||
isObscured: isObscured,
|
||||
onTap: () {
|
||||
confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildSubmitButton(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
AFOutlinedTextButton.normal(
|
||||
text: 'Cancel',
|
||||
textStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
const HSpace(16),
|
||||
AFFilledTextButton.primary(
|
||||
text: 'Save',
|
||||
textStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.onFill,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
onTap: () => _save(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _save(BuildContext context) async {
|
||||
_resetError();
|
||||
|
||||
final password = passwordController.text;
|
||||
final confirmPassword = confirmPasswordController.text;
|
||||
|
||||
if (password.isEmpty) {
|
||||
passwordTextFieldKey.currentState?.syncError(
|
||||
errorText: LocaleKeys
|
||||
.newSettings_myAccount_password_error_newPasswordIsRequired
|
||||
.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmPassword.isEmpty) {
|
||||
confirmPasswordTextFieldKey.currentState?.syncError(
|
||||
errorText: LocaleKeys
|
||||
.newSettings_myAccount_password_error_confirmPasswordIsRequired
|
||||
.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (password != confirmPassword) {
|
||||
confirmPasswordTextFieldKey.currentState?.syncError(
|
||||
errorText: LocaleKeys
|
||||
.newSettings_myAccount_password_error_passwordsDoNotMatch
|
||||
.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// all the verification passed, save the password
|
||||
context.read<PasswordBloc>().add(
|
||||
PasswordEvent.setupPassword(
|
||||
newPassword: password,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetError() {
|
||||
passwordTextFieldKey.currentState?.clearError();
|
||||
confirmPasswordTextFieldKey.currentState?.clearError();
|
||||
}
|
||||
|
||||
void _onPasswordStateChanged(BuildContext context, PasswordState state) {
|
||||
bool hasError = false;
|
||||
String message = '';
|
||||
String description = '';
|
||||
|
||||
final setPasswordResult = state.setupPasswordResult;
|
||||
|
||||
if (setPasswordResult != null) {
|
||||
setPasswordResult.fold(
|
||||
(success) {
|
||||
message = 'Password set';
|
||||
description = 'Your password has been set';
|
||||
},
|
||||
(error) {
|
||||
hasError = true;
|
||||
message = 'Failed to set password';
|
||||
description = error.msg;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!state.isSubmitting && message.isNotEmpty) {
|
||||
showToastNotification(
|
||||
message: message,
|
||||
description: description,
|
||||
type: hasError ? ToastificationType.error : ToastificationType.success,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,12 +4,12 @@ import 'package:appflowy/startup/startup.dart';
|
|||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/about/app_version.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/email/email_section.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
|
@ -45,11 +45,11 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
|
|||
child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
|
||||
builder: (context, state) {
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_accountPage_title.tr(),
|
||||
title: LocaleKeys.newSettings_myAccount_title.tr(),
|
||||
children: [
|
||||
// user profile
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_accountPage_general_title.tr(),
|
||||
title: LocaleKeys.newSettings_myAccount_myProfile.tr(),
|
||||
children: [
|
||||
AccountUserProfile(
|
||||
name: userName,
|
||||
|
@ -61,7 +61,7 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
|
|||
setState(() => userName = newName);
|
||||
context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserName(newName));
|
||||
.add(SettingsUserEvent.updateUserName(name: newName));
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -72,9 +72,14 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
|
|||
if (isAuthEnabled &&
|
||||
state.userProfile.authenticator != AuthenticatorPB.Local) ...[
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_accountPage_email_title.tr(),
|
||||
title: LocaleKeys.newSettings_myAccount_myAccount.tr(),
|
||||
children: [
|
||||
FlowyText.regular(state.userProfile.email),
|
||||
SettingsEmailSection(
|
||||
userProfile: state.userProfile,
|
||||
),
|
||||
ChangePasswordSection(
|
||||
userProfile: state.userProfile,
|
||||
),
|
||||
AccountSignInOutSection(
|
||||
userProfile: state.userProfile,
|
||||
onAction: state.userProfile.authenticator ==
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
@ -25,15 +26,18 @@ class SettingsCategory extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FlowyText.semibold(
|
||||
Text(
|
||||
title,
|
||||
style: theme.textStyle.heading4.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
maxLines: 2,
|
||||
fontSize: 16,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (tooltip != null) ...[
|
||||
|
@ -47,7 +51,7 @@ class SettingsCategory extends StatelessWidget {
|
|||
if (actions != null) ...actions!,
|
||||
],
|
||||
),
|
||||
const VSpace(8),
|
||||
const VSpace(16),
|
||||
if (description?.isNotEmpty ?? false) ...[
|
||||
FlowyText.regular(
|
||||
description!,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// This is used to create a uniform space and divider
|
||||
|
@ -7,6 +8,11 @@ class SettingsCategorySpacer extends StatelessWidget {
|
|||
const SettingsCategorySpacer({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
const Divider(height: 32, color: Color(0xFFF2F2F2));
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Divider(
|
||||
height: 32,
|
||||
color: theme.borderColorScheme.greyPrimary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Renders a simple header for the settings view
|
||||
///
|
||||
|
@ -13,10 +13,16 @@ class SettingsHeader extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.semibold(title, fontSize: 24),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textStyle.heading2.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
if (description?.isNotEmpty == true) ...[
|
||||
const VSpace(8),
|
||||
FlowyText(
|
||||
|
|
|
@ -144,34 +144,34 @@ EXTERNAL SOURCES:
|
|||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
|
||||
appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9
|
||||
auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d
|
||||
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
|
||||
connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38
|
||||
app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468
|
||||
appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7
|
||||
auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118
|
||||
bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9
|
||||
connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5
|
||||
desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43
|
||||
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
|
||||
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
|
||||
flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||
hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c
|
||||
irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478
|
||||
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
|
||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2
|
||||
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
|
||||
local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
|
||||
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
|
||||
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
|
||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
|
||||
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
|
||||
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
|
||||
|
||||
PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ enum AFButtonSize {
|
|||
vertical: theme.spacing.xs,
|
||||
),
|
||||
AFButtonSize.m => EdgeInsets.symmetric(
|
||||
horizontal: theme.spacing.l,
|
||||
horizontal: theme.spacing.xl,
|
||||
vertical: theme.spacing.s,
|
||||
),
|
||||
AFButtonSize.l => EdgeInsets.symmetric(
|
||||
|
|
|
@ -13,6 +13,7 @@ class AFBaseTextButton extends StatelessWidget {
|
|||
this.textColor,
|
||||
this.backgroundColor,
|
||||
this.alignment,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
/// The text of the button.
|
||||
|
@ -44,6 +45,9 @@ class AFBaseTextButton extends StatelessWidget {
|
|||
/// If it's null, the button size will be the size of the text with padding.
|
||||
final Alignment? alignment;
|
||||
|
||||
/// The text style of the button.
|
||||
final TextStyle? textStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
throw UnimplementedError();
|
||||
|
|
|
@ -14,6 +14,7 @@ class AFFilledTextButton extends AFBaseTextButton {
|
|||
super.borderRadius,
|
||||
super.disabled = false,
|
||||
super.alignment,
|
||||
super.textStyle,
|
||||
});
|
||||
|
||||
/// Primary text button.
|
||||
|
@ -26,6 +27,7 @@ class AFFilledTextButton extends AFBaseTextButton {
|
|||
double? borderRadius,
|
||||
bool disabled = false,
|
||||
Alignment? alignment,
|
||||
TextStyle? textStyle,
|
||||
}) {
|
||||
return AFFilledTextButton(
|
||||
key: key,
|
||||
|
@ -36,6 +38,7 @@ class AFFilledTextButton extends AFBaseTextButton {
|
|||
borderRadius: borderRadius,
|
||||
disabled: disabled,
|
||||
alignment: alignment,
|
||||
textStyle: textStyle,
|
||||
textColor: (context, isHovering, disabled) =>
|
||||
AppFlowyTheme.of(context).textColorScheme.onFill,
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
|
@ -60,6 +63,7 @@ class AFFilledTextButton extends AFBaseTextButton {
|
|||
double? borderRadius,
|
||||
bool disabled = false,
|
||||
Alignment? alignment,
|
||||
TextStyle? textStyle,
|
||||
}) {
|
||||
return AFFilledTextButton(
|
||||
key: key,
|
||||
|
@ -70,6 +74,7 @@ class AFFilledTextButton extends AFBaseTextButton {
|
|||
borderRadius: borderRadius,
|
||||
disabled: disabled,
|
||||
alignment: alignment,
|
||||
textStyle: textStyle,
|
||||
textColor: (context, isHovering, disabled) =>
|
||||
AppFlowyTheme.of(context).textColorScheme.onFill,
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
|
@ -92,6 +97,7 @@ class AFFilledTextButton extends AFBaseTextButton {
|
|||
EdgeInsetsGeometry? padding,
|
||||
double? borderRadius,
|
||||
Alignment? alignment,
|
||||
TextStyle? textStyle,
|
||||
}) {
|
||||
return AFFilledTextButton(
|
||||
key: key,
|
||||
|
@ -102,6 +108,7 @@ class AFFilledTextButton extends AFBaseTextButton {
|
|||
borderRadius: borderRadius,
|
||||
disabled: true,
|
||||
alignment: alignment,
|
||||
textStyle: textStyle,
|
||||
textColor: (context, isHovering, disabled) =>
|
||||
AppFlowyTheme.of(context).textColorScheme.tertiary,
|
||||
backgroundColor: (context, isHovering, disabled) =>
|
||||
|
@ -123,7 +130,8 @@ class AFFilledTextButton extends AFBaseTextButton {
|
|||
AppFlowyTheme.of(context).textColorScheme.onFill;
|
||||
Widget child = Text(
|
||||
text,
|
||||
style: size.buildTextStyle(context).copyWith(color: textColor),
|
||||
style: textStyle ??
|
||||
size.buildTextStyle(context).copyWith(color: textColor),
|
||||
);
|
||||
|
||||
final alignment = this.alignment;
|
||||
|
|
|
@ -8,6 +8,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
|
|||
required super.text,
|
||||
required super.onTap,
|
||||
this.borderColor,
|
||||
super.textStyle,
|
||||
super.textColor,
|
||||
super.backgroundColor,
|
||||
super.size = AFButtonSize.m,
|
||||
|
@ -27,6 +28,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
|
|||
double? borderRadius,
|
||||
bool disabled = false,
|
||||
Alignment? alignment,
|
||||
TextStyle? textStyle,
|
||||
}) {
|
||||
return AFOutlinedTextButton._(
|
||||
key: key,
|
||||
|
@ -37,6 +39,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
|
|||
borderRadius: borderRadius,
|
||||
disabled: disabled,
|
||||
alignment: alignment,
|
||||
textStyle: textStyle,
|
||||
borderColor: (context, isHovering, disabled) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
if (disabled) {
|
||||
|
@ -80,6 +83,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
|
|||
double? borderRadius,
|
||||
bool disabled = false,
|
||||
Alignment? alignment,
|
||||
TextStyle? textStyle,
|
||||
}) {
|
||||
return AFOutlinedTextButton._(
|
||||
key: key,
|
||||
|
@ -90,6 +94,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
|
|||
borderRadius: borderRadius,
|
||||
disabled: disabled,
|
||||
alignment: alignment,
|
||||
textStyle: textStyle,
|
||||
borderColor: (context, isHovering, disabled) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
if (disabled) {
|
||||
|
@ -127,6 +132,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
|
|||
EdgeInsetsGeometry? padding,
|
||||
double? borderRadius,
|
||||
Alignment? alignment,
|
||||
TextStyle? textStyle,
|
||||
}) {
|
||||
return AFOutlinedTextButton._(
|
||||
key: key,
|
||||
|
@ -137,6 +143,7 @@ class AFOutlinedTextButton extends AFBaseTextButton {
|
|||
borderRadius: borderRadius,
|
||||
disabled: true,
|
||||
alignment: alignment,
|
||||
textStyle: textStyle,
|
||||
textColor: (context, isHovering, disabled) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return disabled
|
||||
|
@ -185,7 +192,8 @@ class AFOutlinedTextButton extends AFBaseTextButton {
|
|||
|
||||
Widget child = Text(
|
||||
text,
|
||||
style: size.buildTextStyle(context).copyWith(color: textColor),
|
||||
style: textStyle ??
|
||||
size.buildTextStyle(context).copyWith(color: textColor),
|
||||
);
|
||||
|
||||
final alignment = this.alignment;
|
||||
|
|
|
@ -6,8 +6,12 @@ typedef AFTextFieldValidator = (bool result, String errorText) Function(
|
|||
);
|
||||
|
||||
abstract class AFTextFieldState extends State<AFTextField> {
|
||||
// Error handler
|
||||
void syncError({required String errorText}) {}
|
||||
void clearError() {}
|
||||
|
||||
/// Obscure the text.
|
||||
void syncObscured(bool isObscured) {}
|
||||
}
|
||||
|
||||
class AFTextField extends StatefulWidget {
|
||||
|
@ -23,6 +27,9 @@ class AFTextField extends StatefulWidget {
|
|||
this.onSubmitted,
|
||||
this.autoFocus,
|
||||
this.height = 40.0,
|
||||
this.obscureText = false,
|
||||
this.suffixIconBuilder,
|
||||
this.suffixIconConstraints,
|
||||
});
|
||||
|
||||
/// The height of the text field.
|
||||
|
@ -57,6 +64,16 @@ class AFTextField extends StatefulWidget {
|
|||
/// Enable auto focus.
|
||||
final bool? autoFocus;
|
||||
|
||||
/// Obscure the text.
|
||||
final bool obscureText;
|
||||
|
||||
/// The trailing widget to display.
|
||||
final Widget Function(BuildContext context, bool isObscured)?
|
||||
suffixIconBuilder;
|
||||
|
||||
/// The size of the suffix icon.
|
||||
final BoxConstraints? suffixIconConstraints;
|
||||
|
||||
@override
|
||||
State<AFTextField> createState() => _AFTextFieldState();
|
||||
}
|
||||
|
@ -67,6 +84,8 @@ class _AFTextFieldState extends AFTextFieldState {
|
|||
bool hasError = false;
|
||||
String errorText = '';
|
||||
|
||||
bool isObscured = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -79,6 +98,8 @@ class _AFTextFieldState extends AFTextFieldState {
|
|||
}
|
||||
|
||||
effectiveController.addListener(_validate);
|
||||
|
||||
isObscured = widget.obscureText;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -107,6 +128,7 @@ class _AFTextFieldState extends AFTextFieldState {
|
|||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
obscureText: isObscured,
|
||||
onChanged: widget.onChanged,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
autofocus: widget.autoFocus ?? false,
|
||||
|
@ -152,6 +174,8 @@ class _AFTextFieldState extends AFTextFieldState {
|
|||
borderRadius: borderRadius,
|
||||
),
|
||||
hoverColor: theme.borderColorScheme.greyTertiaryHover,
|
||||
suffixIcon: widget.suffixIconBuilder?.call(context, isObscured),
|
||||
suffixIconConstraints: widget.suffixIconConstraints,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -204,4 +228,11 @@ class _AFTextFieldState extends AFTextFieldState {
|
|||
errorText = '';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void syncObscured(bool isObscured) {
|
||||
setState(() {
|
||||
this.isObscured = isObscured;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,66 +6,86 @@ abstract class TextThemeType {
|
|||
TextStyle standard({
|
||||
String family = '',
|
||||
Color? color,
|
||||
FontWeight? weight,
|
||||
});
|
||||
|
||||
TextStyle enhanced({
|
||||
String family = '',
|
||||
Color? color,
|
||||
FontWeight? weight,
|
||||
});
|
||||
|
||||
TextStyle prominent({
|
||||
String family = '',
|
||||
Color? color,
|
||||
FontWeight? weight,
|
||||
});
|
||||
|
||||
TextStyle underline({
|
||||
String family = '',
|
||||
Color? color,
|
||||
FontWeight? weight,
|
||||
});
|
||||
}
|
||||
|
||||
class TextThemeHeading {
|
||||
const TextThemeHeading();
|
||||
class TextThemeHeading1 extends TextThemeType {
|
||||
const TextThemeHeading1();
|
||||
|
||||
TextStyle h1({
|
||||
@override
|
||||
TextStyle standard({
|
||||
String family = '',
|
||||
Color? color,
|
||||
FontWeight? weight,
|
||||
}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
fontSize: 36,
|
||||
height: 40 / 36,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w400,
|
||||
);
|
||||
|
||||
TextStyle h2({
|
||||
@override
|
||||
TextStyle enhanced({
|
||||
String family = '',
|
||||
Color? color,
|
||||
FontWeight? weight,
|
||||
}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
fontSize: 24,
|
||||
height: 32 / 24,
|
||||
fontSize: 36,
|
||||
height: 40 / 36,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle h3({
|
||||
@override
|
||||
TextStyle prominent({
|
||||
String family = '',
|
||||
Color? color,
|
||||
FontWeight? weight,
|
||||
}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
fontSize: 20,
|
||||
height: 28 / 20,
|
||||
fontSize: 36,
|
||||
height: 40 / 36,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w700,
|
||||
);
|
||||
|
||||
TextStyle h4({
|
||||
@override
|
||||
TextStyle underline({
|
||||
String family = '',
|
||||
Color? color,
|
||||
FontWeight? weight,
|
||||
}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
fontSize: 16,
|
||||
height: 22 / 16,
|
||||
fontSize: 36,
|
||||
height: 40 / 36,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.bold,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
static TextStyle _defaultTextStyle({
|
||||
|
@ -74,13 +94,188 @@ class TextThemeHeading {
|
|||
required double height,
|
||||
TextDecoration decoration = TextDecoration.none,
|
||||
Color? color,
|
||||
FontWeight weight = FontWeight.bold,
|
||||
}) =>
|
||||
TextStyle(
|
||||
inherit: false,
|
||||
fontSize: fontSize,
|
||||
decoration: decoration,
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: weight,
|
||||
height: height,
|
||||
fontFamily: family,
|
||||
color: color,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
leadingDistribution: TextLeadingDistribution.even,
|
||||
);
|
||||
}
|
||||
|
||||
class TextThemeHeading2 extends TextThemeType {
|
||||
const TextThemeHeading2();
|
||||
|
||||
@override
|
||||
TextStyle standard({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w400,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w600,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle prominent({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w700,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle underline({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w400,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
static TextStyle _defaultTextStyle({
|
||||
required String family,
|
||||
double fontSize = 24,
|
||||
double height = 32 / 24,
|
||||
TextDecoration decoration = TextDecoration.none,
|
||||
FontWeight weight = FontWeight.w400,
|
||||
Color? color,
|
||||
}) =>
|
||||
TextStyle(
|
||||
inherit: false,
|
||||
fontSize: fontSize,
|
||||
decoration: decoration,
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: weight,
|
||||
height: height,
|
||||
fontFamily: family,
|
||||
color: color,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
leadingDistribution: TextLeadingDistribution.even,
|
||||
);
|
||||
}
|
||||
|
||||
class TextThemeHeading3 extends TextThemeType {
|
||||
const TextThemeHeading3();
|
||||
|
||||
@override
|
||||
TextStyle standard({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w400,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w600,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle prominent({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w700,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle underline({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w400,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
static TextStyle _defaultTextStyle({
|
||||
required String family,
|
||||
double fontSize = 20,
|
||||
double height = 28 / 20,
|
||||
TextDecoration decoration = TextDecoration.none,
|
||||
FontWeight weight = FontWeight.w400,
|
||||
Color? color,
|
||||
}) =>
|
||||
TextStyle(
|
||||
inherit: false,
|
||||
fontSize: fontSize,
|
||||
decoration: decoration,
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: weight,
|
||||
height: height,
|
||||
fontFamily: family,
|
||||
color: color,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
leadingDistribution: TextLeadingDistribution.even,
|
||||
);
|
||||
}
|
||||
|
||||
class TextThemeHeading4 extends TextThemeType {
|
||||
const TextThemeHeading4();
|
||||
|
||||
@override
|
||||
TextStyle standard({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w400,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w600,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle prominent({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w700,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle underline({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.w400,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
static TextStyle _defaultTextStyle({
|
||||
required String family,
|
||||
double fontSize = 16,
|
||||
double height = 22 / 16,
|
||||
TextDecoration decoration = TextDecoration.none,
|
||||
FontWeight weight = FontWeight.w400,
|
||||
Color? color,
|
||||
}) =>
|
||||
TextStyle(
|
||||
inherit: false,
|
||||
fontSize: fontSize,
|
||||
decoration: decoration,
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: weight,
|
||||
height: height,
|
||||
fontFamily: family,
|
||||
color: color,
|
||||
|
@ -93,29 +288,35 @@ class TextThemeHeadline extends TextThemeType {
|
|||
const TextThemeHeadline();
|
||||
|
||||
@override
|
||||
TextStyle standard({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle standard({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.normal,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: FontWeight.w600,
|
||||
weight: weight ?? FontWeight.w600,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle prominent({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: FontWeight.bold,
|
||||
weight: weight ?? FontWeight.bold,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle underline({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle underline({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.normal,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
|
@ -123,8 +324,8 @@ class TextThemeHeadline extends TextThemeType {
|
|||
required String family,
|
||||
double fontSize = 24,
|
||||
double height = 36 / 24,
|
||||
FontWeight weight = FontWeight.normal,
|
||||
TextDecoration decoration = TextDecoration.none,
|
||||
FontWeight weight = FontWeight.normal,
|
||||
Color? color,
|
||||
}) =>
|
||||
TextStyle(
|
||||
|
@ -145,29 +346,35 @@ class TextThemeTitle extends TextThemeType {
|
|||
const TextThemeTitle();
|
||||
|
||||
@override
|
||||
TextStyle standard({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle standard({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.normal,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: FontWeight.w600,
|
||||
weight: weight ?? FontWeight.w600,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle prominent({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: FontWeight.bold,
|
||||
weight: weight ?? FontWeight.bold,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle underline({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle underline({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.normal,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
|
@ -197,29 +404,35 @@ class TextThemeBody extends TextThemeType {
|
|||
const TextThemeBody();
|
||||
|
||||
@override
|
||||
TextStyle standard({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle standard({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.normal,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: FontWeight.w600,
|
||||
weight: weight ?? FontWeight.w600,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle prominent({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: FontWeight.bold,
|
||||
weight: weight ?? FontWeight.bold,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle underline({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle underline({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.normal,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
|
@ -249,29 +462,35 @@ class TextThemeCaption extends TextThemeType {
|
|||
const TextThemeCaption();
|
||||
|
||||
@override
|
||||
TextStyle standard({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle standard({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.normal,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: FontWeight.w600,
|
||||
weight: weight ?? FontWeight.w600,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle prominent({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: FontWeight.bold,
|
||||
weight: weight ?? FontWeight.bold,
|
||||
);
|
||||
|
||||
@override
|
||||
TextStyle underline({String family = '', Color? color}) => _defaultTextStyle(
|
||||
TextStyle underline({String family = '', Color? color, FontWeight? weight}) =>
|
||||
_defaultTextStyle(
|
||||
family: family,
|
||||
color: color,
|
||||
weight: weight ?? FontWeight.normal,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
|
|
|
@ -2,14 +2,20 @@ import 'package:appflowy_ui/src/theme/text_style/base/default_text_style.dart';
|
|||
|
||||
class AppFlowyBaseTextStyle {
|
||||
const AppFlowyBaseTextStyle({
|
||||
this.heading = const TextThemeHeading(),
|
||||
this.heading1 = const TextThemeHeading1(),
|
||||
this.heading2 = const TextThemeHeading2(),
|
||||
this.heading3 = const TextThemeHeading3(),
|
||||
this.heading4 = const TextThemeHeading4(),
|
||||
this.headline = const TextThemeHeadline(),
|
||||
this.title = const TextThemeTitle(),
|
||||
this.body = const TextThemeBody(),
|
||||
this.caption = const TextThemeCaption(),
|
||||
});
|
||||
|
||||
final TextThemeHeading heading;
|
||||
final TextThemeType heading1;
|
||||
final TextThemeType heading2;
|
||||
final TextThemeType heading3;
|
||||
final TextThemeType heading4;
|
||||
final TextThemeType headline;
|
||||
final TextThemeType title;
|
||||
final TextThemeType body;
|
||||
|
|
5
frontend/resources/flowy_icons/20x/hide_password.svg
Normal file
5
frontend/resources/flowy_icons/20x/hide_password.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon / Hide / M">
|
||||
<path id="Vector" d="M11.6674 11.7265C11.2147 12.1636 10.6085 12.4055 9.97928 12.4001C9.35004 12.3946 8.74812 12.1422 8.30317 11.6973C7.85821 11.2523 7.60582 10.6504 7.60035 10.0211C7.59488 9.3919 7.83677 8.78568 8.27393 8.33306M8.9867 4.46126C10.8501 4.23919 12.735 4.6331 14.3535 5.58284C15.972 6.53257 17.2353 7.98594 17.9502 9.72099C18.0169 9.90059 18.0169 10.0982 17.9502 10.2778C17.6563 10.9905 17.2677 11.6605 16.7951 12.2697M14.3832 14.3992C13.3221 15.0277 12.1381 15.4207 10.9117 15.5514C9.68527 15.6821 8.44508 15.5475 7.27529 15.1566C6.10549 14.7658 5.03345 14.1279 4.13191 13.2862C3.23037 12.4445 2.52042 11.4188 2.05025 10.2786C1.98358 10.099 1.98358 9.90139 2.05025 9.72179C2.75952 8.00176 4.00749 6.55815 5.60687 5.6076M2.00065 2.00058L17.9998 17.9998" stroke="#6F748C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 987 B |
5
frontend/resources/flowy_icons/20x/password_close.svg
Normal file
5
frontend/resources/flowy_icons/20x/password_close.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Vector" d="M16 4L4 16M4 4L16 16" stroke="#21232A" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 231 B |
4
frontend/resources/flowy_icons/20x/show_password.svg
Normal file
4
frontend/resources/flowy_icons/20x/show_password.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.05025 10.2786C1.98358 10.099 1.98358 9.90141 2.05025 9.72181C2.69957 8.14737 3.80177 6.80119 5.2171 5.85392C6.63243 4.90666 8.29717 4.40097 10.0002 4.40097C11.7033 4.40097 13.3681 4.90666 14.7834 5.85392C16.1987 6.80119 17.3009 8.14737 17.9502 9.72181C18.0169 9.90141 18.0169 10.099 17.9502 10.2786C17.3009 11.853 16.1987 13.1992 14.7834 14.1465C13.3681 15.0937 11.7033 15.5994 10.0002 15.5994C8.29717 15.5994 6.63243 15.0937 5.2171 14.1465C3.80177 13.1992 2.69957 11.853 2.05025 10.2786Z" stroke="#6F748C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.0002 12.4001C11.3257 12.4001 12.4001 11.3256 12.4001 10.0002C12.4001 8.67478 11.3257 7.60032 10.0002 7.60032C8.67483 7.60032 7.60036 8.67478 7.60036 10.0002C7.60036 11.3256 8.67483 12.4001 10.0002 12.4001Z" stroke="#6F748C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 997 B |
|
@ -81,8 +81,11 @@
|
|||
"enterCode": "Enter code",
|
||||
"enterCodeManually": "Enter code manually",
|
||||
"continueWithEmail": "Continue with email",
|
||||
"enterPassword": "Enter password",
|
||||
"loginAs": "Login as",
|
||||
"invalidVerificationCode": "Please enter a valid verification code",
|
||||
"tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later."
|
||||
"tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later.",
|
||||
"invalidLoginCredentials": "Your password is incorrect, please try again"
|
||||
},
|
||||
"workspace": {
|
||||
"chooseWorkspace": "Choose your workspace",
|
||||
|
@ -2618,7 +2621,7 @@
|
|||
"noLogFiles": "There're no log files",
|
||||
"newSettings": {
|
||||
"myAccount": {
|
||||
"title": "My account",
|
||||
"title": "Account & App",
|
||||
"subtitle": "Customize your profile, manage account security, open AI keys, or login into your account.",
|
||||
"profileLabel": "Account name & Profile image",
|
||||
"profileNamePlaceholder": "Enter your name",
|
||||
|
@ -2644,7 +2647,34 @@
|
|||
"failedToGetCurrentUser": "Failed to get current user email",
|
||||
"confirmTextValidationFailed": "Your confirmation text does not match \"@:newSettings.myAccount.deleteAccount.confirmHint3\"",
|
||||
"deleteAccountSuccess": "Account deleted successfully"
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"title": "Password",
|
||||
"changePassword": "Change password",
|
||||
"currentPassword": "Current password",
|
||||
"newPassword": "New password",
|
||||
"confirmNewPassword": "Confirm new password",
|
||||
"setupPassword": "Setup password",
|
||||
"error": {
|
||||
"newPasswordIsRequired": "New password is required",
|
||||
"confirmPasswordIsRequired": "Confirm password is required",
|
||||
"passwordsDoNotMatch": "Passwords do not match",
|
||||
"newPasswordIsSameAsCurrent": "New password is same as current password"
|
||||
},
|
||||
"toast": {
|
||||
"passwordUpdatedSuccessfully": "Password updated successfully",
|
||||
"passwordUpdatedFailed": "Failed to update password",
|
||||
"passwordSetupSuccessfully": "Password setup successfully",
|
||||
"passwordSetupFailed": "Failed to setup password"
|
||||
},
|
||||
"hint": {
|
||||
"enterYourCurrentPassword": "Enter your current password",
|
||||
"enterYourNewPassword": "Enter your new password",
|
||||
"confirmYourNewPassword": "Confirm your new password"
|
||||
}
|
||||
},
|
||||
"myAccount": "My Account",
|
||||
"myProfile": "My Profile"
|
||||
},
|
||||
"workplace": {
|
||||
"name": "Workplace",
|
||||
|
|
18
frontend/rust-lib/Cargo.lock
generated
18
frontend/rust-lib/Cargo.lock
generated
|
@ -1786,7 +1786,7 @@ dependencies = [
|
|||
"cssparser-macros",
|
||||
"dtoa-short",
|
||||
"itoa",
|
||||
"phf 0.8.0",
|
||||
"phf 0.11.2",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
|
@ -5148,7 +5148,7 @@ version = "0.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_macros 0.8.0",
|
||||
"phf_shared 0.8.0",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
@ -5168,6 +5168,7 @@ version = "0.11.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_macros 0.11.3",
|
||||
"phf_shared 0.11.2",
|
||||
]
|
||||
|
||||
|
@ -5235,6 +5236,19 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator 0.11.2",
|
||||
"phf_shared 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.8.0"
|
||||
|
|
|
@ -31,29 +31,6 @@ async fn sign_up_with_invalid_email() {
|
|||
);
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn sign_up_with_long_password() {
|
||||
let sdk = EventIntegrationTest::new().await;
|
||||
let request = SignUpPayloadPB {
|
||||
email: unique_email(),
|
||||
name: valid_name(),
|
||||
password: "1234".repeat(100).as_str().to_string(),
|
||||
auth_type: AuthenticatorPB::Local,
|
||||
device_id: "".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
EventBuilder::new(sdk)
|
||||
.event(SignUp)
|
||||
.payload(request)
|
||||
.async_send()
|
||||
.await
|
||||
.error()
|
||||
.unwrap()
|
||||
.code,
|
||||
ErrorCode::PasswordTooLong
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sign_in_with_invalid_email() {
|
||||
|
|
|
@ -131,12 +131,10 @@ impl ChatCloudService for LocalServerChatServiceImpl {
|
|||
stream::once(async { Err(FlowyError::local_ai_unavailable().with_context(err)) }).boxed(),
|
||||
),
|
||||
}
|
||||
} else if self.local_ai.is_enabled() {
|
||||
Err(FlowyError::local_ai_not_ready())
|
||||
} else {
|
||||
if self.local_ai.is_enabled() {
|
||||
Err(FlowyError::local_ai_not_ready())
|
||||
} else {
|
||||
Err(FlowyError::local_ai_disabled())
|
||||
}
|
||||
Err(FlowyError::local_ai_disabled())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -247,12 +245,10 @@ impl ChatCloudService for LocalServerChatServiceImpl {
|
|||
),
|
||||
Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()),
|
||||
}
|
||||
} else if self.local_ai.is_enabled() {
|
||||
Err(FlowyError::local_ai_not_ready())
|
||||
} else {
|
||||
if self.local_ai.is_enabled() {
|
||||
Err(FlowyError::local_ai_not_ready())
|
||||
} else {
|
||||
Err(FlowyError::local_ai_disabled())
|
||||
}
|
||||
Err(FlowyError::local_ai_disabled())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,11 +31,10 @@ impl TryInto<SignInParams> for SignInPayloadPB {
|
|||
|
||||
fn try_into(self) -> Result<SignInParams, Self::Error> {
|
||||
let email = UserEmail::parse(self.email)?;
|
||||
let password = UserPassword::parse(self.password)?;
|
||||
|
||||
Ok(SignInParams {
|
||||
email: email.0,
|
||||
password: password.0,
|
||||
password: self.password,
|
||||
name: self.name,
|
||||
auth_type: self.auth_type.into(),
|
||||
})
|
||||
|
@ -65,13 +64,13 @@ impl TryInto<SignUpParams> for SignUpPayloadPB {
|
|||
|
||||
fn try_into(self) -> Result<SignUpParams, Self::Error> {
|
||||
let email = UserEmail::parse(self.email)?;
|
||||
let password = UserPassword::parse(self.password)?;
|
||||
let password = self.password;
|
||||
let name = UserName::parse(self.name)?;
|
||||
|
||||
Ok(SignUpParams {
|
||||
email: email.0,
|
||||
name: name.0,
|
||||
password: password.0,
|
||||
password,
|
||||
auth_type: self.auth_type.into(),
|
||||
device_id: self.device_id,
|
||||
})
|
||||
|
|
|
@ -4,7 +4,7 @@ use lib_infra::validator_fn::required_not_empty_str;
|
|||
use std::convert::TryInto;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword};
|
||||
use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey};
|
||||
use crate::entities::AuthenticatorPB;
|
||||
use crate::errors::ErrorCode;
|
||||
|
||||
|
@ -171,10 +171,7 @@ impl TryInto<UpdateUserProfileParams> for UpdateUserProfilePayloadPB {
|
|||
Some(email) => Some(UserEmail::parse(email)?.0),
|
||||
};
|
||||
|
||||
let password = match self.password {
|
||||
None => None,
|
||||
Some(password) => Some(UserPassword::parse(password)?.0),
|
||||
};
|
||||
let password = self.password;
|
||||
|
||||
let icon_url = match self.icon_url {
|
||||
None => None,
|
||||
|
|
Loading…
Add table
Reference in a new issue