feat: sign in with passcode (#7685)

* feat: sign in with password or passcode

* feat: add loading dialog

* chore: update translations

* feat: support otp on mobile
This commit is contained in:
Lucas 2025-04-10 14:06:08 +08:00 committed by Lucas.Xu
parent 8aa6dd8821
commit 170e09bae9
51 changed files with 1228 additions and 453 deletions

View file

@ -15,7 +15,6 @@ void main() {
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapContinousAnotherWay();
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();

View file

@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget {
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy'),
onTap: () => afLaunchUrlString('https://appflowy.com/privacy'),
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/terms'),
onTap: () => afLaunchUrlString('https://appflowy.com/terms'),
),
if (kDebugMode)
MobileSettingItem(

View file

@ -282,11 +282,12 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
void _inviteMember(BuildContext context) {
final email = emailController.text;
if (!isEmail(email)) {
return showToastNotification(
showToastNotification(
context,
type: ToastificationType.error,
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
);
return;
}
context
.read<WorkspaceMemberBloc>()

View file

@ -104,10 +104,11 @@ Future<void> downloadMediaFile(
await afLaunchUrlString(file.url);
} else {
if (userProfile == null) {
return showToastNotification(
showToastNotification(
context,
message: LocaleKeys.grid_media_downloadFailedToken.tr(),
);
return;
}
final uri = Uri.parse(file.url);

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: Theme.of(context).brightness == Brightness.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 {
@ -224,10 +280,17 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
emailError: null,
);
case ErrorCode.UserUnauthorized:
final errorMsg = error.msg;
String msg = LocaleKeys.signIn_generalError.tr();
if (errorMsg.contains('rate limit')) {
msg = LocaleKeys.signIn_limitRateError.tr();
} else if (errorMsg.contains('invalid')) {
msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr();
}
return state.copyWith(
isSubmitting: false,
successOrFail: FlowyResult.failure(
FlowyError(msg: LocaleKeys.signIn_limitRateError.tr()),
FlowyError(msg: msg),
),
);
default:
@ -243,19 +306,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

@ -7,6 +7,7 @@ import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/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';
@ -37,7 +38,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),
@ -103,21 +104,28 @@ class MobileSignInScreen extends StatelessWidget {
}
Widget _buildSettingsButton(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.signIn_settings.tr(),
textAlign: TextAlign.center,
fontSize: 12.0,
// fontWeight: FontWeight.w500,
color: Colors.grey,
decoration: TextDecoration.underline,
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: () {
context.push(MobileLaunchSettingsPage.routeName);
onTap: () => context.push(MobileLaunchSettingsPage.routeName),
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
FlowySvgs.settings_s,
size: Size.square(20),
color: theme.textColorScheme.secondary,
);
},
),
const HSpace(24),

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,23 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
class ContinueWithEmail extends StatelessWidget {
const ContinueWithEmail({
super.key,
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return AFFilledTextButton.primary(
text: LocaleKeys.signIn_continueWithEmail.tr(),
size: AFButtonSize.l,
alignment: Alignment.center,
onTap: onTap,
);
}
}

View file

@ -0,0 +1,134 @@
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/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,
),
),
// Hide password sign in until we implement the reset password / forgot password
// VSpace(theme.spacing.l),
// ContinueWithPassword(
// onTap: () => _pushContinueWithPasswordPage(
// context,
// controller.text,
// ),
// ),
],
);
}
void _pushContinueWithMagicLinkOrPasscodePage(
BuildContext context,
String email,
) {
if (!isEmail(email)) {
showToastNotification(
context,
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);
return;
}
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) => BlocProvider.value(
value: signInBloc,
child: 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,188 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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;
ToastificationItem? toastificationItem;
@override
void dispose() {
passcodeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocListener<SignInBloc, SignInState>(
listener: (context, state) {
if (state.isSubmitting) {
_showLoadingDialog();
} else {
_dismissLoadingDialog();
}
},
child: 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: LocaleKeys.signIn_enterCodeManually.tr(),
onTap: () => setState(() => isEnteringPasscode = true),
size: AFButtonSize.l,
alignment: Alignment.center,
),
spacing,
];
}
return [
// Enter code manually
SizedBox(
height: 40,
child: AFTextField(
controller: passcodeController,
hintText: LocaleKeys.signIn_enterCode.tr(),
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 in',
onTap: () => widget.onEnterPasscode(passcodeController.text),
size: AFButtonSize.l,
alignment: Alignment.center,
),
spacing,
];
}
List<Widget> _buildBackToLogin() {
return [
AFGhostTextButton(
text: LocaleKeys.signIn_backToLogin.tr(),
size: AFButtonSize.s,
onTap: widget.backToLogin,
textColor: (context, isHovering, disabled) {
final theme = AppFlowyTheme.of(context);
if (isHovering) {
return theme.fillColorScheme.themeThickHover;
}
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(
LocaleKeys.signIn_checkYourEmail.tr(),
style: theme.textStyle.heading.h3(
color: theme.textColorScheme.primary,
),
),
spacing,
// description
Text(
LocaleKeys.signIn_temporaryVerificationSent.tr(),
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,
];
}
void _showLoadingDialog() {
_dismissLoadingDialog();
toastificationItem = showToastNotification(
context,
message: LocaleKeys.signIn_signingIn.tr(),
);
}
void _dismissLoadingDialog() {
final toastificationItem = this.toastificationItem;
if (toastificationItem != null) {
toastification.dismiss(toastificationItem);
}
}
}

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

@ -64,14 +64,17 @@ class _SignInWithMagicLinkButtonsState
void _sendMagicLink(BuildContext context, String email) {
if (!isEmail(email)) {
return showToastNotification(
showToastNotification(
context,
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);
return;
}
context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email));
context
.read<SignInBloc>()
.add(SignInEvent.signInWithMagicLink(email: email));
showConfirmDialog(
context: context,

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,41 +13,40 @@ 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(
children: [
TextSpan(
text: isLocalAuthEnabled
? '${LocaleKeys.web_signInLocalAgreement.tr()} '
: '${LocaleKeys.web_signInAgreement.tr()} ',
style: const TextStyle(color: Colors.grey, fontSize: 12),
? '${LocaleKeys.web_signInLocalAgreement.tr()} \n'
: '${LocaleKeys.web_signInAgreement.tr()} \n',
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'),
..onTap = () => afLaunchUrlString('https://appflowy.com/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'),
..onTap = () => afLaunchUrlString('https://appflowy.com/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,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class MobileLogoutButton extends StatelessWidget {
@ -18,50 +18,19 @@ class MobileLogoutButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
return GestureDetector(
return AFOutlinedIconTextButton.normal(
text: text,
onTap: onPressed,
child: Container(
height: 38,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(4),
),
border: Border.all(
color: textColor ?? style.colorScheme.outline,
width: 0.5,
),
),
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
SizedBox(
// The icon could be in different height as original aspect ratio, we use a fixed sizebox to wrap it to make sure they all occupy the same space.
width: 30,
height: 30,
child: Center(
child: SizedBox(
width: 24,
child: FlowySvg(
icon!,
blendMode: null,
),
),
),
),
const HSpace(8),
],
FlowyText(
text,
fontSize: 14.0,
fontWeight: FontWeight.w400,
color: textColor,
),
],
),
),
size: AFButtonSize.l,
iconBuilder: (context, isHovering, disabled) {
if (icon == null) {
return const SizedBox.shrink();
}
return FlowySvg(
icon!,
size: Size.square(18),
);
},
);
}
}

View file

@ -1,10 +1,7 @@
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';
enum ThirdPartySignInButtonType {
@ -102,118 +99,55 @@ 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;
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
return AnimatedGestureDetector(
scaleFactor: 1.0,
onTapUp: onPressed,
child: Container(
height: height,
decoration: BoxDecoration(
color: type.backgroundColor(context),
borderRadius: const BorderRadius.all(
Radius.circular(4),
),
border: Border.all(
color: style.colorScheme.outline,
width: 0.5,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (type != ThirdPartySignInButtonType.anonymous)
FlowySvg(
type.icon,
size: Size.square(fontSize),
blendMode: type.blendMode,
color: type.textColor(context),
),
const HSpace(8.0),
FlowyText(
type.labelText,
fontSize: fontSize,
color: type.textColor(context),
),
],
),
),
return AFOutlinedIconTextButton.normal(
text: type.labelText,
onTap: onTap,
size: AFButtonSize.l,
iconBuilder: (context, isHovering, disabled) {
return FlowySvg(
type.icon,
size: Size.square(16),
blendMode: type.blendMode,
);
},
);
}
}
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

@ -1,8 +1,7 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -40,7 +39,7 @@ class ThirdPartySignInButtons extends StatelessWidget {
void _signIn(BuildContext context, String provider) {
context.read<SignInBloc>().add(
SignInEvent.signedInWithOAuth(provider),
SignInEvent.signInWithOAuth(platform: provider),
);
}
}
@ -58,23 +57,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 +80,38 @@ 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) {
if (isHovering) {
return theme.fillColorScheme.themeThickHover;
}
return theme.textColorScheme.theme;
},
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
),
];
}
@ -153,14 +151,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,31 +170,33 @@ 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),
),
];
}
List<Widget> _buildCollapsedButtons() {
final theme = AppFlowyTheme.of(context);
return [
const VSpace(padding * 2),
GestureDetector(
AFGhostTextButton(
text: 'More options',
textColor: (context, isHovering, disabled) {
if (isHovering) {
return theme.fillColorScheme.themeThickHover;
}
return theme.textColorScheme.theme;
},
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
child: FlowyText(
LocaleKeys.signIn_continueAnotherWay.tr(),
color: Theme.of(context).colorScheme.onSurface,
decoration: TextDecoration.underline,
fontSize: 14,
),
),
];
}

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

@ -188,6 +188,9 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
UserEventGetWorkspaceUsage(payload).send().then((result) {
result.onSuccess(
(usage) {
if (isClosed) {
return;
}
add(SidebarPlanEvent.updateWorkspaceUsage(usage));
},
);

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

@ -363,7 +363,7 @@ class OkCancelButton extends StatelessWidget {
}
}
void showToastNotification(
ToastificationItem showToastNotification(
BuildContext context, {
String? message,
TextSpan? richMessage,
@ -376,7 +376,7 @@ void showToastNotification(
(message == null) != (richMessage == null),
"Exactly one of message or richMessage must be non-null.",
);
toastification.showCustom(
return toastification.showCustom(
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(milliseconds: 3000),
callbacks: callbacks ?? const ToastificationCallbacks(),

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

@ -206,7 +206,7 @@ class AppFlowyThemeBuilder {
quaternary: colorScheme.neutral.neutral100,
quaternaryHover: colorScheme.neutral.neutral200,
transparent: colorScheme.neutral.alphaWhite0,
primaryAlpha5: colorScheme.neutral.alphaGrey100005,
primaryAlpha5: colorScheme.neutral.alphaGrey10005,
primaryAlpha5Hover: colorScheme.neutral.alphaGrey100010,
primaryAlpha80: colorScheme.neutral.alphaGrey100080,
primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070,

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?",
@ -68,7 +69,16 @@
"logIn": "Log in",
"generalError": "Something went wrong. Please try again later",
"limitRateError": "For security reasons, you can only request a magic link every 60 seconds",
"magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes."
"magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes.",
"tokenHasExpiredOrInvalid": "The token has expired or is invalid. Please try again.",
"signingIn": "Signing in...",
"checkYourEmail": "Check your email",
"temporaryVerificationSent": "A temporary verification link has been sent. Please check your inbox at",
"continueToSignIn": "Continue to sign in",
"backToLogin": "Back to login",
"enterCode": "Enter code",
"enterCodeManually": "Enter code manually",
"continueWithEmail": "Continue with email"
},
"workspace": {
"chooseWorkspace": "Choose your workspace",

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=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
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=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
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=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
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=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
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=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
dependencies = [
"futures-channel",
"futures-util",
@ -1477,7 +1477,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
dependencies = [
"anyhow",
"bincode",
@ -1499,7 +1499,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
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=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
dependencies = [
"bincode",
"bytes",
@ -3424,7 +3424,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
dependencies = [
"anyhow",
"getrandom 0.2.10",
@ -3439,7 +3439,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
dependencies = [
"app-error",
"jsonwebtoken",
@ -4060,7 +4060,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
dependencies = [
"anyhow",
"bytes",
@ -6760,7 +6760,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
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 = "2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" }
[profile.dev]
opt-level = 0

View file

@ -14,7 +14,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};
@ -121,16 +121,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(
@ -148,6 +145,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,4 +1,5 @@
#![allow(unused_variables)]
use client_api::entity::GotrueTokenResponse;
use collab::core::origin::CollabOrigin;
use collab::preclude::Collab;
use collab_entity::CollabObject;
@ -98,7 +99,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"))
}
@ -110,6 +111,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

@ -41,14 +41,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
@ -319,6 +321,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,