feat: support account deletion (#6103)

* feat: support account deletion

* chore: update translation

* feat: support account deletion on mobile

* fix: only display account deletion button in appflowy cloud mode

* chore: remove unused code

* chore: update checkbox style

* fix: integration test

* chore: update translations

* chore: update sentry version in podfile.lock

* chore: update version
This commit is contained in:
Lucas.Xu 2024-09-03 16:58:38 +08:00 committed by GitHub
parent a6cce62b06
commit 9da71c3186
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 936 additions and 471 deletions

View file

@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.6.8"
APPFLOWY_VERSION = "0.6.9"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"

View file

@ -3,8 +3,6 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -12,6 +10,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
@ -19,6 +18,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
@ -51,12 +51,12 @@ void main() {
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
await tester.tapButton(find.byType(AccountSignInOutButton));
// sign up with Google
await tester.tapGoogleLoginInButton();
@ -68,7 +68,7 @@ void main() {
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
@ -85,7 +85,7 @@ void main() {
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
final userNameInput =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
tester.widget(find.byType(AccountUserProfile)) as AccountUserProfile;
expect(userNameInput.name, 'Me');
});
});

View file

@ -6,6 +6,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:easy_localization/easy_localization.dart';
@ -41,11 +42,11 @@ void main() {
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
await tester.tapButton(find.byType(AccountSignInOutButton));
tester.expectToSeeText(LocaleKeys.button_ok.tr());
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
@ -67,11 +68,11 @@ void main() {
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
await tester.tapButton(find.byType(AccountSignInOutButton));
tester.expectToSeeGoogleLoginButton();
});

View file

@ -2,8 +2,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -11,6 +9,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
@ -18,6 +17,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
@ -67,7 +67,7 @@ void main() {
// Verify name
final profileSetting =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
tester.widget(find.byType(AccountUserProfile)) as AccountUserProfile;
expect(profileSetting.name, name);
});

View file

@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
@ -20,12 +20,12 @@ extension AppFlowyAuthTest on WidgetTester {
Future<void> logout() async {
final scrollable = find.findSettingsScrollable();
await scrollUntilVisible(
find.byType(SignInOutButton),
find.byType(AccountSignInOutButton),
100,
scrollable: scrollable,
);
await tapButton(find.byType(SignInOutButton));
await tapButton(find.byType(AccountSignInOutButton));
expectToSeeText(LocaleKeys.button_ok.tr());
await tapButtonWithName(LocaleKeys.button_ok.tr());

View file

@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
@ -77,14 +77,14 @@ extension AppFlowySettings on WidgetTester {
Future<void> enterUserName(String name) async {
// Enable editing username
final editUsernameFinder = find.descendant(
of: find.byType(UserProfileSetting),
of: find.byType(AccountUserProfile),
matching: find.byFlowySvg(FlowySvgs.edit_s),
);
await tap(editUsernameFinder, warnIfMissed: false);
await pumpAndSettle();
final userNameFinder = find.descendant(
of: find.byType(UserProfileSetting),
of: find.byType(AccountUserProfile),
matching: find.byType(FlowyTextField),
);
await enterText(userNameFinder, name);

View file

@ -67,11 +67,11 @@ PODS:
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
- SDWebImage/Core (5.14.2)
- Sentry/HybridSDK (8.33.0)
- sentry_flutter (8.7.0):
- Sentry/HybridSDK (8.35.1)
- sentry_flutter (8.8.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.33.0)
- Sentry/HybridSDK (= 8.35.1)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@ -184,8 +184,8 @@ SPEC CHECKSUMS:
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 8560050221424aef0bebc8e31eedf00af80f90a6
sentry_flutter: e26b861f744e5037a3faf9bf56603ec65d658a61
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec

View file

@ -46,7 +46,9 @@ class MobileHomePageHeader extends StatelessWidget {
? _MobileWorkspace(userProfile: userProfile)
: _MobileUser(userProfile: userProfile),
),
const HomePageSettingsPopupMenu(),
HomePageSettingsPopupMenu(
userProfile: userProfile,
),
const HSpace(8.0),
],
),

View file

@ -87,6 +87,7 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
const SupportSettingGroup(),
const AboutSettingGroup(),
UserSessionSettingGroup(
userProfile: userProfile,
showThirdPartyLogin: showThirdPartyLogin,
),
const VSpace(20),

View file

@ -4,6 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';
import 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'
@ -18,7 +19,12 @@ enum _MobileSettingsPopupMenuItem {
}
class HomePageSettingsPopupMenu extends StatelessWidget {
const HomePageSettingsPopupMenu({super.key});
const HomePageSettingsPopupMenu({
super.key,
required this.userProfile,
});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
@ -41,12 +47,15 @@ class HomePageSettingsPopupMenu extends StatelessWidget {
svg: FlowySvgs.m_notification_settings_s,
text: LocaleKeys.settings_popupMenuItem_settings.tr(),
),
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _MobileSettingsPopupMenuItem.members,
svg: FlowySvgs.m_settings_member_s,
text: LocaleKeys.settings_popupMenuItem_members.tr(),
),
// only show the member items in cloud mode
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _MobileSettingsPopupMenuItem.members,
svg: FlowySvgs.m_settings_member_s,
text: LocaleKeys.settings_popupMenuItem_members.tr(),
),
],
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _MobileSettingsPopupMenuItem.trash,

