sign_in_screen.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  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 BlocBuilder<SignInBloc, SignInState>(
  208. builder: (context, signInState) {
  209. return BlocProvider(
  210. create: (context) => HistoricalUserBloc()
  211. ..add(
  212. const HistoricalUserEvent.initial(),
  213. ),
  214. child: BlocListener<HistoricalUserBloc, HistoricalUserState>(
  215. listenWhen: (previous, current) =>
  216. previous.openedHistoricalUser != current.openedHistoricalUser,
  217. listener: (context, state) async {
  218. await runAppFlowy();
  219. },
  220. child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
  221. builder: (context, state) {
  222. final text = state.historicalUsers.isEmpty
  223. ? FlowyText.medium(
  224. LocaleKeys.signIn_loginAsGuestButtonText.tr(),
  225. textAlign: TextAlign.center,
  226. )
  227. : FlowyText.medium(
  228. LocaleKeys.signIn_continueAnonymousUser.tr(),
  229. textAlign: TextAlign.center,
  230. );
  231. final onTap = state.historicalUsers.isEmpty
  232. ? () {
  233. getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
  234. context
  235. .read<SignInBloc>()
  236. .add(const SignInEvent.signedInAsGuest());
  237. }
  238. : () {
  239. final bloc = context.read<HistoricalUserBloc>();
  240. final user = bloc.state.historicalUsers.first;
  241. bloc.add(HistoricalUserEvent.openHistoricalUser(user));
  242. };
  243. return SizedBox(
  244. height: 48,
  245. child: FlowyButton(
  246. isSelected: true,
  247. disable: signInState.isSubmitting,
  248. text: text,
  249. radius: Corners.s6Border,
  250. onTap: onTap,
  251. ),
  252. );
  253. },
  254. ),
  255. ),
  256. );
  257. },
  258. );
  259. }
  260. }
  261. class ForgetPasswordButton extends StatelessWidget {
  262. const ForgetPasswordButton({
  263. Key? key,
  264. required this.router,
  265. }) : super(key: key);
  266. final AuthRouter router;
  267. @override
  268. Widget build(BuildContext context) {
  269. return TextButton(
  270. style: TextButton.styleFrom(
  271. textStyle: Theme.of(context).textTheme.bodyMedium,
  272. ),
  273. onPressed: () {
  274. throw UnimplementedError();
  275. },
  276. child: Text(
  277. LocaleKeys.signIn_forgotPassword.tr(),
  278. style: TextStyle(color: Theme.of(context).colorScheme.primary),
  279. ),
  280. );
  281. }
  282. }
  283. class PasswordTextField extends StatelessWidget {
  284. const PasswordTextField({
  285. Key? key,
  286. }) : super(key: key);
  287. @override
  288. Widget build(BuildContext context) {
  289. return BlocBuilder<SignInBloc, SignInState>(
  290. buildWhen: (previous, current) =>
  291. previous.passwordError != current.passwordError,
  292. builder: (context, state) {
  293. return RoundedInputField(
  294. obscureText: true,
  295. obscureIcon: const FlowySvg(FlowySvgs.hide_m),
  296. obscureHideIcon: const FlowySvg(FlowySvgs.show_m),
  297. hintText: LocaleKeys.signIn_passwordHint.tr(),
  298. errorText: context
  299. .read<SignInBloc>()
  300. .state
  301. .passwordError
  302. .fold(() => "", (error) => error),
  303. onChanged: (value) => context
  304. .read<SignInBloc>()
  305. .add(SignInEvent.passwordChanged(value)),
  306. );
  307. },
  308. );
  309. }
  310. }
  311. class EmailTextField extends StatelessWidget {
  312. const EmailTextField({
  313. Key? key,
  314. }) : super(key: key);
  315. @override
  316. Widget build(BuildContext context) {
  317. return BlocBuilder<SignInBloc, SignInState>(
  318. buildWhen: (previous, current) =>
  319. previous.emailError != current.emailError,
  320. builder: (context, state) {
  321. return RoundedInputField(
  322. hintText: LocaleKeys.signIn_emailHint.tr(),
  323. errorText: context
  324. .read<SignInBloc>()
  325. .state
  326. .emailError
  327. .fold(() => "", (error) => error),
  328. onChanged: (value) =>
  329. context.read<SignInBloc>().add(SignInEvent.emailChanged(value)),
  330. );
  331. },
  332. );
  333. }
  334. }
  335. class OrContinueWith extends StatelessWidget {
  336. const OrContinueWith({super.key});
  337. @override
  338. Widget build(BuildContext context) {
  339. return const Row(
  340. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  341. children: [
  342. Flexible(
  343. child: Divider(
  344. color: Colors.white,
  345. height: 10,
  346. ),
  347. ),
  348. FlowyText.regular(' Or continue with '),
  349. Flexible(
  350. child: Divider(
  351. color: Colors.white,
  352. height: 10,
  353. ),
  354. ),
  355. ],
  356. );
  357. }
  358. }
  359. class ThirdPartySignInButton extends StatelessWidget {
  360. const ThirdPartySignInButton({
  361. Key? key,
  362. required this.icon,
  363. required this.onPressed,
  364. }) : super(key: key);
  365. final FlowySvgData icon;
  366. final VoidCallback onPressed;
  367. @override
  368. Widget build(BuildContext context) {
  369. return FlowyIconButton(
  370. height: 48,
  371. width: 48,
  372. iconPadding: const EdgeInsets.all(8.0),
  373. radius: Corners.s10Border,
  374. onPressed: onPressed,
  375. icon: FlowySvg(
  376. icon,
  377. blendMode: null,
  378. ),
  379. );
  380. }
  381. }
  382. class ThirdPartySignInButtons extends StatelessWidget {
  383. final MainAxisAlignment mainAxisAlignment;
  384. const ThirdPartySignInButtons({
  385. this.mainAxisAlignment = MainAxisAlignment.center,
  386. super.key,
  387. });
  388. @override
  389. Widget build(BuildContext context) {
  390. return Row(
  391. mainAxisAlignment: mainAxisAlignment,
  392. children: const [
  393. GoogleSignUpButton(),
  394. // const SizedBox(width: 20),
  395. // ThirdPartySignInButton(
  396. // icon: 'login/github-mark',
  397. // onPressed: () {
  398. // getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
  399. // context
  400. // .read<SignInBloc>()
  401. // .add(const SignInEvent.signedInWithOAuth('github'));
  402. // },
  403. // ),
  404. // const SizedBox(width: 20),
  405. // ThirdPartySignInButton(
  406. // icon: 'login/discord-mark',
  407. // onPressed: () {
  408. // getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
  409. // context
  410. // .read<SignInBloc>()
  411. // .add(const SignInEvent.signedInWithOAuth('discord'));
  412. // },
  413. // ),
  414. ],
  415. );
  416. }
  417. }
  418. class GoogleSignUpButton extends StatelessWidget {
  419. const GoogleSignUpButton({super.key});
  420. @override
  421. Widget build(BuildContext context) {
  422. return ThirdPartySignInButton(
  423. icon: FlowySvgs.google_mark_xl,
  424. onPressed: () {
  425. getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
  426. context.read<SignInBloc>().add(
  427. const SignInEvent.signedInWithOAuth('google'),
  428. );
  429. },
  430. );
  431. }
  432. }