diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d20122c7c8..614bd3af2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,6 +20,14 @@ jobs: - name: Checkout uses: actions/checkout@v2 +# - name: Create .env file +# working-directory: frontend/appflowy_flutter +# run: | +# touch .env +# echo SUPABASE_URL=${{ secrets.HOST_URL }} >> .env +# echo SUPABASE_ANON_KEY=${{ secrets.HOST_ANON_KEY }} >> .env +# echo SUPABASE_JWT_SECRET=${{ secrets.HOST_JWT_SECRET }} >> .env + - name: Build release notes run: | touch ${{ env.RELEASE_NOTES_PATH }} diff --git a/frontend/appflowy_flutter/lib/env/env.dart b/frontend/appflowy_flutter/lib/env/env.dart index 3714e9b5f5..7300ca2bdf 100644 --- a/frontend/appflowy_flutter/lib/env/env.dart +++ b/frontend/appflowy_flutter/lib/env/env.dart @@ -1,4 +1,5 @@ // lib/env/env.dart +import 'package:appflowy/startup/startup.dart'; import 'package:envied/envied.dart'; part 'env.g.dart'; @@ -37,12 +38,13 @@ abstract class Env { static final String supabaseJwtSecret = _Env.supabaseJwtSecret; } -bool get isSupabaseEnable => false; - // Env.supabaseUrl.isNotEmpty && - // Env.supabaseAnonKey.isNotEmpty && - // Env.supabaseKey.isNotEmpty && - // Env.supabaseJwtSecret.isNotEmpty && - // Env.supabaseDb.isNotEmpty && - // Env.supabaseDbUser.isNotEmpty && - // Env.supabaseDbPassword.isNotEmpty && - // Env.supabaseDbPort.isNotEmpty; +bool get isSupabaseEnabled { + // Only enable supabase in release and develop mode. + if (integrationEnv().isRelease || integrationEnv().isDevelop) { + return Env.supabaseUrl.isNotEmpty && + Env.supabaseAnonKey.isNotEmpty && + Env.supabaseJwtSecret.isNotEmpty; + } else { + return false; + } +} diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 64d0d55c88..ffe73cbfc2 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -1,5 +1,6 @@ import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/network_monitor.dart'; +import 'package:appflowy/env/env.dart'; import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; @@ -82,8 +83,11 @@ void _resolveCommonService( } void _resolveUserDeps(GetIt getIt) { - // getIt.registerFactory(() => AppFlowyAuthService()); - getIt.registerFactory(() => SupabaseAuthService()); + if (isSupabaseEnabled) { + getIt.registerFactory(() => SupabaseAuthService()); + } else { + getIt.registerFactory(() => AppFlowyAuthService()); + } getIt.registerFactory(() => AuthRouter()); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart index 623b2fe059..412e3831ae 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart @@ -8,7 +8,7 @@ bool isSupabaseInitialized = false; class InitSupabaseTask extends LaunchTask { @override Future initialize(LaunchContext context) async { - if (!isSupabaseEnable) { + if (!isSupabaseEnabled) { return; } @@ -18,7 +18,8 @@ class InitSupabaseTask extends LaunchTask { await Supabase.initialize( url: Env.supabaseUrl, anonKey: Env.supabaseAnonKey, - debug: false, + debug: true, + // authFlowType: AuthFlowType.pkce, ); isSupabaseInitialized = true; diff --git a/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart index 99feb10b60..438e51b837 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart @@ -75,12 +75,28 @@ class AppFlowyAuthService implements AuthService { required String platform, AuthTypePB authType = AuthTypePB.Local, Map map = const {}, - }) { - throw UnimplementedError(); + }) async { + return left( + FlowyError.create() + ..code = 0 + ..msg = "Unsupported sign up action", + ); } @override Future> getUser() async { return UserBackendService.getCurrentUserProfile(); } + + @override + Future> signInWithMagicLink({ + required String email, + Map map = const {}, + }) async { + return left( + FlowyError.create() + ..code = 0 + ..msg = "Unsupported sign up action", + ); + } } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart index 82f44fb113..709796953b 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart @@ -42,6 +42,11 @@ abstract class AuthService { Map map, }); + Future> signInWithMagicLink({ + required String email, + Map map, + }); + /// Future signOut(); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart index f53c15e4f6..45ef5176f5 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart @@ -9,9 +9,13 @@ 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:flutter/foundation.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'auth_error.dart'; +// can't use underscore here. +const loginCallback = 'io.appflowy.appflowy-flutter://login-callback'; + class SupabaseAuthService implements AuthService { SupabaseAuthService(); @@ -28,7 +32,7 @@ class SupabaseAuthService implements AuthService { AuthTypePB authType = AuthTypePB.Supabase, Map map = const {}, }) async { - if (!isSupabaseEnable) { + if (!isSupabaseEnabled) { return _appFlowyAuthService.signUp( name: name, email: email, @@ -65,7 +69,7 @@ class SupabaseAuthService implements AuthService { AuthTypePB authType = AuthTypePB.Supabase, Map map = const {}, }) async { - if (!isSupabaseEnable) { + if (!isSupabaseEnabled) { return _appFlowyAuthService.signIn( email: email, password: password, @@ -101,39 +105,25 @@ class SupabaseAuthService implements AuthService { AuthTypePB authType = AuthTypePB.Supabase, Map map = const {}, }) async { - if (!isSupabaseEnable) { - return _appFlowyAuthService.signUpWithOAuth( - platform: platform, - ); + if (!isSupabaseEnabled) { + return _appFlowyAuthService.signUpWithOAuth(platform: platform); } final provider = platform.toProvider(); - final completer = Completer>(); - late final StreamSubscription subscription; - subscription = _auth.onAuthStateChange.listen((event) async { - final user = event.session?.user; - if (event.event != AuthChangeEvent.signedIn || user == null) { - completer.complete(left(AuthError.supabaseSignInWithOauthError)); - } else { - final Either response = await setupAuth( + final completer = supabaseLoginCompleter( + onSuccess: (userId, userEmail) async { + return await setupAuth( map: { - AuthServiceMapKeys.uuid: user.id, - AuthServiceMapKeys.email: user.email ?? user.newEmail ?? '' + AuthServiceMapKeys.uuid: userId, + AuthServiceMapKeys.email: userEmail }, ); - completer.complete(response); - } - subscription.cancel(); - }); - final Map query = {}; - if (provider == Provider.google) { - query['access_type'] = 'offline'; - query['prompt'] = 'consent'; - } + }, + ); + final response = await _auth.signInWithOAuth( provider, - queryParams: query, - redirectTo: - 'io.appflowy.appflowy-flutter://login-callback', // can't use underscore here. + queryParams: queryParamsForProvider(provider), + redirectTo: loginCallback, ); if (!response) { completer.complete(left(AuthError.supabaseSignInWithOauthError)); @@ -145,7 +135,7 @@ class SupabaseAuthService implements AuthService { Future signOut({ AuthTypePB authType = AuthTypePB.Supabase, }) async { - if (isSupabaseEnable) { + if (isSupabaseEnabled) { await _auth.signOut(); } await _appFlowyAuthService.signOut( @@ -163,17 +153,28 @@ class SupabaseAuthService implements AuthService { return _appFlowyAuthService.signUpAsGuest(); } - // @override - // Future> getUser() async { - // final loginType = await getIt() - // .get(KVKeys.loginType) - // .then((value) => value.toOption().toNullable()); - // if (!isSupabaseEnable || (loginType != null && loginType != 'supabase')) { - // return _appFlowyAuthService.getUser(); - // } - // final user = await getSupabaseUser(); - // return user.map((r) => r.toUserProfile()); - // } + @override + Future> signInWithMagicLink({ + required String email, + Map map = const {}, + }) async { + final completer = supabaseLoginCompleter( + onSuccess: (userId, userEmail) async { + return await setupAuth( + map: { + AuthServiceMapKeys.uuid: userId, + AuthServiceMapKeys.email: userEmail + }, + ); + }, + ); + + await _auth.signInWithOtp( + email: email, + emailRedirectTo: kIsWeb ? null : loginCallback, + ); + return completer.future; + } @override Future> getUser() async { @@ -215,3 +216,45 @@ extension on String { } } } + +Completer> supabaseLoginCompleter({ + required Future> Function( + String userId, + String userEmail, + ) onSuccess, +}) { + final completer = Completer>(); + late final StreamSubscription subscription; + final auth = Supabase.instance.client.auth; + + subscription = auth.onAuthStateChange.listen((event) async { + final user = event.session?.user; + if (event.event != AuthChangeEvent.signedIn || user == null) { + completer.complete(left(AuthError.supabaseSignInWithOauthError)); + } else { + final response = await onSuccess( + user.id, + user.email ?? user.newEmail ?? '', + ); + completer.complete(response); + } + subscription.cancel(); + }); + return completer; +} + +Map queryParamsForProvider(Provider provider) { + switch (provider) { + case Provider.github: + return {}; + case Provider.google: + return { + 'access_type': 'offline', + 'prompt': 'consent', + }; + case Provider.discord: + return {}; + default: + return {}; + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index c6a99318dc..e9cb15a483 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -48,6 +48,9 @@ class SignInBloc extends Bloc { ), ); }, + signedWithMagicLink: (SignedWithMagicLink value) async { + await _performActionOnSignInWithMagicLink(state, emit, value.email); + }, ); }); } @@ -99,6 +102,34 @@ class SignInBloc extends Bloc { ); } + Future _performActionOnSignInWithMagicLink( + SignInState state, + Emitter emit, + String email, + ) async { + emit( + state.copyWith( + isSubmitting: true, + emailError: none(), + passwordError: none(), + successOrFail: none(), + ), + ); + + final result = await authService.signInWithMagicLink( + email: email, + ); + emit( + result.fold( + (error) => stateFromCode(error), + (userProfile) => state.copyWith( + isSubmitting: false, + successOrFail: some(left(userProfile)), + ), + ), + ); + } + Future _performActionOnSignInAsGuest( SignInState state, Emitter emit, @@ -154,6 +185,8 @@ class SignInEvent with _$SignInEvent { const factory SignInEvent.signedInWithOAuth(String platform) = SignedInWithOAuth; const factory SignInEvent.signedInAsGuest() = SignedInAsGuest; + const factory SignInEvent.signedWithMagicLink(String email) = + SignedWithMagicLink; const factory SignInEvent.emailChanged(String email) = EmailChanged; const factory SignInEvent.passwordChanged(String password) = PasswordChanged; } diff --git a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart index 59ace71f8c..c4aacf05cd 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart @@ -333,31 +333,31 @@ class ThirdPartySignInButtons extends StatelessWidget { icon: 'login/google-mark', onPressed: () { getIt().set(KVKeys.loginType, 'supabase'); - context - .read() - .add(const SignInEvent.signedInWithOAuth('google')); - }, - ), - const SizedBox(width: 20), - ThirdPartySignInButton( - icon: 'login/github-mark', - onPressed: () { - getIt().set(KVKeys.loginType, 'supabase'); - context - .read() - .add(const SignInEvent.signedInWithOAuth('github')); - }, - ), - const SizedBox(width: 20), - ThirdPartySignInButton( - icon: 'login/discord-mark', - onPressed: () { - getIt().set(KVKeys.loginType, 'supabase'); - context - .read() - .add(const SignInEvent.signedInWithOAuth('discord')); + context.read().add( + const SignInEvent.signedInWithOAuth('google'), + ); }, ), + // const SizedBox(width: 20), + // ThirdPartySignInButton( + // icon: 'login/github-mark', + // onPressed: () { + // getIt().set(KVKeys.loginType, 'supabase'); + // context + // .read() + // .add(const SignInEvent.signedInWithOAuth('github')); + // }, + // ), + // const SizedBox(width: 20), + // ThirdPartySignInButton( + // icon: 'login/discord-mark', + // onPressed: () { + // getIt().set(KVKeys.loginType, 'supabase'); + // context + // .read() + // .add(const SignInEvent.signedInWithOAuth('discord')); + // }, + // ), ], ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart index 1c33008b34..09197a7f46 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart @@ -88,7 +88,7 @@ class SplashScreen extends StatelessWidget { void _handleUnauthenticated(BuildContext context, Unauthenticated result) { // if the env is not configured, we will skip to the 'skip login screen'. - if (isSupabaseEnable) { + if (isSupabaseEnabled) { getIt().pushSignInScreen(context); } else { getIt().pushSkipLoginScreen(context); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 5c5f45e835..103a80af94 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -60,7 +60,7 @@ class SettingsMenu extends StatelessWidget { ), // Only show supabase setting if supabase is enabled and the current auth type is not local - if (isSupabaseEnable && + if (isSupabaseEnabled && context.read().state.userProfile.authType != AuthTypePB.Local) SettingsMenuElement( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart index 42283ad3a8..9f0d4d5205 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -60,7 +60,7 @@ class SettingsUserView extends StatelessWidget { BuildContext context, SettingsUserState state, ) { - if (!isSupabaseEnable) { + if (!isSupabaseEnabled) { return _renderLogoutButton(context); } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs index 9e5b7d8107..5726a9df6a 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs @@ -181,7 +181,7 @@ async fn send_update( let params = builder.build(); postgrest - .from(&table_name(&object.ty)) + .from(AF_COLLAB_UPDATE_TABLE) .insert(params) .execute() .await? diff --git a/frontend/scripts/code_generation/env/generate_env.cmd b/frontend/scripts/code_generation/env/generate_env.cmd new file mode 100644 index 0000000000..d15d078ddd --- /dev/null +++ b/frontend/scripts/code_generation/env/generate_env.cmd @@ -0,0 +1,17 @@ +@echo off + +REM Store the current working directory +set "original_dir=%CD%" + +REM Change the current working directory to the script's location +cd /d "%~dp0" + +REM Navigate to the project root +cd ..\..\..\appflowy_flutter + +REM Navigate to the appflowy_flutter directory and generate files +echo Generating env files +call flutter packages pub get >nul 2>&1 && call dart run build_runner clean && call dart run build_runner build --delete-conflicting-outputs +echo Done generating env files + +cd /d "%original_dir%" diff --git a/frontend/scripts/code_generation/env/generate_env.sh b/frontend/scripts/code_generation/env/generate_env.sh new file mode 100644 index 0000000000..fcadc3b252 --- /dev/null +++ b/frontend/scripts/code_generation/env/generate_env.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Store the current working directory +original_dir=$(pwd) + +cd "$(dirname "$0")" + +# Navigate to the project root +cd ../../../appflowy_flutter + +# Navigate to the appflowy_flutter directory and generate files +echo "Generating env files" +# flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean && +flutter packages pub get >/dev/null 2>&1 +dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs +echo "Done generating env files" + +# Return to the original directory +cd "$original_dir" diff --git a/frontend/scripts/code_generation/generate.cmd b/frontend/scripts/code_generation/generate.cmd index 17fcfa420f..45dafb42c0 100644 --- a/frontend/scripts/code_generation/generate.cmd +++ b/frontend/scripts/code_generation/generate.cmd @@ -22,6 +22,13 @@ cd freezed REM Allow execution permissions on CI chmod +x generate_freezed.cmd call generate_freezed.cmd %* +cd .. + +echo Generating env files using build_runner +cd env +REM Allow execution permissions on CI +chmod +x generate_env.cmd +call generate_env.cmd %* REM Return to the original directory cd /d "%original_dir%" diff --git a/frontend/scripts/code_generation/generate.sh b/frontend/scripts/code_generation/generate.sh index 8c3d051be0..8c800f2eaa 100755 --- a/frontend/scripts/code_generation/generate.sh +++ b/frontend/scripts/code_generation/generate.sh @@ -22,6 +22,13 @@ cd freezed # Allow execution permissions on CI chmod +x ./generate_freezed.sh ./generate_freezed.sh "$@" +cd.. + +echo "Generating env files using build_runner" +cd env +# Allow execution permissions on CI +chmod +x ./generate_env.sh +./generate_env.sh "$@" # Return to the original directory cd "$original_dir"