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';
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<SignInBloc, SignInState>(
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),
// 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 {

View File

@ -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<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 '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,92 +25,84 @@ class ThirdPartySignInButtons extends StatefulWidget {
final bool expanded;
@override
State<ThirdPartySignInButtons> 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),
);
}
}
class _ThirdPartySignInButtonsState extends State<ThirdPartySignInButtons> {
bool expanded = false;
void _signIn(BuildContext context, String provider) {
context.read<SignInBloc>().add(
SignInEvent.signedInWithOAuth(provider),
);
}
}
class _DesktopThirdPartySignIn extends StatefulWidget {
const _DesktopThirdPartySignIn({
required this.onSignIn,
});
final _SignInCallback onSignIn;
@override
void initState() {
super.initState();
expanded = widget.expanded;
State<_DesktopThirdPartySignIn> createState() =>
_DesktopThirdPartySignInState();
}
class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> {
static const padding = 12.0;
bool isExpanded = false;
@override
Widget build(BuildContext context) {
if (PlatformExtension.isDesktopOrWeb) {
const padding = 16.0;
return Column(
children: [
_DesktopSignInButton(
DesktopSignInButton(
key: signInWithGoogleButtonKey,
type: ThirdPartySignInButtonType.google,
onPressed: () {
_signInWithGoogle(context);
},
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google),
),
const VSpace(padding),
_DesktopSignInButton(
type: ThirdPartySignInButtonType.github,
onPressed: () {
_signInWithGithub(context);
},
),
const VSpace(padding),
_DesktopSignInButton(
type: ThirdPartySignInButtonType.discord,
onPressed: () {
_signInWithDiscord(context);
},
DesktopSignInButton(
type: ThirdPartySignInButtonType.apple,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple),
),
...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(),
],
);
} else {
const padding = 8.0;
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(
}
List<Widget> _buildExpandedButtons() {
return [
const VSpace(padding * 1.5),
DesktopSignInButton(
type: ThirdPartySignInButtonType.github,
onPressed: () {
_signInWithGithub(context);
},
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github),
),
const VSpace(padding),
MobileThirdPartySignInButton(
DesktopSignInButton(
type: ThirdPartySignInButtonType.discord,
onPressed: () {
_signInWithDiscord(context);
},
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord),
),
],
if (!expanded) ...[
const VSpace(padding * 2),
];
}
List<Widget> _buildCollapsedButtons() {
return [
const VSpace(padding),
GestureDetector(
onTap: () {
setState(() {
expanded = !expanded;
isExpanded = !isExpanded;
});
},
child: FlowyText(
@ -121,100 +112,88 @@ class _ThirdPartySignInButtonsState extends State<ThirdPartySignInButtons> {
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,
class _MobileThirdPartySignIn extends StatefulWidget {
const _MobileThirdPartySignIn({
required this.isExpanded,
required this.onSignIn,
});
final ThirdPartySignInButtonType type;
final VoidCallback onPressed;
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();
isExpanded = widget.isExpanded;
}
@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,
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),
],
MobileThirdPartySignInButton(
type: ThirdPartySignInButtonType.google,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google),
),
...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(),
],
);
}
List<Widget> _buildExpandedButtons() {
return [
const VSpace(padding),
MobileThirdPartySignInButton(
type: ThirdPartySignInButtonType.github,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github),
),
label: Container(
padding: const EdgeInsets.only(left: 8),
alignment: Alignment.centerLeft,
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(
type.labelText,
LocaleKeys.signIn_continueAnotherWay.tr(),
color: Theme.of(context).colorScheme.onSurface,
decoration: TextDecoration.underline,
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,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<Widget> children;
@ -11,15 +14,11 @@ 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(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
),
),
);
}
}