mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 06:37:14 -04:00
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:
parent
8aa6dd8821
commit
170e09bae9
51 changed files with 1228 additions and 453 deletions
|
@ -15,7 +15,6 @@ void main() {
|
|||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
|
||||
await tester.tapContinousAnotherWay();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -34,7 +34,7 @@ class SignInAnonymousButtonV3 extends StatelessWidget {
|
|||
? () {
|
||||
context
|
||||
.read<SignInBloc>()
|
||||
.add(const SignInEvent.signedInAsGuest());
|
||||
.add(const SignInEvent.signInAsGuest());
|
||||
}
|
||||
: () {
|
||||
final bloc = context.read<AnonUserBloc>();
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
library;
|
||||
|
||||
export 'src/component/component.dart';
|
||||
export 'src/theme/theme.dart';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
17
frontend/rust-lib/.vscode/launch.json
vendored
Normal 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",
|
||||
},
|
||||
]
|
||||
}
|
24
frontend/rust-lib/Cargo.lock
generated
24
frontend/rust-lib/Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(¶ms.email, ¶ms.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(¶ms.email, ¶ms.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>,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue