feat: sign in with password or passcode

This commit is contained in:
LucasXu0 2025-04-04 14:43:15 +08:00
parent 10dd0fa438
commit adbf5e6d8d
43 changed files with 1095 additions and 467 deletions

View file

@ -19,6 +19,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -223,31 +224,36 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
Tooltip.dismissAllToolTips();
}
},
child: MaterialApp.router(
builder: (context, child) => MediaQuery(
// use the 1.0 as the textScaleFactor to avoid the text size
// affected by the system setting.
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(state.textScaleFactor),
),
child: overlayManagerBuilder(
context,
!UniversalPlatform.isMobile && FeatureFlag.search.isOn
? CommandPalette(
notifier: _commandPaletteNotifier,
child: child,
)
: child,
child: AppFlowyTheme(
data: state.themeMode == ThemeMode.light
? AppFlowyThemeData.light()
: AppFlowyThemeData.dark(),
child: MaterialApp.router(
builder: (context, child) => MediaQuery(
// use the 1.0 as the textScaleFactor to avoid the text size
// affected by the system setting.
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(state.textScaleFactor),
),
child: overlayManagerBuilder(
context,
!UniversalPlatform.isMobile && FeatureFlag.search.isOn
? CommandPalette(
notifier: _commandPaletteNotifier,
child: child,
)
: child,
),
),
debugShowCheckedModeBanner: false,
theme: state.lightTheme,
darkTheme: state.darkTheme,
themeMode: state.themeMode,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: state.locale,
routerConfig: routerConfig,
),
debugShowCheckedModeBanner: false,
theme: state.lightTheme,
darkTheme: state.darkTheme,
themeMode: state.themeMode,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: state.locale,
routerConfig: routerConfig,
),
),
),

View file

