sign_in_screen.dart 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import 'package:appflowy/core/config/kv.dart';
  2. import 'package:appflowy/core/config/kv_keys.dart';
  3. import 'package:appflowy/core/frameless_window.dart';
  4. import 'package:appflowy/startup/startup.dart';
  5. import 'package:appflowy/user/application/sign_in_bloc.dart';
  6. import 'package:appflowy/user/presentation/router.dart';
  7. import 'package:appflowy/user/presentation/widgets/background.dart';
  8. import 'package:easy_localization/easy_localization.dart';
  9. import 'package:flowy_infra/size.dart';
  10. import 'package:flowy_infra_ui/flowy_infra_ui.dart';
  11. import 'package:flowy_infra_ui/widget/rounded_button.dart';
  12. import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
  13. import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
  14. import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
  15. import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
  16. show UserProfilePB;
  17. import 'package:flutter/material.dart';
  18. import 'package:flutter_bloc/flutter_bloc.dart';
  19. import 'package:dartz/dartz.dart';
  20. import 'package:flowy_infra/image.dart';
  21. import 'package:appflowy/generated/locale_keys.g.dart';
  22. class SignInScreen extends StatelessWidget {
  23. const SignInScreen({
  24. super.key,
  25. required this.router,
  26. });
  27. final AuthRouter router;
  28. @override
  29. Widget build(BuildContext context) {
  30. return BlocProvider(
  31. create: (context) => getIt<SignInBloc>(),
  32. child: BlocConsumer<SignInBloc, SignInState>(
  33. listener: (context, state) {
  34. state.successOrFail.fold(
  35. () => null,
  36. (result) => _handleSuccessOrFail(result, context),
  37. );
  38. },
  39. builder: (_, __) => Scaffold(
  40. appBar: const PreferredSize(
  41. preferredSize: Size(double.infinity, 60),
  42. child: MoveWindowDetector(),
  43. ),
  44. body: SignInForm(router: router),
  45. ),
  46. ),
  47. );
  48. }
  49. void _handleSuccessOrFail(
  50. Either<UserProfilePB, FlowyError> result,
  51. BuildContext context,
  52. ) {
  53. result.fold(
  54. (user) => router.pushHomeScreen(context, user),
  55. (error) => showSnapBar(context, error.msg),
  56. );
  57. }
  58. }
  59. class SignInForm extends StatelessWidget {
  60. const SignInForm({
  61. super.key,
  62. required this.router,
  63. });
  64. final AuthRouter router;
  65. @override
  66. Widget build(BuildContext context) {
  67. return Align(
  68. alignment: Alignment.center,
  69. child: AuthFormContainer(
  70. children: [
  71. // Email.
  72. FlowyLogoTitle(
  73. title: LocaleKeys.signIn_loginTitle.tr(),
  74. logoSize: const Size(60, 60),
  75. ),
  76. const VSpace(30),
  77. // Email and password. don't support yet.
  78. /*
  79. ...[
  80. const EmailTextField(),
  81. const VSpace(5),
  82. const PasswordTextField(),
  83. const VSpace(20),
  84. const LoginButton(),
  85. const VSpace(10),
  86. const VSpace(10),
  87. SignUpPrompt(router: router),
  88. ],
  89. */
  90. const SignInAsGuestButton(),
  91. // third-party sign in.
  92. const VSpace(20),
  93. const OrContinueWith(),
  94. const VSpace(10),
  95. const ThirdPartySignInButtons(),
  96. const VSpace(20),
  97. // loading status
  98. if (context.read<SignInBloc>().state.isSubmitting) ...[
  99. const SizedBox(height: 8),
  100. const LinearProgressIndicator(value: null),
  101. const VSpace(20),
  102. ],
  103. ],
  104. ),
  105. );
  106. }
  107. }
  108. class SignUpPrompt extends StatelessWidget {
  109. const SignUpPrompt({
  110. Key? key,
  111. required this.router,
  112. }) : super(key: key);
  113. final AuthRouter router;
  114. @override
  115. Widget build(BuildContext context) {
  116. return Row(
  117. mainAxisAlignment: MainAxisAlignment.center,
  118. children: [
  119. FlowyText.medium(
  120. LocaleKeys.signIn_dontHaveAnAccount.tr(),
  121. color: Theme.of(context).hintColor,
  122. ),
  123. TextButton(
  124. style: TextButton.styleFrom(
  125. textStyle: Theme.of(context).textTheme.bodyMedium,
  126. ),
  127. onPressed: () => router.pushSignUpScreen(context),
  128. child: Text(
  129. LocaleKeys.signUp_buttonText.tr(),
  130. style: TextStyle(color: Theme.of(context).colorScheme.primary),
  131. ),
  132. ),
  133. ForgetPasswordButton(router: router),
  134. ],
  135. );
  136. }
  137. }
  138. class LoginButton extends StatelessWidget {
  139. const LoginButton({
  140. Key? key,
  141. }) : super(key: key);
  142. @override
  143. Widget build(BuildContext context) {
  144. return RoundedTextButton(
  145. title: LocaleKeys.signIn_loginButtonText.tr(),
  146. height: 48,
  147. borderRadius: Corners.s10Border,
  148. onPressed: () => context
  149. .read<SignInBloc>()
  150. .add(const SignInEvent.signedInWithUserEmailAndPassword()),
  151. );
  152. }
  153. }
  154. class SignInAsGuestButton extends StatelessWidget {
  155. const SignInAsGuestButton({
  156. Key? key,
  157. }) : super(key: key);
  158. @override
  159. Widget build(BuildContext context) {
  160. return RoundedTextButton(
  161. title: LocaleKeys.signIn_loginAsGuestButtonText.tr(),
  162. height: 48,
  163. borderRadius: Corners.s6Border,
  164. onPressed: () {
  165. getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
  166. context.read<SignInBloc>().add(const SignInEvent.signedInAsGuest());
  167. },
  168. );
  169. }
  170. }
  171. class ForgetPasswordButton extends StatelessWidget {
  172. const ForgetPasswordButton({
  173. Key? key,
  174. required this.router,
  175. }) : super(key: key);
  176. final AuthRouter router;
  177. @override
  178. Widget build(BuildContext context) {
  179. return TextButton(
  180. style: TextButton.styleFrom(
  181. textStyle: Theme.of(context).textTheme.bodyMedium,
  182. ),
  183. onPressed: () {
  184. throw UnimplementedError();
  185. },
  186. child: Text(
  187. LocaleKeys.signIn_forgotPassword.tr(),
  188. style: TextStyle(color: Theme.of(context).colorScheme.primary),
  189. ),
  190. );
  191. }
  192. }
  193. class PasswordTextField extends StatelessWidget {
  194. const PasswordTextField({
  195. Key? key,
  196. }) : super(key: key);
  197. @override
  198. Widget build(BuildContext context) {
  199. return BlocBuilder<SignInBloc, SignInState>(
  200. buildWhen: (previous, current) =>
  201. previous.passwordError != current.passwordError,
  202. builder: (context, state) {
  203. return RoundedInputField(
  204. obscureText: true,
  205. obscureIcon: svgWidget("home/hide"),
  206. obscureHideIcon: svgWidget("home/show"),
  207. hintText: LocaleKeys.signIn_passwordHint.tr(),
  208. errorText: context
  209. .read<SignInBloc>()
  210. .state
  211. .passwordError
  212. .fold(() => "", (error) => error),
  213. onChanged: (value) => context
  214. .read<SignInBloc>()
  215. .add(SignInEvent.passwordChanged(value)),
  216. );
  217. },
  218. );
  219. }
  220. }
  221. class EmailTextField extends StatelessWidget {
  222. const EmailTextField({
  223. Key? key,
  224. }) : super(key: key);
  225. @override
  226. Widget build(BuildContext context) {
  227. return BlocBuilder<SignInBloc, SignInState>(
  228. buildWhen: (previous, current) =>
  229. previous.emailError != current.emailError,
  230. builder: (context, state) {
  231. return RoundedInputField(
  232. hintText: LocaleKeys.signIn_emailHint.tr(),
  233. errorText: context
  234. .read<SignInBloc>()
  235. .state
  236. .emailError
  237. .fold(() => "", (error) => error),
  238. onChanged: (value) =>
  239. context.read<SignInBloc>().add(SignInEvent.emailChanged(value)),
  240. );
  241. },
  242. );
  243. }
  244. }
  245. class OrContinueWith extends StatelessWidget {
  246. const OrContinueWith({super.key});
  247. @override
  248. Widget build(BuildContext context) {
  249. return Row(
  250. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  251. children: const [
  252. Flexible(
  253. child: Divider(
  254. color: Colors.white,
  255. height: 10,
  256. ),
  257. ),
  258. FlowyText.regular(' Or continue with '),
  259. Flexible(
  260. child: Divider(
  261. color: Colors.white,
  262. height: 10,
  263. ),
  264. ),
  265. ],
  266. );
  267. }
  268. }
  269. class ThirdPartySignInButton extends StatelessWidget {
  270. const ThirdPartySignInButton({
  271. Key? key,
  272. required this.icon,
  273. required this.onPressed,
  274. }) : super(key: key);
  275. final String icon;
  276. final VoidCallback onPressed;
  277. @override
  278. Widget build(BuildContext context) {
  279. return FlowyIconButton(
  280. height: 48,
  281. width: 48,
  282. iconPadding: const EdgeInsets.all(8.0),
  283. radius: Corners.s10Border,
  284. onPressed: onPressed,
  285. icon: svgWidget(
  286. icon,
  287. ),
  288. );
  289. }
  290. }
  291. class ThirdPartySignInButtons extends StatelessWidget {
  292. const ThirdPartySignInButtons({
  293. super.key,
  294. });
  295. @override
  296. Widget build(BuildContext context) {
  297. return Row(
  298. mainAxisAlignment: MainAxisAlignment.center,
  299. children: [
  300. ThirdPartySignInButton(
  301. icon: 'login/google-mark',
  302. onPressed: () {
  303. getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
  304. context
  305. .read<SignInBloc>()
  306. .add(const SignInEvent.signedInWithOAuth('google'));
  307. },
  308. ),
  309. const SizedBox(width: 20),
  310. ThirdPartySignInButton(
  311. icon: 'login/github-mark',
  312. onPressed: () {
  313. getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
  314. context
  315. .read<SignInBloc>()
  316. .add(const SignInEvent.signedInWithOAuth('github'));
  317. },
  318. ),
  319. const SizedBox(width: 20),
  320. ThirdPartySignInButton(
  321. icon: 'login/discord-mark',
  322. onPressed: () {
  323. getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
  324. context
  325. .read<SignInBloc>()
  326. .add(const SignInEvent.signedInWithOAuth('discord'));
  327. },
  328. ),
  329. ],
  330. );
  331. }
  332. }