Quellcode durchsuchen

test: add supabase auth tests (#3250)

* test: add supabase auth tests

* chore: format before test

* chore: fix warnings

* ci: rust test

* chore: disable clicking get started button when logining through the google OAuth
Nathan.fooo vor 1 Jahr
Ursprung
Commit
77637ff461
20 geänderte Dateien mit 457 neuen und 168 gelöschten Zeilen
  1. 85 0
      frontend/appflowy_flutter/integration_test/auth/auth_test.dart
  2. 6 0
      frontend/appflowy_flutter/integration_test/runner.dart
  3. 64 0
      frontend/appflowy_flutter/integration_test/util/auth_operation.dart
  4. 8 0
      frontend/appflowy_flutter/integration_test/util/settings.dart
  5. 1 0
      frontend/appflowy_flutter/integration_test/util/util.dart
  6. 1 1
      frontend/appflowy_flutter/lib/env/env.dart
  7. 8 3
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  8. 5 5
      frontend/appflowy_flutter/lib/startup/startup.dart
  9. 1 1
      frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart
  10. 6 6
      frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart
  11. 5 5
      frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart
  12. 1 1
      frontend/appflowy_flutter/lib/user/application/auth/device_id.dart
  13. 110 0
      frontend/appflowy_flutter/lib/user/application/auth/mock_auth_service.dart
  14. 11 32
      frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart
  15. 72 50
      frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart
  16. 1 1
      frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart
  17. 2 2
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart
  18. 46 36
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart
  19. 19 23
      frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart
  20. 5 2
      frontend/rust-lib/dart-ffi/.cargo/config.toml

+ 85 - 0
frontend/appflowy_flutter/integration_test/auth/auth_test.dart

@@ -0,0 +1,85 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/workspace/application/settings/prelude.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import '../util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('auth', () {
+    testWidgets('sign in with supabase', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoogleLoginInButton();
+      tester.expectToSeeHomePage();
+    });
+
+    testWidgets('sign out with supabase', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoogleLoginInButton();
+
+      // Open the setting page and sign out
+      await tester.openSettings();
+      await tester.openSettingsPage(SettingsPage.user);
+      await tester.tapButton(find.byType(SettingLogoutButton));
+
+      tester.expectToSeeText(LocaleKeys.button_OK.tr());
+      await tester.tapButtonWithName(LocaleKeys.button_OK.tr());
+
+      // Go to the sign in page again
+      await tester.pumpAndSettle(const Duration(seconds: 1));
+      tester.expectToSeeGoogleLoginButton();
+    });
+
+    testWidgets('sign in as annoymous', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapSignInAsGuest();
+
+      // should not see the sync setting page when sign in as annoymous
+      await tester.openSettings();
+      await tester.expectNoSettingsPage(SettingsPage.syncSetting);
+    });
+
+    testWidgets('enable encryption', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoogleLoginInButton();
+
+      // Open the setting page and sign out
+      await tester.openSettings();
+      await tester.openSettingsPage(SettingsPage.syncSetting);
+
+      // the switch should be off by default
+      tester.assertEnableEncryptSwitchValue(false);
+      await tester.toggleEnableEncrypt();
+
+      // the switch should be on after toggling
+      tester.assertEnableEncryptSwitchValue(true);
+
+      // the switch can not be toggled back to off
+      await tester.toggleEnableEncrypt();
+      tester.assertEnableEncryptSwitchValue(true);
+    });
+
+    testWidgets('enable sync', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoogleLoginInButton();
+
+      // Open the setting page and sign out
+      await tester.openSettings();
+      await tester.openSettingsPage(SettingsPage.syncSetting);
+
+      // the switch should be on by default
+      tester.assertEnableSyncSwitchValue(true);
+      await tester.toggleEnableSync();
+
+      // the switch should be off
+      tester.assertEnableSyncSwitchValue(false);
+
+      // the switch should be on after toggling
+      await tester.toggleEnableSync();
+      tester.assertEnableSyncSwitchValue(true);
+    });
+  });
+}