View file

@ -1,10 +1,14 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account_deletion.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -13,60 +17,199 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class UserSessionSettingGroup extends StatelessWidget {
const UserSessionSettingGroup({
super.key,
required this.userProfile,
required this.showThirdPartyLogin,
});
final UserProfilePB userProfile;
final bool showThirdPartyLogin;
@override
Widget build(BuildContext context) {
return Column(
children: [
if (showThirdPartyLogin) ...[
BlocProvider(
create: (context) => getIt<SignInBloc>(),
child: BlocConsumer<SignInBloc, SignInState>(
listener: (context, state) {
state.successOrFail?.fold(
(result) => runAppFlowy(),
(e) => Log.error(e),
);
},
builder: (context, state) {
return const ThirdPartySignInButtons(
expanded: true,
);
},
// third party sign in buttons
if (showThirdPartyLogin) _buildThirdPartySignInButtons(context),
const VSpace(8.0),
// logout button
MobileLogoutButton(
text: LocaleKeys.settings_menu_logout.tr(),
onPressed: () async => _showLogoutDialog(),
),
// delete account button
// only show the delete account button in cloud mode
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[
const VSpace(16.0),
MobileLogoutButton(
text: LocaleKeys.button_deleteAccount.tr(),
textColor: Theme.of(context).colorScheme.error,
onPressed: () => _showDeleteAccountDialog(context),
),
],
],
);
}
Widget _buildThirdPartySignInButtons(BuildContext context) {
return BlocProvider(
create: (context) => getIt<SignInBloc>(),
child: BlocConsumer<SignInBloc, SignInState>(
listener: (context, state) {
state.successOrFail?.fold(
(result) => runAppFlowy(),
(e) => Log.error(e),
);
},
builder: (context, state) {
return const ThirdPartySignInButtons(
expanded: true,
);
},
),
);
}
Future<void> _showDeleteAccountDialog(BuildContext context) async {
return showMobileBottomSheet(
context,
useRootNavigator: true,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) => const _DeleteAccountBottomSheet(),
);
}
Future<void> _showLogoutDialog() async {
return showFlowyCupertinoConfirmDialog(
title: LocaleKeys.settings_menu_logoutPrompt.tr(),
leftButton: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
color: const Color(0xFF007AFF),
),
rightButton: FlowyText(
LocaleKeys.button_logout.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220),
),
onRightButtonPressed: (context) async {
Navigator.of(context).pop();
await getIt<AuthService>().signOut();
await runAppFlowy();
},
);
}
}
class _DeleteAccountBottomSheet extends StatefulWidget {
const _DeleteAccountBottomSheet();
@override
State<_DeleteAccountBottomSheet> createState() =>
_DeleteAccountBottomSheetState();
}
class _DeleteAccountBottomSheetState extends State<_DeleteAccountBottomSheet> {
final emailController = TextEditingController();
final isChecked = ValueNotifier(false);
@override
void dispose() {
emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(18.0),
const FlowySvg(
FlowySvgs.icon_warning_xl,
blendMode: null,
),
const VSpace(12.0),
FlowyText(
LocaleKeys.newSettings_myAccount_deleteAccount_title.tr(),
fontSize: 20.0,
fontWeight: FontWeight.w500,
),
const VSpace(12.0),
FlowyText(
LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint1.tr(),
fontSize: 14.0,
fontWeight: FontWeight.w400,
maxLines: 10,
),
const VSpace(18.0),
SizedBox(
height: 36.0,
child: FlowyTextField(
controller: emailController,
textStyle: const TextStyle(fontSize: 14.0),
hintStyle: const TextStyle(fontSize: 14.0),
hintText: LocaleKeys.settings_user_email.tr(),
),
),
const VSpace(8),
const VSpace(18.0),
_buildCheckbox(),
const VSpace(18.0),
MobileLogoutButton(
text: LocaleKeys.button_deleteAccount.tr(),
textColor: Theme.of(context).colorScheme.error,
onPressed: () => deleteMyAccount(
context,
emailController.text.trim(),
isChecked.value,
),
),
const VSpace(12.0),
MobileLogoutButton(
text: LocaleKeys.button_cancel.tr(),
onPressed: () => Navigator.of(context).pop(),
),
const VSpace(36.0),
],
MobileSignInOrLogoutButton(
labelText: LocaleKeys.settings_menu_logout.tr(),
onPressed: () async {
await showFlowyCupertinoConfirmDialog(
title: LocaleKeys.settings_menu_logoutPrompt.tr(),
leftButton: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
color: const Color(0xFF007AFF),
),
rightButton: FlowyText(
LocaleKeys.button_logout.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220),
),
onRightButtonPressed: (context) async {
Navigator.of(context).pop();
await getIt<AuthService>().signOut();
await runAppFlowy();
},
);
},
),
);
}
Widget _buildCheckbox() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => isChecked.value = !isChecked.value,
child: ValueListenableBuilder<bool>(
valueListenable: isChecked,
builder: (context, isChecked, _) {
return Padding(
padding: const EdgeInsets.all(1.0),
child: FlowySvg(
isChecked ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
size: const Size.square(16.0),
blendMode: isChecked ? null : BlendMode.srcIn,
),
);
},
),
),
const HSpace(6.0),
Expanded(
child: FlowyText.regular(
LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint2.tr(),
fontSize: 14.0,
figmaLineHeight: 18.0,
maxLines: 3,
),
),
],
);

