feat: enable apple sign in on desktop version

This commit is contained in:
Lucas.Xu 2024-08-30 14:49:47 +08:00
parent d89804f3e4
commit 39d6e3f1fe
4 changed files with 274 additions and 202 deletions

View File

@ -12,7 +12,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class DesktopSignInScreen extends StatelessWidget { class DesktopSignInScreen extends StatelessWidget {
const DesktopSignInScreen({super.key}); const DesktopSignInScreen({
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -20,38 +22,32 @@ class DesktopSignInScreen extends StatelessWidget {
return BlocBuilder<SignInBloc, SignInState>( return BlocBuilder<SignInBloc, SignInState>(
builder: (context, state) { builder: (context, state) {
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: _buildAppBar(),
preferredSize:
Size.fromHeight(PlatformExtension.isWindows ? 40 : 60),
child: PlatformExtension.isWindows
? const WindowTitleBar()
: const MoveWindowDetector(),
),
body: Center( body: Center(
child: AuthFormContainer( child: AuthFormContainer(
children: [ children: [
const Spacer(),
const VSpace(20), const VSpace(20),
// logo and title
FlowyLogoTitle( FlowyLogoTitle(
title: LocaleKeys.welcomeText.tr(), title: LocaleKeys.welcomeText.tr(),
logoSize: const Size(60, 60), logoSize: const Size(60, 60),
), ),
const VSpace(20), const VSpace(20),
// magic link sign in
const SignInWithMagicLinkButtons(), const SignInWithMagicLinkButtons(),
// third-party sign in.
const VSpace(20), const VSpace(20),
// third-party sign in.
if (isAuthEnabled) ...[ if (isAuthEnabled) ...[
const _OrDivider(), const _OrDivider(),
const VSpace(20), const VSpace(20),
const ThirdPartySignInButtons(), const ThirdPartySignInButtons(),
const VSpace(20),
], ],
const VSpace(20),
// anonymous sign in
const SignInAnonymousButtonV2(),
const VSpace(16),
// sign in agreement // sign in agreement
const SignInAgreement(), const SignInAgreement(),
@ -64,6 +60,12 @@ class DesktopSignInScreen extends StatelessWidget {
) )
: const VSpace(indicatorMinHeight), : const VSpace(indicatorMinHeight),
const VSpace(20), 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 { class _OrDivider extends StatelessWidget {

View File

@ -1,7 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/animated_gesture.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:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -12,6 +14,21 @@ enum ThirdPartySignInButtonType {
discord, discord,
anonymous; 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 { FlowySvgData get icon {
switch (this) { switch (this) {
case ThirdPartySignInButtonType.apple: 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<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,
),
);
}
}

View File

@ -1,22 +1,21 @@
import 'dart:io'; import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'third_party_sign_in_button.dart'; import 'third_party_sign_in_button.dart';
typedef _SignInCallback = void Function(ThirdPartySignInButtonType signInType);
@visibleForTesting @visibleForTesting
const Key signInWithGoogleButtonKey = Key('signInWithGoogleButton'); const Key signInWithGoogleButtonKey = Key('signInWithGoogleButton');
class ThirdPartySignInButtons extends StatefulWidget { class ThirdPartySignInButtons extends StatelessWidget {
/// Used in DesktopSignInScreen, MobileSignInScreen and SettingThirdPartyLogin /// Used in DesktopSignInScreen, MobileSignInScreen and SettingThirdPartyLogin
const ThirdPartySignInButtons({ const ThirdPartySignInButtons({
super.key, super.key,
@ -26,195 +25,175 @@ class ThirdPartySignInButtons extends StatefulWidget {
final bool expanded; final bool expanded;
@override @override
State<ThirdPartySignInButtons> createState() => Widget build(BuildContext context) {
_ThirdPartySignInButtonsState(); 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<SignInBloc>().add(
SignInEvent.signedInWithOAuth(provider),
);
}
} }
class _ThirdPartySignInButtonsState extends State<ThirdPartySignInButtons> { class _DesktopThirdPartySignIn extends StatefulWidget {
bool expanded = false; 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<Widget> _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<Widget> _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 @override
void initState() { void initState() {
super.initState(); super.initState();
expanded = widget.expanded; isExpanded = widget.isExpanded;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (PlatformExtension.isDesktopOrWeb) { return Column(
const padding = 16.0; children: [
return Column( // only display apple sign in button on iOS
children: [ if (Platform.isIOS) ...[
_DesktopSignInButton( MobileThirdPartySignInButton(
key: signInWithGoogleButtonKey, type: ThirdPartySignInButtonType.apple,
type: ThirdPartySignInButtonType.google, onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
onPressed: () {
_signInWithGoogle(context);
},
), ),
const VSpace(padding), const VSpace(padding),
_DesktopSignInButton(
type: ThirdPartySignInButtonType.github,
onPressed: () {
_signInWithGithub(context);
},
),
const VSpace(padding),
_DesktopSignInButton(
type: ThirdPartySignInButtonType.discord,
onPressed: () {
_signInWithDiscord(context);
},
),
], ],
); MobileThirdPartySignInButton(
} else { type: ThirdPartySignInButtonType.google,
const padding = 8.0; onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google),
return BlocBuilder<SignInBloc, SignInState>(
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<SignInBloc>().add(
const SignInEvent.signedInWithOAuth('apple'),
);
}
void _signInWithGoogle(BuildContext context) {
context.read<SignInBloc>().add(
const SignInEvent.signedInWithOAuth('google'),
);
}
void _signInWithGithub(BuildContext context) {
context
.read<SignInBloc>()
.add(const SignInEvent.signedInWithOAuth('github'));
}
void _signInWithDiscord(BuildContext context) {
context
.read<SignInBloc>()
.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,
),
),
), ),
label: Container( ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(),
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,
),
); );
} }
List<Widget> _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<Widget> _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,
),
),
];
}
} }

View File

@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AuthFormContainer extends StatelessWidget { class AuthFormContainer extends StatelessWidget {
const AuthFormContainer({super.key, required this.children}); const AuthFormContainer({
super.key,
required this.children,
});
final List<Widget> children; final List<Widget> children;
@ -11,14 +14,10 @@ class AuthFormContainer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: width, width: width,
child: ScrollConfiguration( child: Column(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), mainAxisSize: MainAxisSize.min,
child: SingleChildScrollView( mainAxisAlignment: MainAxisAlignment.center,
child: Column( children: children,
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
),
), ),
); );
} }