sign_in_screen.dart 13 KB

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