View file

@ -1,7 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -9,6 +7,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
abstract class IUserBackendService {
Future<FlowyResult<void, FlowyError>> cancelSubscription(
@ -292,4 +291,9 @@ class UserBackendService implements IUserBackendService {
return UserEventUpdateWorkspaceSubscriptionPaymentPeriod(request).send();
}
// NOTE: This function is irreversible and will delete the current user's account.
static Future<FlowyResult<void, FlowyError>> deleteCurrentAccount() {
return UserEventDeleteAccount().send();
}
}

View file

@ -2,16 +2,18 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileSignInOrLogoutButton extends StatelessWidget {
const MobileSignInOrLogoutButton({
class MobileLogoutButton extends StatelessWidget {
const MobileLogoutButton({
super.key,
this.icon,
required this.labelText,
required this.text,
this.textColor,
required this.onPressed,
});
final FlowySvgData? icon;
final String labelText;
final String text;
final Color? textColor;
final VoidCallback onPressed;
@override
@ -26,7 +28,7 @@ class MobileSignInOrLogoutButton extends StatelessWidget {
Radius.circular(4),
),
border: Border.all(
color: style.colorScheme.outline,
color: textColor ?? style.colorScheme.outline,
width: 0.5,
),
),
@ -52,9 +54,10 @@ class MobileSignInOrLogoutButton extends StatelessWidget {
const HSpace(8),
],
FlowyText(
labelText,
text,
fontSize: 14.0,
fontWeight: FontWeight.w400,
color: textColor,
),
],
),

View file

@ -334,8 +334,10 @@ class _ConfirmPopupState extends State<ConfirmPopup> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(),
const VSpace(6),
_buildDescription(),
if (widget.description.isNotEmpty) ...[
const VSpace(6),
_buildDescription(),
],
if (widget.child != null) ...[
const VSpace(12),
widget.child!,
@ -376,6 +378,10 @@ class _ConfirmPopupState extends State<ConfirmPopup> {
}
Widget _buildDescription() {
if (widget.description.isEmpty) {
return const SizedBox.shrink();
}
return FlowyText.regular(
widget.description,
fontSize: 16.0,

View file

@ -0,0 +1,3 @@
export 'account_deletion.dart';
export 'account_sign_in_out.dart';
export 'account_user_profile.dart';

View file

@ -0,0 +1,262 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' show PlatformExtension;
import 'package:appflowy_result/appflowy_result.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:toastification/toastification.dart';
class AccountDeletionButton extends StatefulWidget {
const AccountDeletionButton({
super.key,
});
@override
State<AccountDeletionButton> createState() => _AccountDeletionButtonState();
}
class _AccountDeletionButtonState extends State<AccountDeletionButton> {
final TextEditingController emailController = TextEditingController();
final isCheckedNotifier = ValueNotifier(false);
@override
void dispose() {
emailController.dispose();
isCheckedNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final textColor = Theme.of(context).brightness == Brightness.light
? const Color(0xFF4F4F4F)
: const Color(0xFFB0B0B0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
LocaleKeys.button_deleteAccount.tr(),
fontSize: 14.0,
fontWeight: FontWeight.w500,
figmaLineHeight: 21.0,
color: textColor,
),
const VSpace(8),
Row(
children: [
Flexible(
child: FlowyText.regular(
LocaleKeys.newSettings_myAccount_deleteAccount_description.tr(),
fontSize: 12.0,
figmaLineHeight: 13.0,
maxLines: 2,
color: textColor,
),
),
const HSpace(32),
FlowyTextButton(
LocaleKeys.button_deleteAccount.tr(),
padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 10),
fillColor: Colors.transparent,
radius: Corners.s12Border,
hoverColor: Theme.of(context).colorScheme.error.withOpacity(0.1),
fontColor: Theme.of(context).colorScheme.error,
fontHoverColor: Colors.white,
fontSize: 12,
isDangerous: true,
lineHeight: 18.0 / 12.0,
onPressed: () {
isCheckedNotifier.value = false;
showCancelAndDeleteDialog(
context: context,
title:
LocaleKeys.newSettings_myAccount_deleteAccount_title.tr(),
description: '',
builder: (_) => _AccountDeletionDialog(
emailController: emailController,
isChecked: isCheckedNotifier,
),
onDelete: () => deleteMyAccount(
context,
emailController.text.trim(),
isCheckedNotifier.value,
),
);
},
),
],
),
],
);
}
}
class _AccountDeletionDialog extends StatelessWidget {
const _AccountDeletionDialog({
required this.emailController,
required this.isChecked,
});
final TextEditingController emailController;
final ValueNotifier<bool> isChecked;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FlowyText.regular(
LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint1.tr(),
fontSize: 14.0,
figmaLineHeight: 18.0,
maxLines: 2,
color: ConfirmPopupColor.descriptionColor(context),
),
const VSpace(12.0),
FlowyTextField(
hintText: LocaleKeys.settings_user_email.tr(),
controller: emailController,
),
const VSpace(16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => isChecked.value = !isChecked.value,
child: ValueListenableBuilder<bool>(
valueListenable: isChecked,
builder: (context, isChecked, _) {
return FlowySvg(
isChecked ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
size: const Size.square(16.0),
blendMode: isChecked ? null : BlendMode.srcIn,
);
},
),
),
const HSpace(6.0),
Expanded(
child: FlowyText.regular(
LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint2
.tr(),
fontSize: 14.0,
figmaLineHeight: 16.0,
maxLines: 3,
color: ConfirmPopupColor.descriptionColor(context),
),
),
],
),
],
);
}
}
Future<void> deleteMyAccount(
BuildContext context,
String email,
bool isChecked,
) async {
final bottomPadding = PlatformExtension.isMobile
? MediaQuery.of(context).viewInsets.bottom
: 0.0;
if (!isChecked) {
showToastNotification(
context,
type: ToastificationType.warning,
bottomPadding: bottomPadding,
message: LocaleKeys
.newSettings_myAccount_deleteAccount_checkToConfirmError
.tr(),
);
return;
}
// fetch the user email from server instead of reading from provider,
// this is to avoid the email doesn't match the real user's email
final userEmail = await UserBackendService.getCurrentUserProfile()
.fold((s) => s.email, (_) => null);
if (!context.mounted) {
return;
}
if (userEmail == null) {
showToastNotification(
context,
type: ToastificationType.error,
bottomPadding: bottomPadding,
message: LocaleKeys
.newSettings_myAccount_deleteAccount_failedToGetCurrentUser
.tr(),
);
return;
}
if (email.isEmpty || email.toLowerCase() != userEmail.toLowerCase()) {
showToastNotification(
context,
type: ToastificationType.warning,
bottomPadding: bottomPadding,
message: LocaleKeys
.newSettings_myAccount_deleteAccount_emailValidationFailed
.tr(),
);
return;
}
final loading = Loading(context)..start();
await UserBackendService.deleteCurrentAccount().fold(
(s) {
Log.info('account deletion success, email: $email');
loading.stop();
showToastNotification(
context,
message: LocaleKeys
.newSettings_myAccount_deleteAccount_deleteAccountSuccess
.tr(),
bottomPadding: bottomPadding,
);
// delay 1 second to make sure the toast notification is shown
Future.delayed(const Duration(seconds: 1), () async {
// pop to the home screen
Navigator.of(context).popUntil((route) {
if (route.settings.name == '/') {
return true;
}
return false;
});
// restart the application
await getIt<AuthService>().signOut();
await runAppFlowy();
});
},
(f) {
Log.error('account deletion failed, email: $email, error: $f');
loading.stop();
showToastNotification(
context,
type: ToastificationType.error,
bottomPadding: bottomPadding,
message: f.msg,
);
},
);
}

View file

@ -0,0 +1,185 @@
import 'package:appflowy/env/cloud_env.dart';
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/prelude.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.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: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 AccountSignInOutButton extends StatelessWidget {
const AccountSignInOutButton({
super.key,
required this.userProfile,
required this.onAction,
this.signIn = true,
});
final UserProfilePB userProfile;
final VoidCallback onAction;
final bool signIn;
@override
Widget build(BuildContext context) {
return PrimaryRoundedButton(
text: signIn
? LocaleKeys.settings_accountPage_login_loginLabel.tr()
: LocaleKeys.settings_accountPage_login_logoutLabel.tr(),
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
fontWeight: FontWeight.w600,
radius: 12.0,
onTap: () =>
signIn ? _showSignInDialog(context) : _showLogoutDialog(context),
);
}
void _showLogoutDialog(BuildContext context) {
showConfirmDialog(
context: context,
title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(),
description: userProfile.encryptionType == EncryptionTypePB.Symmetric
? LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr()
: LocaleKeys.settings_menu_logoutPrompt.tr(),
onConfirm: () async {
await getIt<AuthService>().signOut();
onAction();
},
);
}
Future<void> _showSignInDialog(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => BlocProvider<SignInBloc>(
create: (context) => getIt<SignInBloc>(),
child: const FlowyDialog(
constraints: BoxConstraints(maxHeight: 485, maxWidth: 375),
child: _SignInDialogContent(),
),
),
);
}
}
class _SignInDialogContent extends StatelessWidget {
const _SignInDialogContent();
@override
Widget build(BuildContext context) {
return ScaffoldMessenger(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
children: [
const _DialogHeader(),
const _DialogTitle(),
const VSpace(16),
const SignInWithMagicLinkButtons(),
if (isAuthEnabled) ...[
const VSpace(20),
const _OrDivider(),
const VSpace(10),
SettingThirdPartyLogin(
didLogin: () {},
), // TODO: Pass onAction
],
],
),
),
),
),
);
}
}
class _DialogHeader extends StatelessWidget {
const _DialogHeader();
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildBackButton(context),
_buildCloseButton(context),
],
);
}
Widget _buildBackButton(BuildContext context) {
return GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Row(
children: [
const FlowySvg(FlowySvgs.arrow_back_m, size: Size.square(24)),
const HSpace(8),
FlowyText.semibold(LocaleKeys.button_back.tr(), fontSize: 16),
],
),
),
);
}
Widget _buildCloseButton(BuildContext context) {
return GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: FlowySvg(
FlowySvgs.m_close_m,
size: const Size.square(20),
color: Theme.of(context).colorScheme.outline,
),
),
);
}
}
class _DialogTitle extends StatelessWidget {
const _DialogTitle();
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_accountPage_login_loginLabel.tr(),
fontSize: 22,
color: Theme.of(context).colorScheme.tertiary,
maxLines: null,
),
),
],
);
}
}
class _OrDivider extends StatelessWidget {
const _OrDivider();
@override
Widget build(BuildContext context) {
return Row(
children: [
const Flexible(child: Divider(thickness: 1)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: FlowyText.regular(LocaleKeys.signIn_or.tr()),
),
const Flexible(child: Divider(thickness: 1)),
],
);
}
}

