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:
Lucas 2025-04-18 14:46:46 +08:00 committed by GitHub
parent 80df4955e2
commit 3bb5075a98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1854 additions and 264 deletions

View file

@ -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();

View file

@ -58,7 +58,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
userName: userName,
onSubmitted: (value) => context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserName(value)),
.add(SettingsUserEvent.updateUserName(name: value)),
);
},
);

View file

@ -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,
);
}

View file

@ -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()}'),
);
}
}
}

View file

@ -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,

View file

@ -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();
}

View file

@ -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
},
),
),
),
);
}
}

View file

@ -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,
),
),

View file

@ -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,

View file

@ -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,
),
),

View file

@ -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,

View file

@ -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;

View file

@ -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,

View file

@ -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,
),
),
],

View file

@ -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();

View file

@ -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();

View file

@ -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),
),
),
],

View file

@ -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,
),
),
],
);
}
}

View file

@ -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,
);
}
}
}

View file

@ -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),
),
),
);
}
}

View file

@ -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,
);
}
}
}

View file

@ -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 ==

View file

@ -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!,

View file

@ -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,
);
}
}

View file

@ -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(

View file

@ -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

View file

@ -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(

View file

@ -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();

View file

@ -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;

View file

@ -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;

View file

@ -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;
});
}
}

View file

@ -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,
);

View file

@ -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;

View 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

View 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

View 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

View file

@ -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",

View file

@ -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"

View file

@ -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() {

View file

@ -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())
}
}

View file

@ -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,
})

View file

@ -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,