From 39d6e3f1feae9d56ae10cb482e6330cbee629961 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 30 Aug 2024 14:49:47 +0800 Subject: [PATCH] feat: enable apple sign in on desktop version --- .../desktop_sign_in_screen.dart | 41 ++- .../widgets/third_party_sign_in_button.dart | 83 +++++ .../widgets/third_party_sign_in_buttons.dart | 335 ++++++++---------- .../widgets/auth_form_container.dart | 17 +- 4 files changed, 274 insertions(+), 202 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 7c023befea..0d39757201 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -12,7 +12,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DesktopSignInScreen extends StatelessWidget { - const DesktopSignInScreen({super.key}); + const DesktopSignInScreen({ + super.key, + }); @override Widget build(BuildContext context) { @@ -20,38 +22,32 @@ class DesktopSignInScreen extends StatelessWidget { return BlocBuilder( builder: (context, state) { return Scaffold( - appBar: PreferredSize( - preferredSize: - Size.fromHeight(PlatformExtension.isWindows ? 40 : 60), - child: PlatformExtension.isWindows - ? const WindowTitleBar() - : const MoveWindowDetector(), - ), + appBar: _buildAppBar(), body: Center( child: AuthFormContainer( children: [ + const Spacer(), + const VSpace(20), + + // logo and title FlowyLogoTitle( title: LocaleKeys.welcomeText.tr(), logoSize: const Size(60, 60), ), const VSpace(20), + // magic link sign in const SignInWithMagicLinkButtons(), - - // third-party sign in. const VSpace(20), + // third-party sign in. if (isAuthEnabled) ...[ const _OrDivider(), const VSpace(20), const ThirdPartySignInButtons(), + const VSpace(20), ], - const VSpace(20), - - // anonymous sign in - const SignInAnonymousButtonV2(), - const VSpace(16), // sign in agreement const SignInAgreement(), @@ -64,6 +60,12 @@ class DesktopSignInScreen extends StatelessWidget { ) : const VSpace(indicatorMinHeight), const VSpace(20), + + const Spacer(), + + // anonymous sign in + const SignInAnonymousButtonV2(), + const VSpace(16), ], ), ), @@ -71,6 +73,15 @@ class DesktopSignInScreen extends StatelessWidget { }, ); } + + PreferredSize _buildAppBar() { + return PreferredSize( + preferredSize: Size.fromHeight(PlatformExtension.isWindows ? 40 : 60), + child: PlatformExtension.isWindows + ? const WindowTitleBar() + : const MoveWindowDetector(), + ); + } } class _OrDivider extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart index c51634bcf5..36d83ea3bc 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart @@ -1,7 +1,9 @@ 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: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'; @@ -12,6 +14,21 @@ enum ThirdPartySignInButtonType { discord, anonymous; + String get provider { + switch (this) { + case ThirdPartySignInButtonType.apple: + return 'apple'; + case ThirdPartySignInButtonType.google: + return 'google'; + case ThirdPartySignInButtonType.github: + return 'github'; + case ThirdPartySignInButtonType.discord: + return 'discord'; + case ThirdPartySignInButtonType.anonymous: + throw UnsupportedError('Anonymous session does not have a provider'); + } + } + FlowySvgData get icon { switch (this) { case ThirdPartySignInButtonType.apple: @@ -135,3 +152,69 @@ class MobileThirdPartySignInButton extends StatelessWidget { ); } } + + +class DesktopSignInButton extends StatelessWidget { + const DesktopSignInButton({ + super.key, + required this.type, + required this.onPressed, + }); + + final ThirdPartySignInButtonType type; + final VoidCallback onPressed; + + @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( + (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, + ), + ); + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart index 6972eb2105..bd84dfbc1c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart @@ -1,22 +1,21 @@ import 'dart:io'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/presentation.dart'; import 'package:appflowy_editor/appflowy_editor.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 'third_party_sign_in_button.dart'; +typedef _SignInCallback = void Function(ThirdPartySignInButtonType signInType); + @visibleForTesting const Key signInWithGoogleButtonKey = Key('signInWithGoogleButton'); -class ThirdPartySignInButtons extends StatefulWidget { +class ThirdPartySignInButtons extends StatelessWidget { /// Used in DesktopSignInScreen, MobileSignInScreen and SettingThirdPartyLogin const ThirdPartySignInButtons({ super.key, @@ -26,195 +25,175 @@ class ThirdPartySignInButtons extends StatefulWidget { final bool expanded; @override - State createState() => - _ThirdPartySignInButtonsState(); + Widget build(BuildContext context) { + if (PlatformExtension.isDesktopOrWeb) { + return _DesktopThirdPartySignIn( + onSignIn: (type) => _signIn(context, type.provider), + ); + } else { + return _MobileThirdPartySignIn( + isExpanded: expanded, + onSignIn: (type) => _signIn(context, type.provider), + ); + } + } + + void _signIn(BuildContext context, String provider) { + context.read().add( + SignInEvent.signedInWithOAuth(provider), + ); + } } -class _ThirdPartySignInButtonsState extends State { - bool expanded = false; +class _DesktopThirdPartySignIn extends StatefulWidget { + const _DesktopThirdPartySignIn({ + required this.onSignIn, + }); + + final _SignInCallback onSignIn; + + @override + State<_DesktopThirdPartySignIn> createState() => + _DesktopThirdPartySignInState(); +} + +class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> { + static const padding = 12.0; + + bool isExpanded = false; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + DesktopSignInButton( + key: signInWithGoogleButtonKey, + type: ThirdPartySignInButtonType.google, + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google), + ), + const VSpace(padding), + DesktopSignInButton( + type: ThirdPartySignInButtonType.apple, + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple), + ), + ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(), + ], + ); + } + + List _buildExpandedButtons() { + return [ + const VSpace(padding * 1.5), + DesktopSignInButton( + type: ThirdPartySignInButtonType.github, + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github), + ), + const VSpace(padding), + DesktopSignInButton( + type: ThirdPartySignInButtonType.discord, + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord), + ), + ]; + } + + List _buildCollapsedButtons() { + return [ + const VSpace(padding), + GestureDetector( + onTap: () { + setState(() { + isExpanded = !isExpanded; + }); + }, + child: FlowyText( + LocaleKeys.signIn_continueAnotherWay.tr(), + color: Theme.of(context).colorScheme.onSurface, + decoration: TextDecoration.underline, + fontSize: 14, + ), + ), + ]; + } +} + +class _MobileThirdPartySignIn extends StatefulWidget { + const _MobileThirdPartySignIn({ + required this.isExpanded, + required this.onSignIn, + }); + + final bool isExpanded; + final _SignInCallback onSignIn; + + @override + State<_MobileThirdPartySignIn> createState() => + _MobileThirdPartySignInState(); +} + +class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> { + static const padding = 8.0; + + bool isExpanded = false; @override void initState() { super.initState(); - expanded = widget.expanded; + isExpanded = widget.isExpanded; } @override Widget build(BuildContext context) { - if (PlatformExtension.isDesktopOrWeb) { - const padding = 16.0; - return Column( - children: [ - _DesktopSignInButton( - key: signInWithGoogleButtonKey, - type: ThirdPartySignInButtonType.google, - onPressed: () { - _signInWithGoogle(context); - }, + return Column( + children: [ + // only display apple sign in button on iOS + if (Platform.isIOS) ...[ + MobileThirdPartySignInButton( + type: ThirdPartySignInButtonType.apple, + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple), ), const VSpace(padding), - _DesktopSignInButton( - type: ThirdPartySignInButtonType.github, - onPressed: () { - _signInWithGithub(context); - }, - ), - const VSpace(padding), - _DesktopSignInButton( - type: ThirdPartySignInButtonType.discord, - onPressed: () { - _signInWithDiscord(context); - }, - ), ], - ); - } else { - const padding = 8.0; - return BlocBuilder( - builder: (context, state) { - return Column( - children: [ - if (Platform.isIOS) ...[ - MobileThirdPartySignInButton( - type: ThirdPartySignInButtonType.apple, - onPressed: () { - _signInWithApple(context); - }, - ), - const VSpace(padding), - ], - MobileThirdPartySignInButton( - type: ThirdPartySignInButtonType.google, - onPressed: () { - _signInWithGoogle(context); - }, - ), - if (expanded) ...[ - const VSpace(padding), - MobileThirdPartySignInButton( - type: ThirdPartySignInButtonType.github, - onPressed: () { - _signInWithGithub(context); - }, - ), - const VSpace(padding), - MobileThirdPartySignInButton( - type: ThirdPartySignInButtonType.discord, - onPressed: () { - _signInWithDiscord(context); - }, - ), - ], - if (!expanded) ...[ - const VSpace(padding * 2), - GestureDetector( - onTap: () { - setState(() { - expanded = !expanded; - }); - }, - child: FlowyText( - LocaleKeys.signIn_continueAnotherWay.tr(), - color: Theme.of(context).colorScheme.onSurface, - decoration: TextDecoration.underline, - fontSize: 14, - ), - ), - ], - ], - ); - }, - ); - } - } - - void _signInWithApple(BuildContext context) { - context.read().add( - const SignInEvent.signedInWithOAuth('apple'), - ); - } - - void _signInWithGoogle(BuildContext context) { - context.read().add( - const SignInEvent.signedInWithOAuth('google'), - ); - } - - void _signInWithGithub(BuildContext context) { - context - .read() - .add(const SignInEvent.signedInWithOAuth('github')); - } - - void _signInWithDiscord(BuildContext context) { - context - .read() - .add(const SignInEvent.signedInWithOAuth('discord')); - } -} - -class _DesktopSignInButton extends StatelessWidget { - const _DesktopSignInButton({ - super.key, - required this.type, - required this.onPressed, - }); - - final ThirdPartySignInButtonType type; - final VoidCallback onPressed; - - @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, - ), - ), + MobileThirdPartySignInButton( + type: ThirdPartySignInButtonType.google, + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google), ), - label: Container( - padding: const EdgeInsets.only(left: 8), - alignment: Alignment.centerLeft, - child: FlowyText( - type.labelText, - fontSize: 14, - ), - ), - style: ButtonStyle( - overlayColor: WidgetStateProperty.resolveWith( - (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, - ), + ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(), + ], ); } + + List _buildExpandedButtons() { + return [ + const VSpace(padding), + MobileThirdPartySignInButton( + type: ThirdPartySignInButtonType.github, + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github), + ), + const VSpace(padding), + MobileThirdPartySignInButton( + type: ThirdPartySignInButtonType.discord, + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord), + ), + ]; + } + + List _buildCollapsedButtons() { + return [ + const VSpace(padding * 2), + GestureDetector( + onTap: () { + setState(() { + isExpanded = !isExpanded; + }); + }, + child: FlowyText( + LocaleKeys.signIn_continueAnotherWay.tr(), + color: Theme.of(context).colorScheme.onSurface, + decoration: TextDecoration.underline, + fontSize: 14, + ), + ), + ]; + } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart index 9927ee2457..8ce09a5b7f 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; class AuthFormContainer extends StatelessWidget { - const AuthFormContainer({super.key, required this.children}); + const AuthFormContainer({ + super.key, + required this.children, + }); final List children; @@ -11,14 +14,10 @@ class AuthFormContainer extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( width: width, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: children, - ), - ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: children, ), ); }