View file

@ -0,0 +1,179 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Account name and account avatar
class AccountUserProfile extends StatefulWidget {
const AccountUserProfile({
super.key,
required this.name,
required this.iconUrl,
this.onSave,
});
final String name;
final String iconUrl;
final void Function(String)? onSave;
@override
State<AccountUserProfile> createState() => _AccountUserProfileState();
}
class _AccountUserProfileState extends State<AccountUserProfile> {
late final TextEditingController nameController =
TextEditingController(text: widget.name);
final FocusNode focusNode = FocusNode();
bool isEditing = false;
bool isHovering = false;
@override
void initState() {
super.initState();
focusNode
..addListener(_handleFocusChange)
..onKeyEvent = _handleKeyEvent;
}
@override
void dispose() {
nameController.dispose();
focusNode.removeListener(_handleFocusChange);
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAvatar(),
const HSpace(16),
Flexible(
child: isEditing ? _buildEditingField() : _buildNameDisplay(),
),
],
);
}
Widget _buildAvatar() {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _showIconPickerDialog(context),
child: FlowyHover(
resetHoverOnRebuild: false,
onHover: (state) => setState(() => isHovering = state),
style: HoverStyle(
hoverColor: Colors.transparent,
borderRadius: BorderRadius.circular(100),
),
child: FlowyTooltip(
message:
LocaleKeys.settings_accountPage_general_changeProfilePicture.tr(),
child: UserAvatar(
iconUrl: widget.iconUrl,
name: widget.name,
size: 48,
fontSize: 20,
isHovering: isHovering,
),
),
),
);
}
Widget _buildNameDisplay() {
return Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium(
widget.name,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(4),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => isEditing = true),
child: const FlowyHover(
resetHoverOnRebuild: false,
child: Padding(
padding: EdgeInsets.all(4),
child: FlowySvg(FlowySvgs.edit_s),
),
),
),
],
),
);
}
Widget _buildEditingField() {
return SettingsInputField(
textController: nameController,
value: widget.name,
focusNode: focusNode..requestFocus(),
onCancel: () => setState(() => isEditing = false),
onSave: (_) => _saveChanges(),
);
}
Future<void> _showIconPickerDialog(BuildContext context) {
return showDialog(
context: context,
builder: (dialogContext) => SimpleDialog(
children: [
Container(
height: 380,
width: 360,
margin: const EdgeInsets.all(0),
child: FlowyIconEmojiPicker(
onSelectedEmoji: (r) {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserIcon(iconUrl: r.emoji));
Navigator.of(dialogContext).pop();
},
),
),
],
),
);
}
void _handleFocusChange() {
if (!focusNode.hasFocus && isEditing && mounted) {
_saveChanges();
}
}
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape &&
isEditing &&
mounted) {
setState(() => isEditing = false);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
void _saveChanges() {
widget.onSave?.call(nameController.text);
setState(() => isEditing = false);
}
}

