sign_in_screen.dart 12 KB

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