feat: magic link sign-in as anonymous user (#5520)

* feat: magic link sign-in as anonymous user

* chore: remove commented code

* fix: custom sign-in dialog

* fix: bring back sign-out dialog

* fix: add minor space for sync indicator
This commit is contained in:
Mathias Mogensen 2024-06-11 22:17:29 +02:00 committed by GitHub
parent c9fe93920f
commit 815c99710e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 160 additions and 84 deletions

View File

@ -1,3 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
@ -9,7 +11,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB; show UserProfilePB;
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -30,19 +31,12 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
(event, emit) async { (event, emit) async {
await event.when( await event.when(
signedInWithUserEmailAndPassword: () async => _onSignIn(emit), signedInWithUserEmailAndPassword: () async => _onSignIn(emit),
signedInWithOAuth: (platform) async => _onSignInWithOAuth( signedInWithOAuth: (platform) async =>
emit, _onSignInWithOAuth(emit, platform),
platform,
),
signedInAsGuest: () async => _onSignInAsGuest(emit), signedInAsGuest: () async => _onSignInAsGuest(emit),
signedWithMagicLink: (email) async => _onSignInWithMagicLink( signedWithMagicLink: (email) async =>
emit, _onSignInWithMagicLink(emit, email),
email, deepLinkStateChange: (result) => _onDeepLinkStateChange(emit, result),
),
deepLinkStateChange: (result) => _onDeepLinkStateChange(
emit,
result,
),
cancel: () { cancel: () {
emit( emit(
state.copyWith( state.copyWith(
@ -72,9 +66,7 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
); );
}, },
switchLoginType: (type) { switchLoginType: (type) {
emit( emit(state.copyWith(loginType: type));
state.copyWith(loginType: type),
);
}, },
); );
}, },
@ -127,9 +119,7 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
} }
} }
Future<void> _onSignIn( Future<void> _onSignIn(Emitter<SignInState> emit) async {
Emitter<SignInState> emit,
) async {
final result = await authService.signInWithEmailPassword( final result = await authService.signInWithEmailPassword(
email: state.email ?? '', email: state.email ?? '',
password: state.password ?? '', password: state.password ?? '',
@ -158,9 +148,7 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
), ),
); );
final result = await authService.signUpWithOAuth( final result = await authService.signUpWithOAuth(platform: platform);
platform: platform,
);
emit( emit(
result.fold( result.fold(
(userProfile) => state.copyWith( (userProfile) => state.copyWith(
@ -185,15 +173,11 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
), ),
); );
final result = await authService.signInWithMagicLink( final result = await authService.signInWithMagicLink(email: email);
email: email,
);
emit( emit(
result.fold( result.fold(
(userProfile) => state.copyWith( (userProfile) => state.copyWith(isSubmitting: true),
isSubmitting: true,
),
(error) => _stateFromCode(error), (error) => _stateFromCode(error),
), ),
); );

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
@ -8,7 +10,6 @@ import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class DesktopSignInScreen extends StatelessWidget { class DesktopSignInScreen extends StatelessWidget {
@ -37,7 +38,6 @@ class DesktopSignInScreen extends StatelessWidget {
), ),
const VSpace(20), const VSpace(20),
// const SignInAnonymousButton(),
const SignInWithMagicLinkButtons(), const SignInWithMagicLinkButtons(),
// third-party sign in. // third-party sign in.

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/router.dart';
@ -5,15 +7,12 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_i
import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
class SignInScreen extends StatelessWidget { class SignInScreen extends StatelessWidget {
const SignInScreen({ const SignInScreen({super.key});
super.key,
});
static const routeName = '/SignInScreen'; static const routeName = '/SignInScreen';

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
@ -5,14 +7,11 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart'; import 'package:string_validator/string_validator.dart';
class SignInWithMagicLinkButtons extends StatefulWidget { class SignInWithMagicLinkButtons extends StatefulWidget {
const SignInWithMagicLinkButtons({ const SignInWithMagicLinkButtons({super.key});
super.key,
});
@override @override
State<SignInWithMagicLinkButtons> createState() => State<SignInWithMagicLinkButtons> createState() =>
@ -54,16 +53,13 @@ class _SignInWithMagicLinkButtonsState
void _sendMagicLink(BuildContext context, String email) { void _sendMagicLink(BuildContext context, String email) {
if (!isEmail(email)) { if (!isEmail(email)) {
showSnackBarMessage( return showSnackBarMessage(
context, context,
LocaleKeys.signIn_invalidEmail.tr(), LocaleKeys.signIn_invalidEmail.tr(),
duration: const Duration(seconds: 8), duration: const Duration(seconds: 8),
); );
return;
} }
// if (context.read<SignInBloc>().state.isSubmitting) {
// return;
// }
context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email)); context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email));
showSnackBarMessage( showSnackBarMessage(
context, context,

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart';
@ -8,14 +10,11 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class ThirdPartySignInButtons extends StatelessWidget { class ThirdPartySignInButtons extends StatelessWidget {
/// Used in DesktopSignInScreen, MobileSignInScreen and SettingThirdPartyLogin /// Used in DesktopSignInScreen, MobileSignInScreen and SettingThirdPartyLogin
const ThirdPartySignInButtons({ const ThirdPartySignInButtons({super.key});
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
@ -10,7 +12,6 @@ import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -115,10 +116,7 @@ class Body extends StatelessWidget {
return Container( return Container(
alignment: Alignment.center, alignment: Alignment.center,
child: PlatformExtension.isMobile child: PlatformExtension.isMobile
? const FlowySvg( ? const FlowySvg(FlowySvgs.flowy_logo_xl, blendMode: null)
FlowySvgs.flowy_logo_xl,
blendMode: null,
)
: const _DesktopSplashBody(), : const _DesktopSplashBody(),
); );
} }

View File

@ -1,9 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.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/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
@ -15,13 +20,9 @@ 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/auth.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsAccountView extends StatefulWidget { class SettingsAccountView extends StatefulWidget {
@ -238,33 +239,126 @@ class SignInOutButton extends StatelessWidget {
fillColor: Theme.of(context).colorScheme.primary, fillColor: Theme.of(context).colorScheme.primary,
hoverColor: const Color(0xFF005483), hoverColor: const Color(0xFF005483),
fontHoverColor: Colors.white, fontHoverColor: Colors.white,
onPressed: () => SettingsAlertDialog( onPressed: () {
title: signIn if (signIn) {
? LocaleKeys.settings_accountPage_login_loginLabel.tr() _showSignInDialog(context);
: LocaleKeys.settings_accountPage_login_logoutLabel.tr(), } else {
subtitle: signIn SettingsAlertDialog(
? null title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(),
: switch (userProfile.encryptionType) { subtitle: switch (userProfile.encryptionType) {
EncryptionTypePB.Symmetric => LocaleKeys EncryptionTypePB.Symmetric =>
.settings_menu_selfEncryptionLogoutPrompt LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr(),
.tr(),
_ => LocaleKeys.settings_menu_logoutPrompt.tr(), _ => LocaleKeys.settings_menu_logoutPrompt.tr(),
}, },
implyLeading: signIn, confirm: () async {
confirm: !signIn
? () async {
await getIt<AuthService>().signOut(); await getIt<AuthService>().signOut();
onAction(); onAction();
},
).show(context);
} }
: null, },
children:
signIn ? [SettingThirdPartyLogin(didLogin: onAction)] : null,
).show(context),
), ),
), ),
], ],
); );
} }
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: 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 @visibleForTesting

View File

@ -1,10 +1,11 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/widget/dialog/dialog_size.dart'; import 'package:flowy_infra_ui/widget/dialog/dialog_size.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
extension IntoDialog on Widget { extension IntoDialog on Widget {
Future<dynamic> show(BuildContext context) async { Future<dynamic> show(BuildContext context) async {
@ -57,15 +58,17 @@ class StyledDialog extends StatelessWidget {
); );
if (shrinkWrap) { if (shrinkWrap) {
innerContent = innerContent = IntrinsicWidth(
IntrinsicWidth(child: IntrinsicHeight(child: innerContent)); child: IntrinsicHeight(
child: innerContent,
));
} }
return FocusTraversalGroup( return FocusTraversalGroup(
child: Container( child: Container(
margin: margin ?? EdgeInsets.all(Insets.sm * 2), margin: margin ?? EdgeInsets.all(Insets.sm * 2),
alignment: Alignment.center, alignment: Alignment.center,
child: Container( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minWidth: DialogSize.minDialogWidth, minWidth: DialogSize.minDialogWidth,
maxHeight: maxHeight ?? double.infinity, maxHeight: maxHeight ?? double.infinity,

View File

@ -675,6 +675,9 @@ impl UserManager {
email: &str, email: &str,
redirect_to: &str, redirect_to: &str,
) -> Result<(), FlowyError> { ) -> Result<(), FlowyError> {
self
.cloud_services
.set_user_authenticator(&Authenticator::AppFlowyCloud);
let auth_service = self.cloud_services.get_user_service()?; let auth_service = self.cloud_services.get_user_service()?;
auth_service auth_service
.sign_in_with_magic_link(email, redirect_to) .sign_in_with_magic_link(email, redirect_to)