View file

@ -1,25 +1,15 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/prelude.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.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:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsAccountView extends StatefulWidget {
@ -56,10 +46,11 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
return SettingsBody(
title: LocaleKeys.settings_accountPage_title.tr(),
children: [
// user profile
SettingsCategory(
title: LocaleKeys.settings_accountPage_general_title.tr(),
children: [
UserProfileSetting(
AccountUserProfile(
name: userName,
iconUrl: state.userProfile.iconUrl,
onSave: (newName) {
@ -75,6 +66,7 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
],
),
// user email
// Only show email if the user is authenticated and not using local auth
if (isAuthEnabled &&
state.userProfile.authenticator != AuthenticatorPB.Local) ...[
@ -82,62 +74,15 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
title: LocaleKeys.settings_accountPage_email_title.tr(),
children: [
FlowyText.regular(state.userProfile.email),
// Enable when/if we need change email feature
// SingleSettingAction(
// label: state.userProfile.email,
// buttonLabel: LocaleKeys
// .settings_accountPage_email_actions_change
// .tr(),
// onPressed: () => SettingsAlertDialog(
// title: LocaleKeys
// .settings_accountPage_email_actions_change
// .tr(),
// confirmLabel: LocaleKeys.button_save.tr(),
// confirm: () {
// context.read<SettingsUserViewBloc>().add(
// SettingsUserEvent.updateUserEmail(
// _emailController.text,
// ),
// );
// Navigator.of(context).pop();
// },
// children: [
// SettingsInputField(
// label: LocaleKeys.settings_accountPage_email_title
// .tr(),
// value: state.userProfile.email,
// hideActions: true,
// textController: _emailController,
// ),
// ],
// ).show(context),
// ),
],
),
],
/// Enable when we have change password feature and 2FA
// const SettingsCategorySpacer(),
// SettingsCategory(
// title: 'Account & security',
// children: [
// SingleSettingAction(
// label: '**********',
// buttonLabel: 'Change password',
// onPressed: () {},
// ),
// SingleSettingAction(
// label: '2-step authentication',
// buttonLabel: 'Enable 2FA',
// onPressed: () {},
// ),
// ],
// ),
// user sign in/out
SettingsCategory(
title: LocaleKeys.settings_accountPage_login_title.tr(),
children: [
SignInOutButton(
AccountSignInOutButton(
userProfile: state.userProfile,
onAction:
state.userProfile.authenticator == AuthenticatorPB.Local
@ -149,22 +94,10 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
],
),
/// Enable when we can delete accounts
// const SettingsCategorySpacer(),
// SettingsSubcategory(
// title: 'Delete account',
// children: [
// SingleSettingAction(
// label:
// 'Permanently delete your account and remove access from all teamspaces.',
// labelMaxLines: 4,
// onPressed: () {},
// buttonLabel: 'Delete my account',
// isDangerous: true,
// fontSize: 12,
// ),
// ],
// ),
// user deletion
if (widget.userProfile.authenticator ==
AuthenticatorPB.AppFlowyCloud)
const AccountDeletionButton(),
],
);
},
@ -172,308 +105,3 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
);
}
}
@visibleForTesting
class SignInOutButton extends StatelessWidget {
const SignInOutButton({
super.key,
required this.userProfile,
required this.onAction,
this.signIn = true,
});
final UserProfilePB userProfile;
final VoidCallback onAction;
final bool signIn;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 48,
child: PrimaryRoundedButton(
text: signIn
? LocaleKeys.settings_accountPage_login_loginLabel.tr()
: LocaleKeys.settings_accountPage_login_logoutLabel.tr(),
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
fontWeight: FontWeight.w600,
radius: 12.0,
onTap: () {
if (signIn) {
_showSignInDialog(context);
} else {
showConfirmDialog(
context: context,
title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(),
description: switch (userProfile.encryptionType) {
EncryptionTypePB.Symmetric =>
LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr(),
_ => LocaleKeys.settings_menu_logoutPrompt.tr(),
},
onConfirm: () async {
await getIt<AuthService>().signOut();
onAction();
},
);
}
},
),
),
],
);
}
Future<void> _showSignInDialog(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => BlocProvider<SignInBloc>(
create: (context) => getIt<SignInBloc>(),
child: FlowyDialog(
constraints: const BoxConstraints(
maxHeight: 485,
maxWidth: 375,
),
child: ScaffoldMessenger(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Row(
children: [
const FlowySvg(
FlowySvgs.arrow_back_m,
size: Size.square(24),
),
const HSpace(8),
FlowyText.semibold(
LocaleKeys.button_back.tr(),
fontSize: 16,
),
],
),
),
),
const Spacer(),
GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: FlowySvg(
FlowySvgs.m_close_m,
size: const Size.square(20),
color: Theme.of(context).colorScheme.outline,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_accountPage_login_loginLabel
.tr(),
fontSize: 22,
color: Theme.of(context).colorScheme.tertiary,
maxLines: null,
),
),
],
),
const VSpace(16),
const SignInWithMagicLinkButtons(),
if (isAuthEnabled) ...[
const VSpace(20),
Row(
children: [
const Flexible(child: Divider(thickness: 1)),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
child: FlowyText.regular(
LocaleKeys.signIn_or.tr(),
),
),
const Flexible(child: Divider(thickness: 1)),
],
),
const VSpace(10),
SettingThirdPartyLogin(didLogin: onAction),
],
],
),
),
),
),
),
),
),
);
}
}
@visibleForTesting
class UserProfileSetting extends StatefulWidget {
const UserProfileSetting({
super.key,
required this.name,
required this.iconUrl,
this.onSave,
});
final String name;
final String iconUrl;
final void Function(String)? onSave;
@override
State<UserProfileSetting> createState() => _UserProfileSettingState();
}
class _UserProfileSettingState extends State<UserProfileSetting> {
late final _nameController = TextEditingController(text: widget.name);
late final FocusNode focusNode;
bool isEditing = false;
bool isHovering = false;
@override
void initState() {
super.initState();
focusNode = FocusNode(
onKeyEvent: (_, event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape &&
isEditing &&
mounted) {
setState(() => isEditing = false);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
)..addListener(() {
if (!focusNode.hasFocus && isEditing && mounted) {
widget.onSave?.call(_nameController.text);
setState(() => isEditing = false);
}
});
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _showIconPickerDialog(context),
child: FlowyHover(
resetHoverOnRebuild: false,
onHover: (state) => setState(() => isHovering = state),
style: HoverStyle(
hoverColor: Colors.transparent,
borderRadius: BorderRadius.circular(100),
),
child: FlowyTooltip(
message: LocaleKeys
.settings_accountPage_general_changeProfilePicture
.tr(),
child: UserAvatar(
iconUrl: widget.iconUrl,
name: widget.name,
size: 48,
fontSize: 20,
isHovering: isHovering,
),
),
),
),
const HSpace(16),
if (!isEditing) ...[
Flexible(
child: Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium(
widget.name,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(4),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => isEditing = true),
child: const FlowyHover(
resetHoverOnRebuild: false,
child: Padding(
padding: EdgeInsets.all(4),
child: FlowySvg(FlowySvgs.edit_s),
),
),
),
],
),
),
),
] else ...[
Flexible(
child: SettingsInputField(
textController: _nameController,
value: widget.name,
focusNode: focusNode..requestFocus(),
onCancel: () => setState(() => isEditing = false),
onSave: (val) {
widget.onSave?.call(val);
setState(() => isEditing = false);
},
),
),
],
],
);
}
Future<void> _showIconPickerDialog(BuildContext context) {
return showDialog(
context: context,
builder: (dialogContext) => SimpleDialog(
children: [
Container(
height: 380,
width: 360,
margin: const EdgeInsets.all(0),
child: FlowyIconEmojiPicker(
onSelectedEmoji: (r) {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserIcon(iconUrl: r.emoji));
Navigator.of(dialogContext).pop();
},
),
),
],
),
);
}
}