+ 6 - 0
frontend/appflowy_flutter/integration_test/runner.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/env/env.dart';
 import 'package:integration_test/integration_test.dart';
 
 import 'database_calendar_test.dart' as database_calendar_test;
@@ -19,6 +20,7 @@ import 'board/board_test_runner.dart' as board_test_runner;
 import 'tabs_test.dart' as tabs_test;
 import 'hotkeys_test.dart' as hotkeys_test;
 import 'appearance_settings_test.dart' as appearance_test_runner;
+import 'auth/auth_test.dart' as auth_test_runner;
 
 /// The main task runner for all integration tests in AppFlowy.
 ///
@@ -63,6 +65,10 @@ void main() {
   // Appearance integration test
   appearance_test_runner.main();
 
+  if (isSupabaseEnabled) {
+    auth_test_runner.main();
+  }
+
   // board_test.main();
   // empty_document_test.main();
   // smart_menu_test.main();

+ 64 - 0
frontend/appflowy_flutter/integration_test/util/auth_operation.dart

@@ -0,0 +1,64 @@
+import 'package:appflowy/user/presentation/sign_in_screen.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'base.dart';
+
+extension AppFlowyAuthTest on WidgetTester {
+  Future<void> tapGoogleLoginInButton() async {
+    await tapButton(find.byType(GoogleSignUpButton));
+  }
+
+  Future<void> tapSignInAsGuest() async {
+    await tapButton(find.byType(SignInAsGuestButton));
+  }
+
+  void expectToSeeGoogleLoginButton() {
+    expect(find.byType(GoogleSignUpButton), findsOneWidget);
+  }
+
+  void assertSwitchValue(Finder finder, bool value) {
+    final Switch switchWidget = widget(finder);
+    final isSwitched = switchWidget.value;
+    assert(isSwitched == value);
+  }
+
+  void assertEnableEncryptSwitchValue(bool value) {
+    assertSwitchValue(
+      find.descendant(
+        of: find.byType(EnableEncrypt),
+        matching: find.byWidgetPredicate((widget) => widget is Switch),
+      ),
+      value,
+    );
+  }
+
+  void assertEnableSyncSwitchValue(bool value) {
+    assertSwitchValue(
+      find.descendant(
+        of: find.byType(EnableSync),
+        matching: find.byWidgetPredicate((widget) => widget is Switch),
+      ),
+      value,
+    );
+  }
+
+  Future<void> toggleEnableEncrypt() async {
+    final finder = find.descendant(
+      of: find.byType(EnableEncrypt),
+      matching: find.byWidgetPredicate((widget) => widget is Switch),
+    );
+
+    await tapButton(finder);
+  }
+
+  Future<void> toggleEnableSync() async {
+    final finder = find.descendant(
+      of: find.byType(EnableSync),
+      matching: find.byWidgetPredicate((widget) => widget is Switch),
+    );
+
+    await tapButton(finder);
+  }
+}

+ 8 - 0
frontend/appflowy_flutter/integration_test/util/settings.dart

@@ -29,6 +29,14 @@ extension AppFlowySettings on WidgetTester {
     return;
   }
 
+  Future<void> expectNoSettingsPage(SettingsPage page) async {
+    final button = find.byWidgetPredicate(
+      (widget) => widget is SettingsMenuElement && widget.page == page,
+    );
+    expect(button, findsNothing);
+    return;
+  }
+
   /// Restore the AppFlowy data storage location
   Future<void> restoreLocation() async {
     final button =

+ 1 - 0
frontend/appflowy_flutter/integration_test/util/util.dart

@@ -6,3 +6,4 @@ export 'expectation.dart';
 export 'editor_test_operations.dart';
 export 'mock/mock_url_launcher.dart';
 export 'ime.dart';
+export 'auth_operation.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/env/env.dart

@@ -40,7 +40,7 @@ abstract class Env {
 
 bool get isSupabaseEnabled {
   // Only enable supabase in release and develop mode.
-  if (integrationEnv().isRelease || integrationEnv().isDevelop) {
+  if (integrationMode().isRelease || integrationMode().isDevelop) {
     return Env.supabaseUrl.isNotEmpty &&
         Env.supabaseAnonKey.isNotEmpty &&
         Env.supabaseJwtSecret.isNotEmpty;

+ 8 - 3
frontend/appflowy_flutter/lib/startup/deps_resolver.dart

@@ -12,6 +12,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/ser
 import 'package:appflowy/plugins/trash/application/prelude.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy/user/application/auth/mock_auth_service.dart';
 import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
 import 'package:appflowy/user/application/prelude.dart';
 import 'package:appflowy/user/application/user_listener.dart';
@@ -38,7 +39,7 @@ class DependencyResolver {
     GetIt getIt,
     IntegrationMode mode,
   ) async {
-    _resolveUserDeps(getIt);
+    _resolveUserDeps(getIt, mode);
     _resolveHomeDeps(getIt);
     _resolveFolderDeps(getIt);
     _resolveDocDeps(getIt);
@@ -86,9 +87,13 @@ void _resolveCommonService(
   );
 }
 
-void _resolveUserDeps(GetIt getIt) {
+void _resolveUserDeps(GetIt getIt, IntegrationMode mode) {
   if (isSupabaseEnabled) {
-    getIt.registerFactory<AuthService>(() => SupabaseAuthService());
+    if (mode.isIntegrationTest) {
+      getIt.registerFactory<AuthService>(() => MockAuthService());
+    } else {
+      getIt.registerFactory<AuthService>(() => SupabaseAuthService());
+    }
   } else {
     getIt.registerFactory<AuthService>(() => AppFlowyAuthService());
   }

+ 5 - 5
frontend/appflowy_flutter/lib/startup/startup.dart

@@ -27,7 +27,7 @@ class FlowyRunnerContext {
 Future<void> runAppFlowy() async {
   await FlowyRunner.run(
     FlowyApp(),
-    integrationEnv(),
+    integrationMode(),
   );
 }
 
@@ -86,7 +86,7 @@ class FlowyRunner {
 
 Future<void> initGetIt(
   GetIt getIt,
-  IntegrationMode env,
+  IntegrationMode mode,
   EntryPoint f,
   LaunchConfiguration config,
 ) async {
@@ -98,14 +98,14 @@ Future<void> initGetIt(
     () => AppLauncher(
       context: LaunchContext(
         getIt,
-        env,
+        mode,
         config,
       ),
     ),
   );
   getIt.registerSingleton<PluginSandbox>(PluginSandbox());
 
-  await DependencyResolver.resolve(getIt, env);
+  await DependencyResolver.resolve(getIt, mode);
 }
 
 class LaunchContext {
@@ -171,7 +171,7 @@ enum IntegrationMode {
   bool get isDevelop => this == IntegrationMode.develop;
 }
 
-IntegrationMode integrationEnv() {
+IntegrationMode integrationMode() {
   if (Platform.environment.containsKey('FLUTTER_TEST')) {
     return IntegrationMode.unitTest;
   }

+ 1 - 1
frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart

@@ -45,7 +45,7 @@ AppFlowyEnv getAppFlowyEnv() {
 /// The default directory to store the user data. The directory can be
 /// customized by the user via the [ApplicationDataStorage]
 Future<Directory> appFlowyApplicationDataDirectory() async {
-  switch (integrationEnv()) {
+  switch (integrationMode()) {
     case IntegrationMode.develop:
       final Directory documentsDir = await getApplicationSupportDirectory()
         ..create();

+ 6 - 6
frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart

@@ -19,7 +19,7 @@ class AppFlowyAuthService implements AuthService {
     required String email,
     required String password,
     AuthTypePB authType = AuthTypePB.Local,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
     final request = SignInPayloadPB.create()
       ..email = email
@@ -36,7 +36,7 @@ class AppFlowyAuthService implements AuthService {
     required String email,
     required String password,
     AuthTypePB authType = AuthTypePB.Local,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
     final request = SignUpPayloadPB.create()
       ..name = name
@@ -53,7 +53,7 @@ class AppFlowyAuthService implements AuthService {
   @override
   Future<void> signOut({
     AuthTypePB authType = AuthTypePB.Local,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
     await UserEventSignOut().send();
     return;
@@ -62,7 +62,7 @@ class AppFlowyAuthService implements AuthService {
   @override
   Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
     AuthTypePB authType = AuthTypePB.Local,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) {
     const password = "Guest!@123456";
     final uid = uuid();
@@ -78,7 +78,7 @@ class AppFlowyAuthService implements AuthService {
   Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
     required String platform,
     AuthTypePB authType = AuthTypePB.Local,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
     return left(
       FlowyError.create()
@@ -95,7 +95,7 @@ class AppFlowyAuthService implements AuthService {
   @override
   Future<Either<FlowyError, UserProfilePB>> signInWithMagicLink({
     required String email,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
     return left(
       FlowyError.create()

+ 5 - 5
frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart

@@ -18,7 +18,7 @@ abstract class AuthService {
     required String email,
     required String password,
     AuthTypePB authType,
-    Map<String, String> map,
+    Map<String, String> params,
   });
 
   /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
@@ -27,25 +27,25 @@ abstract class AuthService {
     required String email,
     required String password,
     AuthTypePB authType,
-    Map<String, String> map,
+    Map<String, String> params,
   });
 
   ///
   Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
     required String platform,
     AuthTypePB authType,
-    Map<String, String> map,
+    Map<String, String> params,
   });
 
   /// Returns a default [UserProfilePB]
   Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
     AuthTypePB authType,
-    Map<String, String> map,
+    Map<String, String> params,
   });
 
   Future<Either<FlowyError, UserProfilePB>> signInWithMagicLink({
     required String email,
-    Map<String, String> map,
+    Map<String, String> params,
   });
 
   ///

+ 1 - 1
frontend/appflowy_flutter/lib/user/application/auth/device_id.dart

@@ -8,7 +8,7 @@ import 'package:flutter/services.dart';
 final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
 
 Future<String> getDeviceId() async {
-  if (integrationEnv().isTest) {
+  if (integrationMode().isTest) {
     return "test_device_id";
   }
 

+ 110 - 0
frontend/appflowy_flutter/lib/user/application/auth/mock_auth_service.dart

@@ -0,0 +1,110 @@
+import 'dart:async';
+
+import 'package:appflowy/user/application/auth/appflowy_auth_service.dart';
+import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy/user/application/user_service.dart';
+import 'package:appflowy_backend/dispatch/dispatch.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:dartz/dartz.dart';
+import 'package:nanoid/nanoid.dart';
+import 'package:supabase_flutter/supabase_flutter.dart';
+
+import 'auth_error.dart';
+
+/// Only used for testing.
+class MockAuthService implements AuthService {
+  MockAuthService();
+
+  SupabaseClient get _client => Supabase.instance.client;
+  GoTrueClient get _auth => _client.auth;
+
+  final AppFlowyAuthService _appFlowyAuthService = AppFlowyAuthService();
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signUp({
+    required String name,
+    required String email,
+    required String password,
+    AuthTypePB authType = AuthTypePB.Supabase,
+    Map<String, String> params = const {},
+  }) async {
+    throw UnimplementedError();
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signIn({
+    required String email,
+    required String password,
+    AuthTypePB authType = AuthTypePB.Supabase,
+    Map<String, String> params = const {},
+  }) async {
+    throw UnimplementedError();
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
+    required String platform,
+    AuthTypePB authType = AuthTypePB.Supabase,
+    Map<String, String> params = const {},
+  }) async {
+    try {
+      final response = await _auth.signUp(
+        email: "${nanoid(10)}@appflowy.io",
+        password: "AppFlowyTest123!",
+      );
+
+      final uuid = response.user!.id;
+      final email = response.user!.email!;
+
+      final payload = ThirdPartyAuthPB(
+        authType: AuthTypePB.Supabase,
+        map: {
+          AuthServiceMapKeys.uuid: uuid,
+          AuthServiceMapKeys.email: email,
+          AuthServiceMapKeys.deviceId: 'MockDeviceId'
+        },
+      );
+      return UserEventThirdPartyAuth(payload)
+          .send()
+          .then((value) => value.swap());
+    } on AuthException catch (e) {
+      Log.error(e);
+      return Left(AuthError.supabaseSignInError);
+    }
+  }
+
+  @override
+  Future<void> signOut({
+    AuthTypePB authType = AuthTypePB.Supabase,
+  }) async {
+    await _auth.signOut();
+    await _appFlowyAuthService.signOut(
+      authType: authType,
+    );
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
+    AuthTypePB authType = AuthTypePB.Supabase,
+    Map<String, String> params = const {},
+  }) async {
+    // supabase don't support guest login.
+    // so, just forward to our backend.
+    return _appFlowyAuthService.signUpAsGuest();
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signInWithMagicLink({
+    required String email,
+    Map<String, String> params = const {},
+  }) async {
+    throw UnimplementedError();
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> getUser() async {
+    return UserBackendService.getCurrentUserProfile();
+  }
+}

+ 11 - 32
frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart

@@ -1,6 +1,5 @@
 import 'dart:async';
 
-import 'package:appflowy/env/env.dart';
 import 'package:appflowy/startup/tasks/prelude.dart';
 import 'package:appflowy/user/application/auth/appflowy_auth_service.dart';
 import 'package:appflowy/user/application/auth/auth_service.dart';
@@ -29,16 +28,8 @@ class SupabaseAuthService implements AuthService {
     required String email,
     required String password,
     AuthTypePB authType = AuthTypePB.Supabase,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
-    if (!isSupabaseEnabled) {
-      return _appFlowyAuthService.signUp(
-        name: name,
-        email: email,
-        password: password,
-      );
-    }
-
     // fetch the uuid from supabase.
     final response = await _auth.signUp(
       email: email,
@@ -55,7 +46,7 @@ class SupabaseAuthService implements AuthService {
       email: email,
       password: password,
       authType: authType,
-      map: {
+      params: {
         AuthServiceMapKeys.uuid: uuid,
       },
     );
@@ -66,15 +57,8 @@ class SupabaseAuthService implements AuthService {
     required String email,
     required String password,
     AuthTypePB authType = AuthTypePB.Supabase,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
-    if (!isSupabaseEnabled) {
-      return _appFlowyAuthService.signIn(
-        email: email,
-        password: password,
-      );
-    }
-
     try {
       final response = await _auth.signInWithPassword(
         email: email,
@@ -88,7 +72,7 @@ class SupabaseAuthService implements AuthService {
         email: email,
         password: password,
         authType: authType,
-        map: {
+        params: {
           AuthServiceMapKeys.uuid: uuid,
         },
       );
@@ -102,11 +86,8 @@ class SupabaseAuthService implements AuthService {
   Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
     required String platform,
     AuthTypePB authType = AuthTypePB.Supabase,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
-    if (!isSupabaseEnabled) {
-      return _appFlowyAuthService.signUpWithOAuth(platform: platform);
-    }
     // Before signing in, sign out any existing users. Otherwise, the callback will be triggered even if the user doesn't click the 'Sign In' button on the website
     if (_auth.currentUser != null) {
       await _auth.signOut();
@@ -115,7 +96,7 @@ class SupabaseAuthService implements AuthService {
     final provider = platform.toProvider();
     final completer = supabaseLoginCompleter(
       onSuccess: (userId, userEmail) async {
-        return await setupAuth(
+        return await _setupAuth(
           map: {
             AuthServiceMapKeys.uuid: userId,
             AuthServiceMapKeys.email: userEmail,
@@ -140,9 +121,7 @@ class SupabaseAuthService implements AuthService {
   Future<void> signOut({
     AuthTypePB authType = AuthTypePB.Supabase,
   }) async {
-    if (isSupabaseEnabled) {
-      await _auth.signOut();
-    }
+    await _auth.signOut();
     await _appFlowyAuthService.signOut(
       authType: authType,
     );
@@ -151,7 +130,7 @@ class SupabaseAuthService implements AuthService {
   @override
   Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
     AuthTypePB authType = AuthTypePB.Supabase,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
     // supabase don't support guest login.
     // so, just forward to our backend.
@@ -161,11 +140,11 @@ class SupabaseAuthService implements AuthService {
   @override
   Future<Either<FlowyError, UserProfilePB>> signInWithMagicLink({
     required String email,
-    Map<String, String> map = const {},
+    Map<String, String> params = const {},
   }) async {
     final completer = supabaseLoginCompleter(
       onSuccess: (userId, userEmail) async {
-        return await setupAuth(
+        return await _setupAuth(
           map: {
             AuthServiceMapKeys.uuid: userId,
             AuthServiceMapKeys.email: userEmail,
@@ -195,7 +174,7 @@ class SupabaseAuthService implements AuthService {
     return Right(user);
   }
 
-  Future<Either<FlowyError, UserProfilePB>> setupAuth({
+  Future<Either<FlowyError, UserProfilePB>> _setupAuth({
     required Map<String, String> map,
   }) async {
     final payload = ThirdPartyAuthPB(

+ 72 - 50
frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart

@@ -222,46 +222,59 @@ class SignInAsGuestButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return BlocProvider(
-      create: (context) => HistoricalUserBloc()
-        ..add(
-          const HistoricalUserEvent.initial(),
-        ),
-      child: BlocListener<HistoricalUserBloc, HistoricalUserState>(
-        listenWhen: (previous, current) =>
-            previous.openedHistoricalUser != current.openedHistoricalUser,
-        listener: (context, state) async {
-          await runAppFlowy();
-        },
-        child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
-          builder: (context, state) {
-            if (state.historicalUsers.isEmpty) {
-              return RoundedTextButton(
-                title: LocaleKeys.signIn_loginAsGuestButtonText.tr(),
-                height: 48,
-                borderRadius: Corners.s6Border,
-                onPressed: () {
-                  getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
-                  context
-                      .read<SignInBloc>()
-                      .add(const SignInEvent.signedInAsGuest());
-                },
-              );
-            } else {
-              return RoundedTextButton(
-                title: LocaleKeys.signIn_continueAnonymousUser.tr(),
-                height: 48,
-                borderRadius: Corners.s6Border,
-                onPressed: () {
-                  final bloc = context.read<HistoricalUserBloc>();
-                  final user = bloc.state.historicalUsers.first;
-                  bloc.add(HistoricalUserEvent.openHistoricalUser(user));
-                },
-              );
-            }
-          },
-        ),
-      ),
+    return BlocBuilder<SignInBloc, SignInState>(
+      builder: (context, signInState) {
+        return BlocProvider(
+          create: (context) => HistoricalUserBloc()
+            ..add(
+              const HistoricalUserEvent.initial(),
+            ),
+          child: BlocListener<HistoricalUserBloc, HistoricalUserState>(
+            listenWhen: (previous, current) =>
+                previous.openedHistoricalUser != current.openedHistoricalUser,
+            listener: (context, state) async {
+              await runAppFlowy();
+            },
+            child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
+              builder: (context, state) {
+                final text = state.historicalUsers.isEmpty
+                    ? FlowyText.medium(
+                        LocaleKeys.signIn_loginAsGuestButtonText.tr(),
+                        textAlign: TextAlign.center,
+                      )
+                    : FlowyText.medium(
+                        LocaleKeys.signIn_continueAnonymousUser.tr(),
+                        textAlign: TextAlign.center,
+                      );
+
+                final onTap = state.historicalUsers.isEmpty
+                    ? () {
+                        getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
+                        context
+                            .read<SignInBloc>()
+                            .add(const SignInEvent.signedInAsGuest());
+                      }
+                    : () {
+                        final bloc = context.read<HistoricalUserBloc>();
+                        final user = bloc.state.historicalUsers.first;
+                        bloc.add(HistoricalUserEvent.openHistoricalUser(user));
+                      };
+
+                return SizedBox(
+                  height: 48,
+                  child: FlowyButton(
+                    isSelected: true,
+                    disable: signInState.isSubmitting,
+                    text: text,
+                    radius: Corners.s6Border,
+                    onTap: onTap,
+                  ),
+                );
+              },
+            ),
+          ),
+        );
+      },
     );
   }
 }
@@ -410,16 +423,8 @@ class ThirdPartySignInButtons extends StatelessWidget {
   Widget build(BuildContext context) {
     return Row(
       mainAxisAlignment: mainAxisAlignment,
-      children: [
-        ThirdPartySignInButton(
-          icon: FlowySvgs.google_mark_xl,
-          onPressed: () {
-            getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
-            context.read<SignInBloc>().add(
-                  const SignInEvent.signedInWithOAuth('google'),
-                );
-          },
-        ),
+      children: const [
+        GoogleSignUpButton(),
         // const SizedBox(width: 20),
         // ThirdPartySignInButton(
         //   icon: 'login/github-mark',
@@ -444,3 +449,20 @@ class ThirdPartySignInButtons extends StatelessWidget {
     );
   }
 }
+
+class GoogleSignUpButton extends StatelessWidget {
+  const GoogleSignUpButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return ThirdPartySignInButton(
+      icon: FlowySvgs.google_mark_xl,
+      onPressed: () {
+        getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
+        context.read<SignInBloc>().add(
+              const SignInEvent.signedInWithOAuth('google'),
+            );
+      },
+    );
+  }
+}

+ 1 - 1
frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart

@@ -108,7 +108,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
   Future<void> _relaunchAppAndAutoRegister() async {
     await FlowyRunner.run(
       FlowyApp(),
-      integrationEnv(),
+      integrationMode(),
       config: const LaunchConfiguration(
         autoRegistrationSupported: true,
       ),

+ 2 - 2
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart

@@ -177,7 +177,7 @@ class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> {
           await context.read<SettingsLocationCubit>().setCustomPath(path);
           await FlowyRunner.run(
             FlowyApp(),
-            integrationEnv(),
+            integrationMode(),
             config: const LaunchConfiguration(
               autoRegistrationSupported: true,
             ),
@@ -252,7 +252,7 @@ class _RecoverDefaultStorageButtonState
             .resetDataStoragePathToApplicationDefault();
         await FlowyRunner.run(
           FlowyApp(),
-          integrationEnv(),
+          integrationMode(),
           config: const LaunchConfiguration(
             autoRegistrationSupported: true,
           ),

+ 46 - 36
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart

@@ -82,8 +82,7 @@ class SettingsUserView extends StatelessWidget {
         );
       }
     }
-
-    return _renderLogoutButton(context);
+    return SettingLogoutButton(user: user, didLogout: didLogout);
   }
 
   Widget _renderUserNameInput(BuildContext context) {
@@ -106,40 +105,6 @@ class SettingsUserView extends StatelessWidget {
         context.read<SettingsUserViewBloc>().state.userProfile.openaiKey;
     return _OpenaiKeyInput(openAIKey);
   }
-
-  Widget _renderLogoutButton(BuildContext context) {
-    return Center(
-      child: SizedBox(
-        width: 160,
-        child: FlowyButton(
-          margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 2.0),
-          text: FlowyText.medium(
-            LocaleKeys.settings_menu_logout.tr(),
-            fontSize: 13,
-            textAlign: TextAlign.center,
-          ),
-          onTap: () async {
-            NavigatorAlertDialog(
-              title: logoutPromptMessage(),
-              confirm: () async {
-                await getIt<AuthService>().signOut();
-                didLogout();
-              },
-            ).show(context);
-          },
-        ),
-      ),
-    );
-  }
-
-  String logoutPromptMessage() {
-    switch (user.encryptionType) {
-      case EncryptionTypePB.Symmetric:
-        return LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr();
-      default:
-        return LocaleKeys.settings_menu_logoutPrompt.tr();
-    }
-  }
 }
 
 @visibleForTesting
@@ -409,3 +374,48 @@ class IconOption extends StatelessWidget {
     );
   }
 }
+
+class SettingLogoutButton extends StatelessWidget {
+  final UserProfilePB user;
+  final VoidCallback didLogout;
+  const SettingLogoutButton({
+    required this.user,
+    required this.didLogout,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: SizedBox(
+        width: 160,
+        child: FlowyButton(
+          margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 2.0),
+          text: FlowyText.medium(
+            LocaleKeys.settings_menu_logout.tr(),
+            fontSize: 13,
+            textAlign: TextAlign.center,
+          ),
+          onTap: () async {
+            NavigatorAlertDialog(
+              title: logoutPromptMessage(),
+              confirm: () async {
+                await getIt<AuthService>().signOut();
+                didLogout();
+              },
+            ).show(context);
+          },
+        ),
+      ),
+    );
+  }
+
+  String logoutPromptMessage() {
+    switch (user.encryptionType) {
+      case EncryptionTypePB.Symmetric:
+        return LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr();
+      default:
+        return LocaleKeys.settings_menu_logoutPrompt.tr();
+    }
+  }
+}

+ 19 - 23
frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -43,30 +43,26 @@ class FlowyButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    if (!disable) {
-      return GestureDetector(
-        behavior: HitTestBehavior.opaque,
-        onTap: onTap,
-        onSecondaryTap: onSecondaryTap,
-        child: FlowyHover(
-          style: HoverStyle(
-            borderRadius: radius ?? Corners.s6Border,
-            hoverColor: hoverColor ?? Theme.of(context).colorScheme.secondary,
-          ),
-          onHover: onHover,
-          isSelected: () => isSelected,
-          builder: (context, onHover) => _render(),
-        ),
-      );
-    } else {
-      return Opacity(
-        opacity: disableOpacity,
-        child: MouseRegion(
-          cursor: SystemMouseCursors.forbidden,
-          child: _render(),
+    final color = hoverColor ?? Theme.of(context).colorScheme.secondary;
+    final alpha = (255 * disableOpacity).toInt();
+    color.withAlpha(alpha);
+
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      onTap: disable ? null : onTap,
+      onSecondaryTap: disable ? null : onSecondaryTap,
+      child: FlowyHover(
+        cursor:
+            disable ? SystemMouseCursors.forbidden : SystemMouseCursors.click,
+        style: HoverStyle(
+          borderRadius: radius ?? Corners.s6Border,
+          hoverColor: color,
         ),
-      );
-    }
+        onHover: disable ? null : onHover,
+        isSelected: () => isSelected,
+        builder: (context, onHover) => _render(),
+      ),
+    );
   }
 
   Widget _render() {

+ 5 - 2
frontend/rust-lib/dart-ffi/.cargo/config.toml

@@ -1,5 +1,8 @@
 [build]
 rustflags = ["--cfg", "tokio_unstable"]
 
-#[target.aarch64-apple-darwin]
-#BINDGEN_EXTRA_CLANG_ARGS="--target=aarch64-apple-darwin"
+[target.x86_64-apple-darwin]
+rustflags = ["-C", "target-cpu=native", "-C", "link-arg=-mmacosx-version-min=11.0"]
+
+[target.aarch64-apple-darwin]
+rustflags = ["-C", "target-cpu=native", "-C", "link-arg=-mmacosx-version-min=11.0"]