sign_in_screen.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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. final isSubmitting = context.read<SignInBloc>().state.isSubmitting;
  68. const indicatorMinHeight = 4.0;
  69. return Align(
  70. alignment: Alignment.center,
  71. child: AuthFormContainer(
  72. children: [
  73. // Email.
  74. FlowyLogoTitle(
  75. title: LocaleKeys.signIn_loginTitle.tr(),
  76. logoSize: const Size(60, 60),
  77. ),
  78. const VSpace(30),
  79. // Email and password. don't support yet.
  80. /*
  81. ...[
  82. const EmailTextField(),
  83. const VSpace(5),
  84. const PasswordTextField(),
  85. const VSpace(20),
  86. const LoginButton(),
  87. const VSpace(10),
  88. const VSpace(10),
  89. SignUpPrompt(router: router),
  90. ],
  91. */
  92. const SignInAsGuestButton(),
  93. // third-party sign in.
  94. const VSpace(20),
  95. const OrContinueWith(),
  96. const VSpace(10),
  97. const ThirdPartySignInButtons(),
  98. const VSpace(20),
  99. // loading status
  100. ...isSubmitting
  101. ? [
  102. const VSpace(indicatorMinHeight),
  103. const LinearProgressIndicator(
  104. value: null,
  105. minHeight: indicatorMinHeight,
  106. ),
  107. ]
  108. : [
  109. const VSpace(indicatorMinHeight * 2.0)
  110. ], // add the same space when there's no loading status.
  111. const VSpace(20)
  112. ],
  113. ),
  114. );
  115. }
  116. }
  117. class SignUpPrompt extends StatelessWidget {
  118. const SignUpPrompt({
  119. Key? key,
  120. required this.router,
  121. }) : super(key: key);
  122. final AuthRouter router;
  123. @override
  124. Widget build(BuildContext context) {
  125. return Row(
  126. mainAxisAlignment: MainAxisAlignment.center,
  127. children: [
  128. FlowyText.medium(
  129. LocaleKeys.signIn_dontHaveAnAccount.tr(),
  130. color: Theme.of(context).hintColor,
  131. ),
  132. TextButton(
  133. style: TextButton.styleFrom(
  134. textStyle: Theme.of(context).textTheme.bodyMedium,
  135. ),
  136. onPressed: () => router.pushSignUpScreen(context),
  137. child: Text(
  138. LocaleKeys.signUp_buttonText.tr(),
  139. style: TextStyle(color: Theme.of(context).colorScheme.primary),
  140. ),
  141. ),
  142. ForgetPasswordButton(router: router),
  143. ],
  144. );
  145. }
  146. }
  147. class LoginButton extends StatelessWidget {
  148. const LoginButton({
  149. Key? key,
  150. }) : super(key: key);
  151. @override
  152. Widget build(BuildContext context) {
  153. return RoundedTextButton(
  154. title: LocaleKeys.signIn_loginButtonText.tr(),
  155. height: 48,
  156. borderRadius: Corners.s10Border,
  157. onPressed: () => context
  158. .read<SignInBloc>()
  159. .add(const SignInEvent.signedInWithUserEmailAndPassword()),
  160. );
  161. }
  162. }
  163. class SignInAsGuestButton extends StatelessWidget {
  164. const SignInAsGuestButton({
  165. Key? key,
  166. }) : super(key: key);
  167. @override
  168. Widget build(BuildContext context) {
  169. return RoundedTextButton(
  170. title: LocaleKeys.signIn_loginAsGuestButtonText.tr(),
  171. height: 48,
  172. borderRadius: Corners.s6Border,
  173. onPressed: () {
  174. getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
  175. context.read<SignInBloc>().add(const SignInEvent.signedInAsGuest());
  176. },
  177. );
  178. }
  179. }
  180. class ForgetPasswordButton extends StatelessWidget {
  181. const ForgetPasswordButton({
  182. Key? key,
  183. required this.router,
  184. }) : super(key: key);
  185. final AuthRouter router;
  186. @override
  187. Widget build(BuildContext context) {
  188. return TextButton(
  189. style: TextButton.styleFrom(
  190. textStyle: Theme.of(context).textTheme.bodyMedium,
  191. ),
  192. onPressed: () {
  193. throw UnimplementedError();
  194. },
  195. child: Text(
  196. LocaleKeys.signIn_forgotPassword.tr(),
  197. style: TextStyle(color: Theme.of(context).colorScheme.primary),
  198. ),
  199. );
  200. }
  201. }
  202. class PasswordTextField extends StatelessWidget {
  203. const PasswordTextField({
  204. Key? key,
  205. }) : super(key: key);
  206. @override
  207. Widget build(BuildContext context) {
  208. return BlocBuilder<SignInBloc, SignInState>(
  209. buildWhen: (previous, current) =>
  210. previous.passwordError != current.passwordError,
  211. builder: (context, state) {
  212. return RoundedInputField(
  213. obscureText: true,
  214. obscureIcon: svgWidget("home/hide"),
  215. obscureHideIcon: svgWidget("home/show"),
  216. hintText: LocaleKeys.signIn_passwordHint.tr(),
  217. errorText: context
  218. .read<SignInBloc>()
  219. .state
  220. .passwordError
  221. .fold(() => "", (error) => error),
  222. onChanged: (value) => context
  223. .read<SignInBloc>()
  224. .add(SignInEvent.passwordChanged(value)),
  225. );
  226. },
  227. );
  228. }
  229. }
  230. class EmailTextField extends StatelessWidget {
  231. const EmailTextField({
  232. Key? key,
  233. }) : super(key: key);
  234. @override
  235. Widget build(BuildContext context) {
  236. return BlocBuilder<SignInBloc, SignInState>(
  237. buildWhen: (previous, current) =>
  238. previous.emailError != current.emailError,
  239. builder: (context, state) {
  240. return RoundedInputField(
  241. hintText: LocaleKeys.signIn_emailHint.tr(),
  242. errorText: context
  243. .read<SignInBloc>()
  244. .state
  245. .emailError
  246. .fold(() => "", (error) => error),
  247. onChanged: (value) =>
  248. context.read<SignInBloc>().add(SignInEvent.emailChanged(value)),
  249. );
  250. },
  251. );
  252. }
  253. }
  254. class OrContinueWith extends StatelessWidget {
  255. const OrContinueWith({super.key});
  256. @override
  257. Widget build(BuildContext context) {
  258. return const Row(
  259. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  260. children: [
  261. Flexible(
  262. child: Divider(
  263. color: Colors.white,
  264. height: 10,
  265. ),
  266. ),
  267. FlowyText.regular(' Or continue with '),
  268. Flexible(
  269. child: Divider(
  270. color: Colors.white,
  271. height: 10,
  272. ),
  273. ),
  274. ],
  275. );
  276. }
  277. }
  278. class ThirdPartySignInButton extends StatelessWidget {
  279. const ThirdPartySignInButton({
  280. Key? key,
  281. required this.icon,
  282. required this.onPressed,
  283. }) : super(key: key);
  284. final String icon;
  285. final VoidCallback onPressed;
  286. @override
  287. Widget build(BuildContext context) {
  288. return FlowyIconButton(
  289. height: 48,
  290. width: 48,
  291. iconPadding: const EdgeInsets.all(8.0),
  292. radius: Corners.s10Border,
  293. onPressed: onPressed,
  294. icon: svgWidget(
  295. icon,
  296. ),
  297. );
  298. }
  299. }
  300. class ThirdPartySignInButtons extends StatelessWidget {
  301. final MainAxisAlignment mainAxisAlignment;
  302. const ThirdPartySignInButtons({
  303. this.mainAxisAlignment = MainAxisAlignment.center,
  304. super.key,
  305. });
  306. @override
  307. Widget build(BuildContext context) {
  308. return Row(
  309. mainAxisAlignment: mainAxisAlignment,
  310. children: [
  311. ThirdPartySignInButton(
  312. icon: 'login/google-mark',
  313. onPressed: () {
  314. getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
  315. context.read<SignInBloc>().add(
  316. const SignInEvent.signedInWithOAuth('google'),
  317. );
  318. },
  319. ),
  320. // const SizedBox(width: 20),
  321. // ThirdPartySignInButton(
  322. // icon: 'login/github-mark',
  323. // onPressed: () {
  324. // getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
  325. // context
  326. // .read<SignInBloc>()
  327. // .add(const SignInEvent.signedInWithOAuth('github'));
  328. // },
  329. // ),
  330. // const SizedBox(width: 20),
  331. // ThirdPartySignInButton(
  332. // icon: 'login/discord-mark',
  333. // onPressed: () {
  334. // getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
  335. // context
  336. // .read<SignInBloc>()
  337. // .add(const SignInEvent.signedInWithOAuth('discord'));
  338. // },
  339. // ),
  340. ],
  341. );
  342. }
  343. }