View file

@ -67,8 +67,8 @@ class RestartButton extends StatelessWidget {
// ],
// );
} else {
return MobileSignInOrLogoutButton(
labelText: LocaleKeys.settings_menu_restartApp.tr(),
return MobileLogoutButton(
text: LocaleKeys.settings_menu_restartApp.tr(),
onPressed: onClick,
);
}

View file

@ -523,3 +523,35 @@ Future<void> showCustomConfirmDialog({
},
);
}
Future<void> showCancelAndDeleteDialog({
required BuildContext context,
required String title,
required String description,
required Widget Function(BuildContext) builder,
VoidCallback? onDelete,
String? confirmLabel,
}) {
return showDialog(
context: context,
builder: (_) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: SizedBox(
width: 440,
child: ConfirmPopup(
title: title,
description: description,
onConfirm: () => onDelete?.call(),
closeOnAction: false,
confirmLabel: confirmLabel,
confirmButtonColor: Theme.of(context).colorScheme.error,
child: builder(context),
),
),
);
},
);
}

View file

@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.8
version: 0.6.9
environment:
flutter: ">=3.22.0"

View file

@ -2227,10 +2227,17 @@
"deleteAccount": {
"title": "Delete Account",
"subtitle": "Permanently delete your account and all of your data.",
"description": "Permanently delete your account and remove access from all teamspaces.",
"deleteMyAccount": "Delete my account",
"dialogTitle": "Delete account",
"dialogContent1": "Are you sure you want to permanently delete your account?",
"dialogContent2": "This action cannot be undone, and will remove access from all teamspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces."
"dialogContent2": "This action cannot be undone, and will remove access from all teamspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces.",
"confirmHint1": "Please type in your email address to confirm.",
"confirmHint2": "I understand that this action is irreversible and will permanently delete my account and all associated data.",
"checkToConfirmError": "You must check the box to confirm deletion",
"failedToGetCurrentUser": "Failed to get current user email",
"emailValidationFailed": "Your email address does not match the account email address",
"deleteAccountSuccess": "Account deleted successfully"
}
},
"workplace": {