@ -83,6 +83,13 @@ class AppFlowyCloudDeepLink {
void unsubscribeDeepLinkLoadingState(VoidCallback listener) =>
_stateNotifier?.removeListener(listener);
Future<void> passGotrueTokenResponse(
GotrueTokenResponsePB gotrueTokenResponse,
) async {
final uri = _buildDeepLinkUri(gotrueTokenResponse);
await _handleUri(uri);
}
Future<void> _handleUri(
Uri? uri,
) async {
@ -173,6 +180,57 @@ class AppFlowyCloudDeepLink {
bool _isPaymentSuccessUri(Uri uri) {
return uri.host == 'payment-success';
}
Uri? _buildDeepLinkUri(GotrueTokenResponsePB gotrueTokenResponse) {
final params = <String, String>{};
if (gotrueTokenResponse.hasAccessToken() &&
gotrueTokenResponse.accessToken.isNotEmpty) {
params['access_token'] = gotrueTokenResponse.accessToken;
}
if (gotrueTokenResponse.hasExpiresAt()) {
params['expires_at'] = gotrueTokenResponse.expiresAt.toString();
}
if (gotrueTokenResponse.hasExpiresIn()) {
params['expires_in'] = gotrueTokenResponse.expiresIn.toString();
}
if (gotrueTokenResponse.hasProviderRefreshToken() &&
gotrueTokenResponse.providerRefreshToken.isNotEmpty) {
params['provider_refresh_token'] =
gotrueTokenResponse.providerRefreshToken;
}
if (gotrueTokenResponse.hasProviderAccessToken() &&
gotrueTokenResponse.providerAccessToken.isNotEmpty) {
params['provider_token'] = gotrueTokenResponse.providerAccessToken;
}
if (gotrueTokenResponse.hasRefreshToken() &&
gotrueTokenResponse.refreshToken.isNotEmpty) {
params['refresh_token'] = gotrueTokenResponse.refreshToken;
}
if (gotrueTokenResponse.hasTokenType() &&
gotrueTokenResponse.tokenType.isNotEmpty) {
params['token_type'] = gotrueTokenResponse.tokenType;
}
if (params.isEmpty) {
return null;
}
final fragment = params.entries
.map(
(e) =>
'${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}',
)
.join('&');
return Uri.parse('appflowy-flutter://login-callback#$fragment');
}
}
class InitAppFlowyCloudTask extends LaunchTask {

View file

@ -32,12 +32,17 @@ class AppFlowyCloudAuthService implements AuthService {
}
@override
Future<FlowyResult<UserProfilePB, FlowyError>> signInWithEmailPassword({
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithEmailPassword({
required String email,
required String password,
Map<String, String> params = const {},
}) async {
throw UnimplementedError();
return _backendAuthService.signInWithEmailPassword(
email: email,
password: password,
params: params,
);
}
@override
@ -106,6 +111,17 @@ class AppFlowyCloudAuthService implements AuthService {
);
}
@override
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({
required String email,
required String passcode,
}) async {
return _backendAuthService.signInWithPasscode(
email: email,
passcode: passcode,
);
}
@override
Future<FlowyResult<UserProfilePB, FlowyError>> getUser() async {
return UserBackendService.getCurrentUserProfile();

View file

@ -33,7 +33,8 @@ class AppFlowyCloudMockAuthService implements AuthService {
}
@override
Future<FlowyResult<UserProfilePB, FlowyError>> signInWithEmailPassword({
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithEmailPassword({
required String email,
required String password,
Map<String, String> params = const {},
@ -106,4 +107,12 @@ class AppFlowyCloudMockAuthService implements AuthService {
Future<FlowyResult<UserProfilePB, FlowyError>> getUser() async {
return UserBackendService.getCurrentUserProfile();
}
@override
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({
required String email,
required String passcode,
}) async {
throw UnimplementedError();
}
}

View file

@ -1,5 +1,5 @@
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
class AuthServiceMapKeys {
@ -23,7 +23,8 @@ abstract class AuthService {
///
/// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
Future<FlowyResult<UserProfilePB, FlowyError>> signInWithEmailPassword({
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithEmailPassword({
required String email,
required String password,
Map<String, String> params,
@ -75,6 +76,17 @@ abstract class AuthService {
Map<String, String> params,
});
/// Authenticates a user with a passcode sent to their email.
///
/// - `email`: The email address of the user.
/// - `passcode`: The passcode of the user.
///
/// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({
required String email,
required String passcode,
});
/// Signs out the currently authenticated user.
Future<void> signOut();

View file

@ -19,7 +19,8 @@ class BackendAuthService implements AuthService {
final AuthenticatorPB authType;
@override
Future<FlowyResult<UserProfilePB, FlowyError>> signInWithEmailPassword({
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithEmailPassword({
required String email,
required String password,
Map<String, String> params = const {},
@ -29,8 +30,7 @@ class BackendAuthService implements AuthService {
..password = password
..authType = authType
..deviceId = await getDeviceId();
final response = UserEventSignInWithEmailPassword(request).send();
return response.then((value) => value);
return UserEventSignInWithEmailPassword(request).send();
}
@override
@ -107,4 +107,12 @@ class BackendAuthService implements AuthService {
// No need to pass the redirect URL.
return UserBackendService.signInWithMagicLink(email, '');
}
@override
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({
required String email,
required String passcode,
}) async {
return UserBackendService.signInWithPasscode(email, passcode);
}
}

View file

@ -30,12 +30,26 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
on<SignInEvent>(
(event, emit) async {
await event.when(
signedInWithUserEmailAndPassword: () async => _onSignIn(emit),
signedInWithOAuth: (platform) async =>
_onSignInWithOAuth(emit, platform),
signedInAsGuest: () async => _onSignInAsGuest(emit),
signedWithMagicLink: (email) async =>
_onSignInWithMagicLink(emit, email),
signInWithEmailAndPassword: (email, password) async =>
_onSignInWithEmailAndPassword(
emit,
email: email,
password: password,
),
signInWithOAuth: (platform) async => _onSignInWithOAuth(
emit,
platform: platform,
),
signInAsGuest: () async => _onSignInAsGuest(emit),
signInWithMagicLink: (email) async => _onSignInWithMagicLink(
emit,
email: email,
),
signInWithPasscode: (email, passcode) async => _onSignInWithPasscode(
emit,
email: email,
passcode: passcode,
),
deepLinkStateChange: (result) => _onDeepLinkStateChange(emit, result),
cancel: () {
emit(
@ -119,26 +133,34 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
}
}
Future<void> _onSignIn(Emitter<SignInState> emit) async {
Future<void> _onSignInWithEmailAndPassword(
Emitter<SignInState> emit, {
required String email,
required String password,
}) async {
final result = await authService.signInWithEmailPassword(
email: state.email ?? '',
password: state.password ?? '',
email: email,
password: password,
);
emit(
result.fold(
(userProfile) => state.copyWith(
isSubmitting: false,
successOrFail: FlowyResult.success(userProfile),
),
(gotrueTokenResponse) {
getIt<AppFlowyCloudDeepLink>().passGotrueTokenResponse(
gotrueTokenResponse,
);
return state.copyWith(
isSubmitting: false,
);
},
(error) => _stateFromCode(error),
),
);
}
Future<void> _onSignInWithOAuth(
Emitter<SignInState> emit,
String platform,
) async {
Emitter<SignInState> emit, {
required String platform,
}) async {
emit(
state.copyWith(
isSubmitting: true,
@ -161,9 +183,9 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
}
Future<void> _onSignInWithMagicLink(
Emitter<SignInState> emit,
String email,
) async {
Emitter<SignInState> emit, {
required String email,
}) async {
emit(
state.copyWith(
isSubmitting: true,
@ -183,6 +205,40 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
);
}
Future<void> _onSignInWithPasscode(
Emitter<SignInState> emit, {
required String email,
required String passcode,
}) async {
emit(
state.copyWith(
isSubmitting: true,
emailError: null,
passwordError: null,
successOrFail: null,
),
);
final result = await authService.signInWithPasscode(
email: email,
passcode: passcode,
);
emit(
result.fold(
(gotrueTokenResponse) {
getIt<AppFlowyCloudDeepLink>().passGotrueTokenResponse(
gotrueTokenResponse,
);
return state.copyWith(
isSubmitting: false,
);
},
(error) => _stateFromCode(error),
),
);
}
Future<void> _onSignInAsGuest(
Emitter<SignInState> emit,
) async {
@ -243,19 +299,35 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
@freezed
class SignInEvent with _$SignInEvent {
const factory SignInEvent.signedInWithUserEmailAndPassword() =
SignedInWithUserEmailAndPassword;
const factory SignInEvent.signedInWithOAuth(String platform) =
SignedInWithOAuth;
const factory SignInEvent.signedInAsGuest() = SignedInAsGuest;
const factory SignInEvent.signedWithMagicLink(String email) =
SignedWithMagicLink;
const factory SignInEvent.emailChanged(String email) = EmailChanged;
const factory SignInEvent.passwordChanged(String password) = PasswordChanged;
// Sign in methods
const factory SignInEvent.signInWithEmailAndPassword({
required String email,
required String password,
}) = SignInWithEmailAndPassword;
const factory SignInEvent.signInWithOAuth({
required String platform,
}) = SignInWithOAuth;
const factory SignInEvent.signInAsGuest() = SignInAsGuest;
const factory SignInEvent.signInWithMagicLink({
required String email,
}) = SignInWithMagicLink;
const factory SignInEvent.signInWithPasscode({
required String email,
required String passcode,
}) = SignInWithPasscode;
// Event handlers
const factory SignInEvent.emailChanged({
required String email,
}) = EmailChanged;
const factory SignInEvent.passwordChanged({
required String password,
}) = PasswordChanged;
const factory SignInEvent.deepLinkStateChange(DeepLinkResult result) =
DeepLinkStateChange;
const factory SignInEvent.cancel() = _Cancel;
const factory SignInEvent.switchLoginType(LoginType type) = _SwitchLoginType;
const factory SignInEvent.cancel() = Cancel;
const factory SignInEvent.switchLoginType(LoginType type) = SwitchLoginType;
}
// we support sign in directly without sign up, but we want to allow the users to sign up if they want to

View file

@ -86,6 +86,15 @@ class UserBackendService implements IUserBackendService {
return UserEventMagicLinkSignIn(payload).send();
}
static Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
signInWithPasscode(
String email,
String passcode,
) async {
final payload = PasscodeSignInPB(email: email, passcode: passcode);
return UserEventPasscodeSignIn(payload).send();
}
static Future<FlowyResult<void, FlowyError>> signOut() {
return UserEventSignOut().send();
}

View file

@ -1,11 +1,13 @@
import 'package:appflowy/core/frameless_window.dart';
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/settings/show_settings.dart';
import 'package:appflowy/shared/window_title_bar.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/user/presentation/widgets/widgets.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';
@ -19,6 +21,8 @@ class DesktopSignInScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
const indicatorMinHeight = 4.0;
return BlocBuilder<SignInBloc, SignInState>(
builder: (context, state) {
@ -29,25 +33,23 @@ class DesktopSignInScreen extends StatelessWidget {
children: [
const Spacer(),
const VSpace(20),
// logo and title
FlowyLogoTitle(
title: LocaleKeys.welcomeText.tr(),
logoSize: const Size(60, 60),
logoSize: Size.square(36),
),
const VSpace(20),
VSpace(theme.spacing.xxl),
// magic link sign in
const SignInWithMagicLinkButtons(),
const VSpace(20),
// continue with email and password
const ContinueWithEmailAndPassword(),
VSpace(theme.spacing.xxl),
// third-party sign in.
if (isAuthEnabled) ...[
const _OrDivider(),
const VSpace(20),
VSpace(theme.spacing.xxl),
const ThirdPartySignInButtons(),
const VSpace(20),
VSpace(theme.spacing.xxl),
],
// sign in agreement
@ -69,7 +71,7 @@ class DesktopSignInScreen extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
DesktopSignInSettingsButton(),
HSpace(42),
HSpace(20),
SignInAnonymousButtonV2(),
],
),
@ -99,18 +101,24 @@ class DesktopSignInSettingsButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.signIn_settings.tr(),
textAlign: TextAlign.center,
fontSize: 12.0,
// fontWeight: FontWeight.w500,
color: Colors.grey,
decoration: TextDecoration.underline,
final theme = AppFlowyTheme.of(context);
return AFGhostIconTextButton(
text: LocaleKeys.signIn_settings.tr(),
textColor: (context, isHovering, disabled) {
return theme.textColorScheme.secondary;
},
size: AFButtonSize.s,
padding: EdgeInsets.symmetric(
horizontal: theme.spacing.m,
vertical: theme.spacing.xs,
),
onTap: () {
showSimpleSettingsDialog(context);
onTap: () => showSimpleSettingsDialog(context),
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
FlowySvgs.settings_s,
size: Size.square(20),
color: theme.textColorScheme.secondary,
);
},
);
}
@ -121,14 +129,30 @@ class _OrDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Row(
children: [
const Flexible(child: Divider(thickness: 1)),
Flexible(
child: Divider(
thickness: 1,
color: theme.borderColorScheme.greyTertiary,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: FlowyText.regular(LocaleKeys.signIn_or.tr()),
child: Text(
LocaleKeys.signIn_or.tr(),
style: theme.textStyle.body.standard(
color: theme.textColorScheme.secondary,
),
),
),
Flexible(
child: Divider(
thickness: 1,
color: theme.borderColorScheme.greyTertiary,
),
),
const Flexible(child: Divider(thickness: 1)),
],
);
}

View file

@ -37,7 +37,7 @@ class MobileSignInScreen extends StatelessWidget {
const VSpace(spacing * 2),
isLocalAuthEnabled
? const SignInAnonymousButtonV3()
: const SignInWithMagicLinkButtons(),
: const ContinueWithEmailAndPassword(),
const VSpace(spacing),
if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme),
const VSpace(spacing * 1.5),

View file

@ -34,7 +34,7 @@ class SignInAnonymousButtonV3 extends StatelessWidget {
? () {
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
.add(const SignInEvent.signInAsGuest());
}
: () {
final bloc = context.read<AnonUserBloc>();

View file

@ -0,0 +1,16 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class AnonymousSignInButton extends StatelessWidget {
const AnonymousSignInButton({super.key});
@override
Widget build(BuildContext context) {
return AFGhostButton.normal(
onTap: () {},
builder: (context, isHovering, disabled) {
return const Placeholder();
},
);
}
}

View file

@ -0,0 +1,21 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class ContinueWithEmail extends StatelessWidget {
const ContinueWithEmail({
super.key,
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return AFFilledTextButton.primary(
text: 'Continue with email',
size: AFButtonSize.l,
alignment: Alignment.center,
onTap: onTap,
);
}
}

View file

@ -0,0 +1,131 @@
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/workspace/presentation/widgets/dialogs.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';
import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
class ContinueWithEmailAndPassword extends StatefulWidget {
const ContinueWithEmailAndPassword({super.key});
@override
State<ContinueWithEmailAndPassword> createState() =>
_ContinueWithEmailAndPasswordState();
}
class _ContinueWithEmailAndPasswordState
extends State<ContinueWithEmailAndPassword> {
final controller = TextEditingController();
final focusNode = FocusNode();
@override
void dispose() {
controller.dispose();
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Column(
children: [
SizedBox(
height: UniversalPlatform.isMobile ? 38.0 : 40.0,
child: AFTextField(
controller: controller,
hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(),
radius: 10,
onSubmitted: (value) => _pushContinueWithMagicLinkOrPasscodePage(
context,
value,
),
),
),
VSpace(theme.spacing.l),
ContinueWithEmail(
onTap: () => _pushContinueWithMagicLinkOrPasscodePage(
context,
controller.text,
),
),
VSpace(theme.spacing.l),
ContinueWithPassword(
onTap: () => _pushContinueWithPasswordPage(
context,
controller.text,
),
),
],
);
}
void _pushContinueWithMagicLinkOrPasscodePage(
BuildContext context,
String email,
) {
if (!isEmail(email)) {
return showToastNotification(
context,
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);
}
final signInBloc = context.read<SignInBloc>();
signInBloc.add(SignInEvent.signInWithMagicLink(email: email));
// push the a continue with magic link or passcode screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ContinueWithMagicLinkOrPasscodePage(
email: email,
backToLogin: () => Navigator.pop(context),
onEnterPasscode: (passcode) => signInBloc.add(
SignInEvent.signInWithPasscode(
email: email,
passcode: passcode,
),
),
),
),
);
}
void _pushContinueWithPasswordPage(
BuildContext context,
String email,
) {
final signInBloc = context.read<SignInBloc>();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ContinueWithPasswordPage(
email: email,
backToLogin: () => Navigator.pop(context),
onEnterPassword: (password) => signInBloc.add(
SignInEvent.signInWithEmailAndPassword(
email: email,
password: password,
),
),
onForgotPassword: () {
// todo: implement forgot password
},
),
),
);
}
}

View file

@ -0,0 +1,153 @@
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class ContinueWithMagicLinkOrPasscodePage extends StatefulWidget {
const ContinueWithMagicLinkOrPasscodePage({
super.key,
required this.backToLogin,
required this.email,
required this.onEnterPasscode,
});
final String email;
final VoidCallback backToLogin;
final ValueChanged<String> onEnterPasscode;
@override
State<ContinueWithMagicLinkOrPasscodePage> createState() =>
_ContinueWithMagicLinkOrPasscodePageState();
}
class _ContinueWithMagicLinkOrPasscodePageState
extends State<ContinueWithMagicLinkOrPasscodePage> {
final passcodeController = TextEditingController();
bool isEnteringPasscode = false;
@override
void dispose() {
passcodeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
width: 320,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo, title and description
..._buildLogoTitleAndDescription(),
// Enter code manually
..._buildEnterCodeManually(),
// Back to login
..._buildBackToLogin(),
],
),
),
),
);
}
List<Widget> _buildEnterCodeManually() {
// todo: ask designer to provide the spacing
final spacing = VSpace(20);
if (!isEnteringPasscode) {
return [
AFFilledTextButton.primary(
text: 'Enter code manually',
onTap: () => setState(() => isEnteringPasscode = true),
size: AFButtonSize.l,
alignment: Alignment.center,
),
spacing,
];
}
return [
// Enter code manually
SizedBox(
height: 40, // fixme: use the height from the designer
child: AFTextField(
controller: passcodeController,
hintText: 'Enter code',
keyboardType: TextInputType.number,
radius: 10,
autoFocus: true,
onSubmitted: widget.onEnterPasscode,
),
),
// todo: ask designer to provide the spacing
VSpace(12),
// continue to login
AFFilledTextButton.primary(
text: 'Continue to sign up',
onTap: () => widget.onEnterPasscode(passcodeController.text),
size: AFButtonSize.l,
alignment: Alignment.center,
),
spacing,
];
}
List<Widget> _buildBackToLogin() {
return [
AFGhostTextButton(
text: 'Back to login',
size: AFButtonSize.s,
onTap: widget.backToLogin,
textColor: (context, isHovering, disabled) {
final theme = AppFlowyTheme.of(context);
return theme.textColorScheme.theme;
},
),
];
}
List<Widget> _buildLogoTitleAndDescription() {
final theme = AppFlowyTheme.of(context);
final spacing = VSpace(theme.spacing.xxl);
return [
// logo
const AFLogo(),
spacing,
// title
Text(
'Check your email',
style: theme.textStyle.heading.h3(
color: theme.textColorScheme.primary,
),
),
spacing,
// description
Text(
'A temporary verification link has been sent. Please check your inbox at',
style: theme.textStyle.body.standard(
color: theme.textColorScheme.primary,
),
textAlign: TextAlign.center,
),
Text(
widget.email,
style: theme.textStyle.body.enhanced(
color: theme.textColorScheme.primary,
),
textAlign: TextAlign.center,
),
spacing,
];
}
}

View file

@ -0,0 +1,21 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class ContinueWithPassword extends StatelessWidget {
const ContinueWithPassword({
super.key,
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return AFOutlinedTextButton.normal(
text: 'Continue with password',
size: AFButtonSize.l,
alignment: Alignment.center,
onTap: onTap,
);
}
}

View file

@ -0,0 +1,147 @@
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class ContinueWithPasswordPage extends StatefulWidget {
const ContinueWithPasswordPage({
super.key,
required this.backToLogin,
required this.email,
required this.onEnterPassword,
required this.onForgotPassword,
});
final String email;
final VoidCallback backToLogin;
final ValueChanged<String> onEnterPassword;
final VoidCallback onForgotPassword;
@override
State<ContinueWithPasswordPage> createState() =>
_ContinueWithPasswordPageState();
}
class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
final passwordController = TextEditingController();
@override
void dispose() {
passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
width: 320,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo and title
..._buildLogoAndTitle(),
// Password input and buttons
..._buildPasswordSection(),
// Back to login
..._buildBackToLogin(),
],
),
),
),
);
}
List<Widget> _buildLogoAndTitle() {
final theme = AppFlowyTheme.of(context);
final spacing = VSpace(theme.spacing.xxl);
return [
// logo
const AFLogo(),
spacing,
// title
Text(
'Enter password',
style: theme.textStyle.heading.h3(
color: theme.textColorScheme.primary,
),
),
spacing,
// email display
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Login as ',
style: theme.textStyle.body.standard(
color: theme.textColorScheme.primary,
),
),
TextSpan(
text: widget.email,
style: theme.textStyle.body.enhanced(
color: theme.textColorScheme.primary,
),
),
],
),
),
spacing,
];
}
List<Widget> _buildPasswordSection() {
return [
// Password input
AFTextField(
controller: passwordController,
hintText: 'Enter password',
autoFocus: true,
onSubmitted: widget.onEnterPassword,
),
// todo: ask designer to provide the spacing
VSpace(12),
// todo: forgot password is not implemented yet
// Forgot password button
// AFGhostTextButton(
// text: 'Forget password?',
// size: AFButtonSize.s,
// onTap: widget.onForgotPassword,
// textColor: (context, isHovering, disabled) {
// return theme.textColorScheme.theme;
// },
// ),
VSpace(12),
// Continue button
AFFilledTextButton.primary(
text: 'Continue',
onTap: () => widget.onEnterPassword(passwordController.text),
size: AFButtonSize.l,
alignment: Alignment.center,
),
VSpace(20),
];
}
List<Widget> _buildBackToLogin() {
return [
AFGhostTextButton(
text: 'Back to Login',
size: AFButtonSize.s,
onTap: widget.backToLogin,
textColor: (context, isHovering, disabled) {
final theme = AppFlowyTheme.of(context);
return theme.textColorScheme.theme;
},
),
];
}
}

View file

@ -0,0 +1,20 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flutter/material.dart';
class AFLogo extends StatelessWidget {
const AFLogo({
super.key,
this.size = const Size.square(36),
});
final Size size;
@override
Widget build(BuildContext context) {
return FlowySvg(
FlowySvgs.flowy_logo_xl,
blendMode: null,
size: size,
);
}
}

View file

@ -1,131 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.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:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
class SignInWithMagicLinkButtons extends StatefulWidget {
const SignInWithMagicLinkButtons({super.key});
@override
State<SignInWithMagicLinkButtons> createState() =>
_SignInWithMagicLinkButtonsState();
}
class _SignInWithMagicLinkButtonsState
extends State<SignInWithMagicLinkButtons> {
final controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
@override
void dispose() {
controller.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: UniversalPlatform.isMobile ? 38.0 : 48.0,
child: FlowyTextField(
autoFocus: false,
focusNode: _focusNode,
controller: controller,
borderRadius: BorderRadius.circular(4.0),
hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(),
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 14.0,
color: Theme.of(context).hintColor,
),
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 14.0,
),
keyboardType: TextInputType.emailAddress,
onSubmitted: (_) => _sendMagicLink(context, controller.text),
onTapOutside: (_) => _focusNode.unfocus(),
),
),
const VSpace(12),
_ConfirmButton(
onTap: () => _sendMagicLink(context, controller.text),
),
],
);
}
void _sendMagicLink(BuildContext context, String email) {
if (!isEmail(email)) {
return showToastNotification(
context,
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);
}
context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email));
showConfirmDialog(
context: context,
title: LocaleKeys.signIn_magicLinkSent.tr(),
description: LocaleKeys.signIn_magicLinkSentDescription.tr(),
);
}
}
class _ConfirmButton extends StatelessWidget {
const _ConfirmButton({
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return BlocBuilder<SignInBloc, SignInState>(
builder: (context, state) {
final name = switch (state.loginType) {
LoginType.signIn => LocaleKeys.signIn_signInWithMagicLink.tr(),
LoginType.signUp => LocaleKeys.signIn_signUpWithMagicLink.tr(),
};
if (UniversalPlatform.isMobile) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 32),
maximumSize: const Size(double.infinity, 38),
),
onPressed: onTap,
child: FlowyText(
name,
fontSize: 14,
color: Theme.of(context).colorScheme.onPrimary,
),
);
} else {
return SizedBox(
height: 48,
child: FlowyButton(
isSelected: true,
onTap: onTap,
hoverColor: Theme.of(context).colorScheme.primary,
text: FlowyText.medium(
name,
textAlign: TextAlign.center,
color: Theme.of(context).colorScheme.onPrimary,
),
radius: Corners.s6Border,
),
);
}
},
);
}
}

