sign_in_screen.dart 12 KB

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