sign_in_screen.dart 14 KB

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