View file

@ -1,6 +1,7 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@ -12,6 +13,13 @@ class SignInAgreement extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
final textStyle = theme.textStyle.caption.standard(
color: theme.textColorScheme.secondary,
);
final underlinedTextStyle = theme.textStyle.caption.underline(
color: theme.textColorScheme.secondary,
);
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
@ -20,30 +28,22 @@ class SignInAgreement extends StatelessWidget {
text: isLocalAuthEnabled
? '${LocaleKeys.web_signInLocalAgreement.tr()} '
: '${LocaleKeys.web_signInAgreement.tr()} ',
style: const TextStyle(color: Colors.grey, fontSize: 12),
style: textStyle,
),
TextSpan(
text: '${LocaleKeys.web_termOfUse.tr()} ',
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
decoration: TextDecoration.underline,
),
style: underlinedTextStyle,
mouseCursor: SystemMouseCursors.click,
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString('https://appflowy.io/terms'),
),
TextSpan(
text: '${LocaleKeys.web_and.tr()} ',
style: const TextStyle(color: Colors.grey, fontSize: 12),
style: textStyle,
),
TextSpan(
text: LocaleKeys.web_privacyPolicy.tr(),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
decoration: TextDecoration.underline,
),
style: underlinedTextStyle,
mouseCursor: SystemMouseCursors.click,
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString('https://appflowy.io/privacy'),

