sign_in_screen.dart 13 KB

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