View file

@ -1,91 +1,14 @@
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/anon_user_bloc.dart';
import 'package:appflowy/user/application/sign_in_bloc.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:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
/// Used in DesktopSignInScreen and MobileSignInScreen
class SignInAnonymousButton extends StatelessWidget {
const SignInAnonymousButton({
super.key,
});
@override
Widget build(BuildContext context) {
final isMobile = UniversalPlatform.isMobile;
return BlocBuilder<SignInBloc, SignInState>(
builder: (context, signInState) {
return BlocProvider(
create: (context) => AnonUserBloc()
..add(
const AnonUserEvent.initial(),
),
child: BlocListener<AnonUserBloc, AnonUserState>(
listener: (context, state) async {
if (state.openedAnonUser != null) {
await runAppFlowy();
}
},
child: BlocBuilder<AnonUserBloc, AnonUserState>(
builder: (context, state) {
final text = state.anonUsers.isEmpty
? LocaleKeys.signIn_loginStartWithAnonymous.tr()
: LocaleKeys.signIn_continueAnonymousUser.tr();
final onTap = state.anonUsers.isEmpty
? () {
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
}
: () {
final bloc = context.read<AnonUserBloc>();
final user = bloc.state.anonUsers.first;
bloc.add(AnonUserEvent.openAnonUser(user));
};
// SignInAnonymousButton in mobile
if (isMobile) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
),
onPressed: onTap,
child: FlowyText(
LocaleKeys.signIn_loginStartWithAnonymous.tr(),
fontSize: 14,
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.w500,
),
);
}
// SignInAnonymousButton in desktop
return SizedBox(
height: 48,
child: FlowyButton(
isSelected: true,
disable: signInState.isSubmitting,
text: FlowyText.medium(
text,
textAlign: TextAlign.center,
),
radius: Corners.s6Border,
onTap: onTap,
),
);
},
),
),
);
},
);
}
}
class SignInAnonymousButtonV2 extends StatelessWidget {
const SignInAnonymousButtonV2({
@ -109,27 +32,35 @@ class SignInAnonymousButtonV2 extends StatelessWidget {
},
child: BlocBuilder<AnonUserBloc, AnonUserState>(
builder: (context, state) {
final text = LocaleKeys.signIn_anonymous.tr();
final theme = AppFlowyTheme.of(context);
final onTap = state.anonUsers.isEmpty
? () {
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
.add(const SignInEvent.signInAsGuest());
}
: () {
final bloc = context.read<AnonUserBloc>();
final user = bloc.state.anonUsers.first;
bloc.add(AnonUserEvent.openAnonUser(user));
};
return FlowyButton(
useIntrinsicWidth: true,
onTap: onTap,
text: FlowyText(
text,
color: Colors.grey,
decoration: TextDecoration.underline,
fontSize: 12,
return AFGhostIconTextButton(
text: LocaleKeys.signIn_anonymousMode.tr(),
textColor: (context, isHovering, disabled) {
return theme.textColorScheme.secondary;
},
padding: EdgeInsets.symmetric(
horizontal: theme.spacing.m,
vertical: theme.spacing.xs,
),
size: AFButtonSize.s,
onTap: onTap,
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
FlowySvgs.anonymous_mode_m,
color: theme.textColorScheme.secondary,
);
},
);
},
),

View file

@ -1,9 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/user/presentation/widgets/widgets.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';
@ -102,11 +101,11 @@ class MobileThirdPartySignInButton extends StatelessWidget {
super.key,
this.height = 38,
this.fontSize = 14.0,
required this.onPressed,
required this.onTap,
required this.type,
});
final VoidCallback onPressed;
final VoidCallback onTap;
final double height;
final double fontSize;
final ThirdPartySignInButtonType type;
@ -117,7 +116,7 @@ class MobileThirdPartySignInButton extends StatelessWidget {
return AnimatedGestureDetector(
scaleFactor: 1.0,
onTapUp: onPressed,
onTapUp: onTap,
child: Container(
height: height,
decoration: BoxDecoration(
@ -153,67 +152,29 @@ class MobileThirdPartySignInButton extends StatelessWidget {
}
}
class DesktopSignInButton extends StatelessWidget {
const DesktopSignInButton({
class DesktopThirdPartySignInButton extends StatelessWidget {
const DesktopThirdPartySignInButton({
super.key,
required this.type,
required this.onPressed,
required this.onTap,
});
final ThirdPartySignInButtonType type;
final VoidCallback onPressed;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
// In desktop, the width of button is limited by [AuthFormContainer]
return SizedBox(
height: 48,
width: AuthFormContainer.width,
child: OutlinedButton.icon(
// In order to align all the labels vertically in a relatively centered position to the button, we use a fixed width container to wrap the icon(align to the right), then use another container to align the label to left.
icon: Container(
width: AuthFormContainer.width / 4,
alignment: Alignment.centerRight,
child: SizedBox(
// Some icons are not square, so we just use a fixed width here.
width: 24,
child: FlowySvg(
type.icon,
blendMode: type.blendMode,
),
),
),
label: Container(
padding: const EdgeInsets.only(left: 8),
alignment: Alignment.centerLeft,
child: FlowyText(
type.labelText,
fontSize: 14,
),
),
style: ButtonStyle(
overlayColor: WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(WidgetState.hovered)) {
return style.colorScheme.onSecondaryContainer;
}
return null;
},
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: Corners.s6Border,
),
),
side: WidgetStateProperty.all(
BorderSide(
color: style.dividerColor,
),
),
),
onPressed: onPressed,
),
return AFOutlinedIconTextButton.normal(
text: type.labelText,
onTap: onTap,
size: AFButtonSize.l,
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
type.icon,
size: Size.square(18),
blendMode: type.blendMode,
);
},
);
}
}

View file

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.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';
@ -40,7 +41,7 @@ class ThirdPartySignInButtons extends StatelessWidget {
void _signIn(BuildContext context, String provider) {
context.read<SignInBloc>().add(
SignInEvent.signedInWithOAuth(provider),
SignInEvent.signInWithOAuth(platform: provider),
);
}
}
@ -58,23 +59,22 @@ class _DesktopThirdPartySignIn extends StatefulWidget {
}
class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> {
static const padding = 12.0;
bool isExpanded = false;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Column(
children: [
DesktopSignInButton(
DesktopThirdPartySignInButton(
key: signInWithGoogleButtonKey,
type: ThirdPartySignInButtonType.google,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google),
),
const VSpace(padding),
DesktopSignInButton(
VSpace(theme.spacing.l),
DesktopThirdPartySignInButton(
type: ThirdPartySignInButtonType.apple,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
),
...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(),
],
@ -82,38 +82,35 @@ class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> {
}
List<Widget> _buildExpandedButtons() {
final theme = AppFlowyTheme.of(context);
return [
const VSpace(padding * 1.5),
DesktopSignInButton(
VSpace(theme.spacing.l),
DesktopThirdPartySignInButton(
type: ThirdPartySignInButtonType.github,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github),
),
const VSpace(padding),
DesktopSignInButton(
VSpace(theme.spacing.l),
DesktopThirdPartySignInButton(
type: ThirdPartySignInButtonType.discord,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord),
),
];
}
List<Widget> _buildCollapsedButtons() {
final theme = AppFlowyTheme.of(context);
return [
const VSpace(padding),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
child: FlowyText(
LocaleKeys.signIn_continueAnotherWay.tr(),
color: Theme.of(context).colorScheme.onSurface,
decoration: TextDecoration.underline,
fontSize: 14,
),
),
VSpace(theme.spacing.l),
AFGhostTextButton(
text: 'More options',
textColor: (context, isHovering, disabled) {
return theme.textColorScheme.theme;
},
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
),
];
}
@ -153,14 +150,14 @@ class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> {
if (Platform.isIOS) ...[
MobileThirdPartySignInButton(
type: ThirdPartySignInButtonType.apple,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
),
const VSpace(padding),
],
MobileThirdPartySignInButton(
key: signInWithGoogleButtonKey,
type: ThirdPartySignInButtonType.google,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google),
),
...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(),
],
@ -172,12 +169,12 @@ class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> {
const VSpace(padding),
MobileThirdPartySignInButton(
type: ThirdPartySignInButtonType.github,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github),
),
const VSpace(padding),
MobileThirdPartySignInButton(
type: ThirdPartySignInButtonType.discord,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord),
onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord),
),
];
}

View file

@ -1,7 +1,7 @@
export 'magic_link_sign_in_buttons.dart';
export 'continue_with/continue_with_email_and_password.dart';
export 'sign_in_agreement.dart';
export 'sign_in_anonymous_button.dart';
export 'sign_in_or_logout_button.dart';
export 'third_party_sign_in_button.dart';
export 'third_party_sign_in_button/third_party_sign_in_button.dart';
// export 'switch_sign_in_sign_up_button.dart';
export 'third_party_sign_in_buttons.dart';
export 'sign_in_agreement.dart';
export 'third_party_sign_in_button/third_party_sign_in_buttons.dart';

View file

@ -8,7 +8,7 @@ class AuthFormContainer extends StatelessWidget {
final List<Widget> children;
static const double width = 340;
static const double width = 320;
@override
Widget build(BuildContext context) {

View file

@ -1,8 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra/size.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class FlowyLogoTitle extends StatelessWidget {
const FlowyLogoTitle({
@ -16,24 +15,19 @@ class FlowyLogoTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return SizedBox(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox.fromSize(
size: logoSize,
child: const FlowySvg(
FlowySvgs.flowy_logo_xl,
blendMode: null,
),
),
AFLogo(size: logoSize),
const VSpace(20),
FlowyText.regular(
Text(
title,
fontSize: FontSizes.s24,
fontFamily:
GoogleFonts.poppins(fontWeight: FontWeight.w500).fontFamily,
color: Theme.of(context).colorScheme.tertiary,
style: theme.textStyle.heading.h3(
color: theme.textColorScheme.primary,
),
),
],
),

View file

@ -4,7 +4,7 @@ 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/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/widgets/setting_third_party_login.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
@ -111,7 +111,7 @@ class _SignInDialogContent extends StatelessWidget {
const _DialogHeader(),
const _DialogTitle(),
const VSpace(16),
const SignInWithMagicLinkButtons(),
const ContinueWithEmailAndPassword(),
if (isAuthEnabled) ...[
const VSpace(20),
const _OrDivider(),

View file

@ -9,7 +9,7 @@ AppFlowy UI is a Flutter package that provides a collection of reusable UI compo
## Installation
Add the following to your *app's* `pubspec.yaml` file:
Add the following to your `pubspec.yaml` file:
```yaml
dependencies:

View file

@ -1,4 +1,2 @@
library;
export 'src/component/component.dart';
export 'src/theme/theme.dart';

View file

@ -126,6 +126,13 @@ packages:
relative: true
source: path
version: "0.0.1"
appflowy_ui:
dependency: "direct main"
description:
path: "packages/appflowy_ui"
relative: true
source: path
version: "1.0.0"
archive:
dependency: "direct main"
description:
@ -2597,5 +2604,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"
dart: ">=3.6.2 <4.0.0"
flutter: ">=3.27.4"

View file

@ -25,7 +25,8 @@ dependencies:
path: packages/appflowy_popover
appflowy_result:
path: packages/appflowy_result
appflowy_ui:
path: packages/appflowy_ui
archive: ^3.4.10
auto_size_text_field: ^2.2.3
auto_updater: ^1.0.0

View file

@ -37,6 +37,7 @@
"loginStartWithAnonymous": "Continue with an anonymous session",
"continueAnonymousUser": "Continue with an anonymous session",
"anonymous": "Anonymous",
"anonymousMode": "Anonymous mode",
"buttonText": "Sign In",
"signingInText": "Signing in...",
"forgotPassword": "Forgot Password?",

17
frontend/rust-lib/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "AF-desktop: Debug Rust",
"type": "lldb",
// "request": "attach",
// "pid": "${command:pickMyProcess}"
// To launch the application directly, use the following configuration:
"request": "launch",
"program": "/Users/lucas.xu/Desktop/appflowy_backup/frontend/appflowy_flutter/build/macos/Build/Products/Debug/AppFlowy.app",
},
]
}

View file

@ -496,7 +496,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"anyhow",
"bincode",
@ -516,7 +516,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"anyhow",
"bytes",
@ -1137,7 +1137,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"again",
"anyhow",
@ -1192,7 +1192,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -1205,7 +1205,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"futures-channel",
"futures-util",
@ -1478,7 +1478,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"anyhow",
"bincode",
@ -1500,7 +1500,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"anyhow",
"async-trait",
@ -1947,7 +1947,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"bincode",
"bytes",
@ -3432,7 +3432,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"anyhow",
"getrandom 0.2.10",
@ -3447,7 +3447,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"app-error",
"jsonwebtoken",
@ -4068,7 +4068,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"anyhow",
"bytes",
@ -6782,7 +6782,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=02315b1e11b9c2b6f71a503b7844a94f9787e31a#02315b1e11b9c2b6f71a503b7844a94f9787e31a"
dependencies = [
"anyhow",
"app-error",

View file

@ -103,8 +103,8 @@ dashmap = "6.0.1"
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f7288f46c27dc8e3c7829cda1b70b61118e88336" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f7288f46c27dc8e3c7829cda1b70b61118e88336" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "02315b1e11b9c2b6f71a503b7844a94f9787e31a" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "02315b1e11b9c2b6f71a503b7844a94f9787e31a" }
[profile.dev]
opt-level = 0

View file

@ -13,7 +13,7 @@ use client_api::entity::workspace_dto::{
};
use client_api::entity::{
AFRole, AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange,
AuthProvider, CollabParams, CreateCollabParams, QueryWorkspaceMember,
AuthProvider, CollabParams, CreateCollabParams, GotrueTokenResponse, QueryWorkspaceMember,
};
use client_api::entity::{QueryCollab, QueryCollabParams};
use client_api::{Client, ClientConfiguration};
@ -120,16 +120,13 @@ where
&self,
email: &str,
password: &str,
) -> Result<UserProfile, FlowyError> {
) -> Result<GotrueTokenResponse, FlowyError> {
let password = password.to_string();
let email = email.to_string();
let try_get_client = self.server.try_get_client();
let client = try_get_client?;
client.sign_in_password(&email, &password).await?;
let profile = client.get_profile().await?;
let token = client.get_token()?;
let profile = user_profile_from_af_profile(token, profile)?;
Ok(profile)
let response = client.sign_in_password(&email, &password).await?;
Ok(response)
}
async fn sign_in_with_magic_link(
@ -147,6 +144,19 @@ where
Ok(())
}
async fn sign_in_with_passcode(
&self,
email: &str,
passcode: &str,
) -> Result<GotrueTokenResponse, FlowyError> {
let email = email.to_owned();
let passcode = passcode.to_owned();
let try_get_client = self.server.try_get_client();
let client = try_get_client?;
let response = client.sign_in_with_passcode(&email, &passcode).await?;
Ok(response)
}
async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result<String, FlowyError> {
let provider = AuthProvider::from(provider);
let try_get_client = self.server.try_get_client();

View file

@ -1,3 +1,4 @@
use client_api::entity::GotrueTokenResponse;
use collab::core::origin::CollabOrigin;
use collab::preclude::Collab;
use collab_entity::CollabObject;
@ -97,7 +98,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
&self,
_email: &str,
_password: &str,
) -> Result<UserProfile, FlowyError> {
) -> Result<GotrueTokenResponse, FlowyError> {
Err(FlowyError::local_version_not_support().with_context("Not support"))
}
@ -109,6 +110,14 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
Err(FlowyError::local_version_not_support().with_context("Not support"))
}
async fn sign_in_with_passcode(
&self,
_email: &str,
_passcode: &str,
) -> Result<GotrueTokenResponse, FlowyError> {
Err(FlowyError::local_version_not_support().with_context("Not support"))
}
async fn generate_oauth_url_with_provider(&self, _provider: &str) -> Result<String, FlowyError> {
Err(FlowyError::internal().with_context("Can't oauth url when using offline mode"))
}

View file

@ -199,6 +199,16 @@ where
})
}
fn sign_in_with_passcode(
&self,
_email: &str,
_passcode: &str,
) -> FutureResult<GotrueTokenResponse, FlowyError> {
FutureResult::new(async {
Err(FlowyError::not_support().with_context("Can't sign in with passcode when using supabase"))
})
}
fn generate_oauth_url_with_provider(&self, _provider: &str) -> FutureResult<String, FlowyError> {
FutureResult::new(async {
Err(FlowyError::internal().with_context("Can't generate oauth url when using supabase"))

View file

@ -4,6 +4,7 @@ use client_api::entity::billing_dto::SubscriptionPlanDetail;
pub use client_api::entity::billing_dto::SubscriptionStatus;
use client_api::entity::billing_dto::WorkspaceSubscriptionStatus;
use client_api::entity::billing_dto::WorkspaceUsageAndLimit;
use client_api::entity::GotrueTokenResponse;
pub use client_api::entity::{AFWorkspaceSettings, AFWorkspaceSettingsChange};
use collab_entity::{CollabObject, CollabType};
use flowy_error::{internal_error, ErrorCode, FlowyError};
@ -148,11 +149,17 @@ pub trait UserCloudService: Send + Sync + 'static {
&self,
email: &str,
password: &str,
) -> Result<UserProfile, FlowyError>;
) -> Result<GotrueTokenResponse, FlowyError>;
async fn sign_in_with_magic_link(&self, email: &str, redirect_to: &str)
-> Result<(), FlowyError>;
async fn sign_in_with_passcode(
&self,
email: &str,
passcode: &str,
) -> Result<GotrueTokenResponse, FlowyError>;
/// When the user opens the OAuth URL, it redirects to the corresponding provider's OAuth web page.
/// After the user is authenticated, the browser will open a deep link to the AppFlowy app (iOS, macOS, etc.),
/// which will call [Client::sign_in_with_url]generate_sign_in_url_with_email to sign in.

View file

@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::convert::TryInto;
use client_api::entity::GotrueTokenResponse;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_user_pub::entities::*;
@ -86,6 +87,53 @@ pub struct MagicLinkSignInPB {
pub redirect_to: String,
}
#[derive(ProtoBuf, Default)]
pub struct PasscodeSignInPB {
#[pb(index = 1)]
pub email: String,
#[pb(index = 2)]
pub passcode: String,
}
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct GotrueTokenResponsePB {
#[pb(index = 1)]
pub access_token: String,
#[pb(index = 2)]
pub token_type: String,
#[pb(index = 3)]
pub expires_in: i64,
#[pb(index = 4)]
pub expires_at: i64,
#[pb(index = 5)]
pub refresh_token: String,
#[pb(index = 6, one_of)]
pub provider_access_token: Option<String>,
#[pb(index = 7, one_of)]
pub provider_refresh_token: Option<String>,
}
impl From<GotrueTokenResponse> for GotrueTokenResponsePB {
fn from(response: GotrueTokenResponse) -> Self {
Self {
access_token: response.access_token,
token_type: response.token_type,
expires_in: response.expires_in,
expires_at: response.expires_at,
refresh_token: response.refresh_token,
provider_access_token: response.provider_access_token,
provider_refresh_token: response.provider_refresh_token,
}
}
}
#[derive(ProtoBuf, Default)]
pub struct OauthSignInPB {
/// Use this field to store the third party auth information.

View file

@ -39,14 +39,16 @@ fn upgrade_store_preferences(
pub async fn sign_in_with_email_password_handler(
data: AFPluginData<SignInPayloadPB>,
manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<UserProfilePB, FlowyError> {
) -> DataResult<GotrueTokenResponsePB, FlowyError> {
let manager = upgrade_manager(manager)?;
let params: SignInParams = data.into_inner().try_into()?;
let auth_type = params.auth_type.clone();
let old_authenticator = manager.cloud_services.get_user_authenticator();
match manager.sign_in(params, auth_type).await {
Ok(profile) => data_result_ok(UserProfilePB::from(profile)),
match manager
.sign_in_with_password(&params.email, &params.password)
.await
{
Ok(token) => data_result_ok(token.into()),
Err(err) => {
manager
.cloud_services
@ -317,6 +319,19 @@ pub async fn sign_in_with_magic_link_handler(
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub async fn sign_in_with_passcode_handler(
data: AFPluginData<PasscodeSignInPB>,
manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<GotrueTokenResponsePB, FlowyError> {
let manager = upgrade_manager(manager)?;
let params = data.into_inner();
let response = manager
.sign_in_with_passcode(&params.email, &params.passcode)
.await?;
data_result_ok(response.into())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub async fn oauth_sign_in_handler(
data: AFPluginData<OauthSignInPB>,

View file

@ -81,7 +81,7 @@ pub fn init(user_manager: Weak<UserManager>) -> AFPlugin {
.event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting)
.event(UserEvent::GetWorkspaceSetting, get_workspace_setting)
.event(UserEvent::NotifyDidSwitchPlan, notify_did_switch_plan_handler)
.event(UserEvent::PasscodeSignIn, sign_in_with_passcode_handler)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@ -89,7 +89,7 @@ pub fn init(user_manager: Weak<UserManager>) -> AFPlugin {
pub enum UserEvent {
/// Only use when the [Authenticator] is Local or SelfHosted
/// Logging into an account using a register email and password
#[event(input = "SignInPayloadPB", output = "UserProfilePB")]
#[event(input = "SignInPayloadPB", output = "GotrueTokenResponsePB")]
SignInWithEmailPassword = 0,
/// Only use when the [Authenticator] is Local or SelfHosted
@ -278,6 +278,9 @@ pub enum UserEvent {
#[event()]
DeleteAccount = 64,
#[event(input = "PasscodeSignInPB", output = "GotrueTokenResponsePB")]
PasscodeSignIn = 65,
}
#[async_trait]

View file

@ -1,3 +1,4 @@
use client_api::entity::GotrueTokenResponse;
use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use collab_integrate::CollabKVDB;
use flowy_error::{internal_error, ErrorCode, FlowyResult};
@ -720,6 +721,19 @@ impl UserManager {
Ok(url)
}
pub(crate) async fn sign_in_with_password(
&self,
email: &str,
password: &str,
) -> Result<GotrueTokenResponse, FlowyError> {
self
.cloud_services
.set_user_authenticator(&Authenticator::AppFlowyCloud);
let auth_service = self.cloud_services.get_user_service()?;
let response = auth_service.sign_in_with_password(email, password).await?;
Ok(response)
}
pub(crate) async fn sign_in_with_magic_link(
&self,
email: &str,
@ -735,6 +749,19 @@ impl UserManager {
Ok(())
}
pub(crate) async fn sign_in_with_passcode(
&self,
email: &str,
passcode: &str,
) -> Result<GotrueTokenResponse, FlowyError> {
self
.cloud_services
.set_user_authenticator(&Authenticator::AppFlowyCloud);
let auth_service = self.cloud_services.get_user_service()?;
let response = auth_service.sign_in_with_passcode(email, passcode).await?;
Ok(response)
}
pub(crate) async fn generate_oauth_url(
&self,
oauth_provider: &str,