mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: custom server url in application (#3996)
* chore:test * chore: update ui * feat: set appflowy cloud url * chore: add self host docs * fix: save user * fix: sign out when authenticator not match * fix: sign out when authenticator not match * fix: db lock * chore: remove unuse env file * test: disable supabase cloud test * test: disable supabase cloud test * chore: fix save
This commit is contained in:
parent
e18e031710
commit
1fad713477
@ -1,40 +0,0 @@
|
||||
# Copy the 'dev.env' file to '.env':
|
||||
# Use the command 'cp dev.env .env' to create a copy of 'dev.env' named '.env'.
|
||||
# After copying, update the '.env' file with the necessary environment parameters.
|
||||
|
||||
# Generate the 'env.dart' from this '.env' file:
|
||||
# Option 1: Use the "Generate Env File" task in VSCode.
|
||||
# Option 2: Execute the commands in the appflowy_flutter directory:
|
||||
# cd appflowy_flutter
|
||||
# dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Note on Configuration Priority:
|
||||
# If both Supabase config and AppFlowy cloud config are provided in the '.env' file,
|
||||
# the AppFlowy cloud config will be prioritized and the Supabase config ignored.
|
||||
# Ensure only one of these configurations is active at any given time.
|
||||
|
||||
|
||||
# Cloud Type Configuration
|
||||
# Use this configuration file to specify the cloud type and its associated settings. The available cloud types are:
|
||||
# Local: 0
|
||||
# Supabase: 1
|
||||
# AppFlowy Cloud: 2
|
||||
# By default, it's set to Local.
|
||||
CLOUD_TYPE=0
|
||||
|
||||
# Supabase Configuration
|
||||
# If using Supabase (CLOUD_TYPE=1), provide the following details:
|
||||
SUPABASE_URL=
|
||||
SUPABASE_ANON_KEY=
|
||||
|
||||
# AppFlowy Cloud Configuration
|
||||
# If using Supabase (CLOUD_TYPE=2), provide the following details:
|
||||
#
|
||||
# When using localhost for development. you can use the following settings:
|
||||
# APPFLOWY_CLOUD_BASE_URL=http://localhost:8000
|
||||
# APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws
|
||||
# APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998
|
||||
|
||||
APPFLOWY_CLOUD_BASE_URL=
|
||||
APPFLOWY_CLOUD_WS_BASE_URL=
|
||||
APPFLOWY_CLOUD_GOTRUE_URL=
|
@ -1,8 +1,6 @@
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'appearance_settings_test.dart' as appearance_test_runner;
|
||||
import 'auth/auth_test.dart' as auth_test_runner;
|
||||
import 'board/board_test_runner.dart' as board_test_runner;
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_cell_test.dart' as database_cell_test;
|
||||
@ -32,7 +30,7 @@ import 'tabs_test.dart' as tabs_test;
|
||||
/// If flutter/flutter#101031 is resolved, this file can be removed completely.
|
||||
/// Once removed, the integration_test.yaml must be updated to exclude this as
|
||||
/// as the test target.
|
||||
void main() {
|
||||
Future<void> main() async {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// This test must be run first, otherwise the CI will fail.
|
||||
@ -76,9 +74,10 @@ void main() {
|
||||
// User settings
|
||||
settings_test_runner.main();
|
||||
|
||||
if (isCloudEnabled) {
|
||||
auth_test_runner.main();
|
||||
}
|
||||
// final cloudType = await getCloudType();
|
||||
// if (cloudType == CloudType.supabase) {
|
||||
// auth_test_runner.main();
|
||||
// }
|
||||
|
||||
// board_test.main();
|
||||
// empty_document_test.main();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -37,7 +37,7 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
void assertEnableSyncSwitchValue(bool value) {
|
||||
assertSwitchValue(
|
||||
find.descendant(
|
||||
of: find.byType(EnableSync),
|
||||
of: find.byType(SupabaseEnableSync),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
),
|
||||
value,
|
||||
@ -55,7 +55,7 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
|
||||
Future<void> toggleEnableSync() async {
|
||||
final finder = find.descendant(
|
||||
of: find.byType(EnableSync),
|
||||
of: find.byType(SupabaseEnableSync),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
);
|
||||
|
||||
|
@ -83,7 +83,7 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> waitUntilSignInPageShow() async {
|
||||
if (isCloudEnabled) {
|
||||
if (isAuthEnabled) {
|
||||
final finder = find.byType(SignInAnonymousButton);
|
||||
await pumpUntilFound(finder);
|
||||
expect(finder, findsOneWidget);
|
||||
|
@ -303,7 +303,7 @@ extension CommonOperations on WidgetTester {
|
||||
KVKeys.showRenameDialogWhenCreatingNewFile,
|
||||
(value) => bool.parse(value),
|
||||
);
|
||||
final showRenameDialog = settingsOrFailure.fold((l) => false, (r) => r);
|
||||
final showRenameDialog = settingsOrFailure.fold(() => false, (r) => r);
|
||||
if (showRenameDialog) {
|
||||
await tapOKButton();
|
||||
}
|
||||
|
@ -1,13 +1,10 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class KeyValueStorage {
|
||||
Future<void> set(String key, String value);
|
||||
Future<Either<FlowyError, String>> get(String key);
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
Future<Option<String>> get(String key);
|
||||
Future<Option<T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
);
|
||||
@ -20,25 +17,25 @@ class DartKeyValue implements KeyValueStorage {
|
||||
SharedPreferences get sharedPreferences => _sharedPreferences!;
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, String>> get(String key) async {
|
||||
Future<Option<String>> get(String key) async {
|
||||
await _initSharedPreferencesIfNeeded();
|
||||
|
||||
final value = sharedPreferences.getString(key);
|
||||
if (value != null) {
|
||||
return Right(value);
|
||||
return Some(value);
|
||||
}
|
||||
return Left(FlowyError());
|
||||
return none();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
Future<Option<T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
) async {
|
||||
final value = await get(key);
|
||||
return value.fold(
|
||||
(l) => left(l),
|
||||
(r) => right(formatter(r)),
|
||||
() => none(),
|
||||
(s) => Some(formatter(s)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -67,38 +64,3 @@ class DartKeyValue implements KeyValueStorage {
|
||||
_sharedPreferences ??= await SharedPreferences.getInstance();
|
||||
}
|
||||
}
|
||||
|
||||
/// Key-value store
|
||||
/// The data is stored in the local storage of the device.
|
||||
class RustKeyValue {
|
||||
static Future<void> set(String key, String value) async {
|
||||
await ConfigEventSetKeyValue(
|
||||
KeyValuePB.create()
|
||||
..key = key
|
||||
..value = value,
|
||||
).send();
|
||||
}
|
||||
|
||||
static Future<Either<FlowyError, String>> get(String key) async {
|
||||
final payload = KeyPB.create()..key = key;
|
||||
final response = await ConfigEventGetKeyValue(payload).send();
|
||||
return response.swap().map((r) => r.value);
|
||||
}
|
||||
|
||||
static Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
) async {
|
||||
final value = await get(key);
|
||||
return value.fold(
|
||||
(l) => left(l),
|
||||
(r) => right(formatter(r)),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> remove(String key) async {
|
||||
await ConfigEventRemoveKeyValue(
|
||||
KeyPB.create()..key = key,
|
||||
).send();
|
||||
}
|
||||
}
|
||||
|
@ -42,4 +42,9 @@ class KVKeys {
|
||||
/// The value is a boolean string.
|
||||
static const String showRenameDialogWhenCreatingNewFile =
|
||||
'showRenameDialogWhenCreatingNewFile';
|
||||
|
||||
static const String kCloudType = 'kCloudType';
|
||||
static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL';
|
||||
static const String kSupabaseURL = 'kSupbaseURL';
|
||||
static const String kSupabaseAnonKey = 'kSupabaseAnonKey';
|
||||
}
|
||||
|
@ -49,6 +49,10 @@ class SupabaseConfiguration {
|
||||
anon_key: '',
|
||||
);
|
||||
}
|
||||
|
||||
bool get isValid {
|
||||
return url.isNotEmpty && anon_key.isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
@ -75,4 +79,10 @@ class AppFlowyCloudConfiguration {
|
||||
gotrue_url: '',
|
||||
);
|
||||
}
|
||||
|
||||
bool get isValid {
|
||||
return base_url.isNotEmpty &&
|
||||
ws_base_url.isNotEmpty &&
|
||||
gotrue_url.isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
300
frontend/appflowy_flutter/lib/env/env.dart
vendored
300
frontend/appflowy_flutter/lib/env/env.dart
vendored
@ -1,76 +1,104 @@
|
||||
// lib/env/env.dart
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/env/backend_env.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:envied/envied.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
part 'env.g.dart';
|
||||
|
||||
/// The environment variables are defined in `.env` file that is located in the
|
||||
/// appflowy_flutter.
|
||||
/// Run `dart run build_runner build --delete-conflicting-outputs`
|
||||
/// to generate the keys from the env file.
|
||||
/// Sets the cloud type for the application.
|
||||
///
|
||||
/// If you want to regenerate the keys, you need to run `dart run
|
||||
/// build_runner clean` before running `dart run build_runner build
|
||||
/// --delete-conflicting-outputs`.
|
||||
|
||||
/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.
|
||||
/// This method updates the cloud type setting in the key-value storage
|
||||
/// using the [KeyValueStorage] service. The cloud type is identified
|
||||
/// by the [CloudType] enum.
|
||||
///
|
||||
@Envied(path: '.env')
|
||||
abstract class Env {
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'CLOUD_TYPE',
|
||||
defaultValue: '0',
|
||||
)
|
||||
static final int cloudType = _Env.cloudType;
|
||||
|
||||
/// AppFlowy Cloud Configuration
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_BASE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudBaseUrl = _Env.afCloudBaseUrl;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_WS_BASE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudWSBaseUrl = _Env.afCloudWSBaseUrl;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_GOTRUE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudGoTrueUrl = _Env.afCloudGoTrueUrl;
|
||||
|
||||
// Supabase Configuration:
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseUrl = _Env.supabaseUrl;
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_ANON_KEY',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseAnonKey = _Env.supabaseAnonKey;
|
||||
/// [ty] - The type of cloud to be set. It must be one of the values from
|
||||
/// [CloudType] enum. The corresponding integer value of the enum is stored:
|
||||
/// - `CloudType.local` is stored as "0".
|
||||
/// - `CloudType.supabase` is stored as "1".
|
||||
/// - `CloudType.appflowyCloud` is stored as "2".
|
||||
Future<void> setCloudType(CloudType ty) async {
|
||||
switch (ty) {
|
||||
case CloudType.local:
|
||||
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
|
||||
break;
|
||||
case CloudType.supabase:
|
||||
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 1.toString());
|
||||
break;
|
||||
case CloudType.appflowyCloud:
|
||||
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isCloudEnabled {
|
||||
/// Retrieves the currently set cloud type.
|
||||
///
|
||||
/// This method fetches the cloud type setting from the key-value storage
|
||||
/// using the [KeyValueStorage] service and returns the corresponding
|
||||
/// [CloudType] enum value.
|
||||
///
|
||||
/// Returns:
|
||||
/// A Future that resolves to a [CloudType] enum value representing the
|
||||
/// currently set cloud type. The default return value is `CloudType.local`
|
||||
/// if no valid setting is found.
|
||||
///
|
||||
Future<CloudType> getCloudType() async {
|
||||
final value = await getIt<KeyValueStorage>().get(KVKeys.kCloudType);
|
||||
return value.fold(() => CloudType.local, (s) {
|
||||
switch (s) {
|
||||
case "0":
|
||||
return CloudType.local;
|
||||
case "1":
|
||||
return CloudType.supabase;
|
||||
case "2":
|
||||
return CloudType.appflowyCloud;
|
||||
default:
|
||||
return CloudType.local;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Determines whether authentication is enabled.
|
||||
///
|
||||
/// This getter evaluates if authentication should be enabled based on the
|
||||
/// current integration mode and cloud type settings.
|
||||
///
|
||||
/// Returns:
|
||||
/// A boolean value indicating whether authentication is enabled. It returns
|
||||
/// `true` if the application is in release or develop mode, and the cloud type
|
||||
/// is not set to `CloudType.local`. Additionally, it checks if either the
|
||||
/// AppFlowy Cloud or Supabase configuration is valid.
|
||||
/// Returns `false` otherwise.
|
||||
bool get isAuthEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return currentCloudType().isEnabled;
|
||||
final env = getIt<AppFlowyCloudSharedEnv>();
|
||||
if (env.cloudType == CloudType.local) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (env.cloudType == CloudType.supabase) {
|
||||
return env.supabaseConfig.isValid;
|
||||
}
|
||||
|
||||
if (env.cloudType == CloudType.appflowyCloud) {
|
||||
return env.appflowyCloudConfig.isValid;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if Supabase is enabled.
|
||||
///
|
||||
/// This getter evaluates if Supabase should be enabled based on the
|
||||
/// current integration mode and cloud type setting.
|
||||
///
|
||||
/// Returns:
|
||||
/// A boolean value indicating whether Supabase is enabled. It returns `true`
|
||||
/// if the application is in release or develop mode and the current cloud type
|
||||
/// is `CloudType.supabase`. Otherwise, it returns `false`.
|
||||
bool get isSupabaseEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
@ -80,6 +108,15 @@ bool get isSupabaseEnabled {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines if AppFlowy Cloud is enabled.
|
||||
///
|
||||
/// This getter assesses if AppFlowy Cloud should be enabled based on the
|
||||
/// current integration mode and cloud type setting.
|
||||
///
|
||||
/// Returns:
|
||||
/// A boolean value indicating whether AppFlowy Cloud is enabled. It returns
|
||||
/// `true` if the application is in release or develop mode and the current
|
||||
/// cloud type is `CloudType.appflowyCloud`. Otherwise, it returns `false`.
|
||||
bool get isAppFlowyCloudEnabled {
|
||||
// Only enable appflowy cloud in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
@ -90,40 +127,125 @@ bool get isAppFlowyCloudEnabled {
|
||||
}
|
||||
|
||||
enum CloudType {
|
||||
unknown,
|
||||
local,
|
||||
supabase,
|
||||
appflowyCloud;
|
||||
|
||||
bool get isEnabled => this != CloudType.unknown;
|
||||
bool get isEnabled => this != CloudType.local;
|
||||
int get value {
|
||||
switch (this) {
|
||||
case CloudType.local:
|
||||
return 0;
|
||||
case CloudType.supabase:
|
||||
return 1;
|
||||
case CloudType.appflowyCloud:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloudType currentCloudType() {
|
||||
final value = Env.cloudType;
|
||||
if (value == 1) {
|
||||
if (Env.supabaseUrl.isEmpty || Env.supabaseAnonKey.isEmpty) {
|
||||
Log.error(
|
||||
"Supabase is not configured correctly. The values are: "
|
||||
"url: ${Env.supabaseUrl}, anonKey: ${Env.supabaseAnonKey}",
|
||||
);
|
||||
return CloudType.unknown;
|
||||
} else {
|
||||
return CloudType.supabase;
|
||||
}
|
||||
}
|
||||
|
||||
if (value == 2) {
|
||||
if (Env.afCloudBaseUrl.isEmpty ||
|
||||
Env.afCloudWSBaseUrl.isEmpty ||
|
||||
Env.afCloudGoTrueUrl.isEmpty) {
|
||||
Log.error(
|
||||
"AppFlowy cloud is not configured correctly. The values are: "
|
||||
"baseUrl: ${Env.afCloudBaseUrl}, wsBaseUrl: ${Env.afCloudWSBaseUrl}, gotrueUrl: ${Env.afCloudGoTrueUrl}",
|
||||
);
|
||||
return CloudType.unknown;
|
||||
} else {
|
||||
return CloudType.appflowyCloud;
|
||||
}
|
||||
}
|
||||
|
||||
return CloudType.unknown;
|
||||
return getIt<AppFlowyCloudSharedEnv>().cloudType;
|
||||
}
|
||||
|
||||
Future<void> setAppFlowyCloudBaseUrl(Option<String> url) async {
|
||||
await url.fold(
|
||||
() => getIt<KeyValueStorage>().remove(KVKeys.kAppflowyCloudBaseURL),
|
||||
(s) => getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, s),
|
||||
);
|
||||
}
|
||||
|
||||
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
|
||||
class AppFlowyCloudSharedEnv {
|
||||
final CloudType cloudType;
|
||||
final AppFlowyCloudConfiguration appflowyCloudConfig;
|
||||
final SupabaseConfiguration supabaseConfig;
|
||||
|
||||
AppFlowyCloudSharedEnv({
|
||||
required this.cloudType,
|
||||
required this.appflowyCloudConfig,
|
||||
required this.supabaseConfig,
|
||||
});
|
||||
}
|
||||
|
||||
Future<AppFlowyCloudConfiguration> getAppFlowyCloudConfig() async {
|
||||
return AppFlowyCloudConfiguration(
|
||||
base_url: await getAppFlowyCloudUrl(),
|
||||
ws_base_url: await _getAppFlowyCloudWSUrl(),
|
||||
gotrue_url: await _getAppFlowyCloudGotrueUrl(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> getAppFlowyCloudUrl() async {
|
||||
final result =
|
||||
await getIt<KeyValueStorage>().get(KVKeys.kAppflowyCloudBaseURL);
|
||||
return result.fold(
|
||||
() => "",
|
||||
(url) => url,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getAppFlowyCloudWSUrl() async {
|
||||
try {
|
||||
final serverUrl = await getAppFlowyCloudUrl();
|
||||
final uri = Uri.parse(serverUrl);
|
||||
|
||||
// Construct the WebSocket URL directly from the parsed URI.
|
||||
final wsScheme = uri.isScheme('HTTPS') ? 'wss' : 'ws';
|
||||
final wsUrl = Uri(scheme: wsScheme, host: uri.host, path: '/ws');
|
||||
|
||||
return wsUrl.toString();
|
||||
} catch (e) {
|
||||
Log.error("Failed to get WebSocket URL: $e");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getAppFlowyCloudGotrueUrl() async {
|
||||
final serverUrl = await getAppFlowyCloudUrl();
|
||||
return "$serverUrl/gotrue";
|
||||
}
|
||||
|
||||
Future<void> setSupbaseServer(
|
||||
Option<String> url,
|
||||
Option<String> anonKey,
|
||||
) async {
|
||||
assert(
|
||||
(url.isSome() && anonKey.isSome()) || (url.isNone() && anonKey.isNone()),
|
||||
"Either both Supabase URL and anon key must be set, or both should be unset",
|
||||
);
|
||||
|
||||
await url.fold(
|
||||
() => getIt<KeyValueStorage>().remove(KVKeys.kSupabaseURL),
|
||||
(s) => getIt<KeyValueStorage>().set(KVKeys.kSupabaseURL, s),
|
||||
);
|
||||
await anonKey.fold(
|
||||
() => getIt<KeyValueStorage>().remove(KVKeys.kSupabaseAnonKey),
|
||||
(s) => getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, s),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SupabaseConfiguration> getSupabaseCloudConfig() async {
|
||||
final url = await _getSupbaseUrl();
|
||||
final anonKey = await _getSupabaseAnonKey();
|
||||
return SupabaseConfiguration(
|
||||
url: url,
|
||||
anon_key: anonKey,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getSupbaseUrl() async {
|
||||
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseURL);
|
||||
return result.fold(
|
||||
() => "",
|
||||
(url) => url,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getSupabaseAnonKey() async {
|
||||
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseAnonKey);
|
||||
return result.fold(
|
||||
() => "",
|
||||
(url) => url,
|
||||
);
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: userName,
|
||||
subtitle: isCloudEnabled
|
||||
subtitle: isAuthEnabled
|
||||
? Text(
|
||||
userProfile.email,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
|
@ -43,6 +43,10 @@ class DependencyResolver {
|
||||
GetIt getIt,
|
||||
IntegrationMode mode,
|
||||
) async {
|
||||
// getIt.registerFactory<KeyValueStorage>(() => RustKeyValue());
|
||||
getIt.registerFactory<KeyValueStorage>(() => DartKeyValue());
|
||||
|
||||
await _resolveCloudDeps(getIt);
|
||||
_resolveUserDeps(getIt, mode);
|
||||
_resolveHomeDeps(getIt);
|
||||
_resolveFolderDeps(getIt);
|
||||
@ -52,12 +56,23 @@ class DependencyResolver {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resolveCloudDeps(GetIt getIt) async {
|
||||
final cloudType = await getCloudType();
|
||||
final appflowyCloudConfig = await getAppFlowyCloudConfig();
|
||||
final supabaseCloudConfig = await getSupabaseCloudConfig();
|
||||
getIt.registerFactory<AppFlowyCloudSharedEnv>(() {
|
||||
return AppFlowyCloudSharedEnv(
|
||||
cloudType: cloudType,
|
||||
appflowyCloudConfig: appflowyCloudConfig,
|
||||
supabaseConfig: supabaseCloudConfig,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _resolveCommonService(
|
||||
GetIt getIt,
|
||||
IntegrationMode mode,
|
||||
) async {
|
||||
// getIt.registerFactory<KeyValueStorage>(() => RustKeyValue());
|
||||
getIt.registerFactory<KeyValueStorage>(() => DartKeyValue());
|
||||
getIt.registerFactory<FilePickerService>(() => FilePicker());
|
||||
if (mode.isTest) {
|
||||
getIt.registerFactory<ApplicationDataStorage>(
|
||||
@ -115,7 +130,7 @@ void _resolveCommonService(
|
||||
|
||||
void _resolveUserDeps(GetIt getIt, IntegrationMode mode) {
|
||||
switch (currentCloudType()) {
|
||||
case CloudType.unknown:
|
||||
case CloudType.local:
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => BackendAuthService(
|
||||
AuthTypePB.Local,
|
||||
|
@ -45,7 +45,7 @@ class FlowyRunner {
|
||||
await getIt.reset();
|
||||
|
||||
// Specify the env
|
||||
initGetIt(getIt, mode, f, config);
|
||||
await initGetIt(getIt, mode, f, config);
|
||||
|
||||
final applicationDataDirectory =
|
||||
await getIt<ApplicationDataStorage>().getPath().then(
|
||||
|
@ -50,7 +50,7 @@ class WindowSizeManager {
|
||||
Future<Offset?> getPosition() async {
|
||||
final position = await getIt<KeyValueStorage>().get(KVKeys.windowPosition);
|
||||
return position.fold(
|
||||
(l) => null,
|
||||
() => null,
|
||||
(r) {
|
||||
final offset = json.decode(r);
|
||||
return Offset(offset[dx], offset[dy]);
|
||||
|
@ -45,41 +45,15 @@ AppFlowyConfiguration _getAppFlowyConfiguration(
|
||||
String originAppPath,
|
||||
String deviceId,
|
||||
) {
|
||||
if (isCloudEnabled) {
|
||||
final supabaseConfig = SupabaseConfiguration(
|
||||
url: Env.supabaseUrl,
|
||||
anon_key: Env.supabaseAnonKey,
|
||||
);
|
||||
|
||||
final appflowyCloudConfig = AppFlowyCloudConfiguration(
|
||||
base_url: Env.afCloudBaseUrl,
|
||||
ws_base_url: Env.afCloudWSBaseUrl,
|
||||
gotrue_url: Env.afCloudGoTrueUrl,
|
||||
);
|
||||
|
||||
return AppFlowyConfiguration(
|
||||
custom_app_path: customAppPath,
|
||||
origin_app_path: originAppPath,
|
||||
device_id: deviceId,
|
||||
cloud_type: Env.cloudType,
|
||||
supabase_config: supabaseConfig,
|
||||
appflowy_cloud_config: appflowyCloudConfig,
|
||||
);
|
||||
} else {
|
||||
// Use the default configuration if the cloud feature is disabled
|
||||
final supabaseConfig = SupabaseConfiguration.defaultConfig();
|
||||
final appflowyCloudConfig = AppFlowyCloudConfiguration.defaultConfig();
|
||||
|
||||
return AppFlowyConfiguration(
|
||||
custom_app_path: customAppPath,
|
||||
origin_app_path: originAppPath,
|
||||
device_id: deviceId,
|
||||
// 0 means the cloud type is local
|
||||
cloud_type: 0,
|
||||
supabase_config: supabaseConfig,
|
||||
appflowy_cloud_config: appflowyCloudConfig,
|
||||
);
|
||||
}
|
||||
final env = getIt<AppFlowyCloudSharedEnv>();
|
||||
return AppFlowyConfiguration(
|
||||
custom_app_path: customAppPath,
|
||||
origin_app_path: originAppPath,
|
||||
device_id: deviceId,
|
||||
cloud_type: env.cloudType.value,
|
||||
supabase_config: env.supabaseConfig,
|
||||
appflowy_cloud_config: env.appflowyCloudConfig,
|
||||
);
|
||||
}
|
||||
|
||||
/// The default directory to store the user data. The directory can be
|
||||
|
@ -36,8 +36,8 @@ class InitSupabaseTask extends LaunchTask {
|
||||
supabase?.dispose();
|
||||
supabase = null;
|
||||
final initializedSupabase = await Supabase.initialize(
|
||||
url: Env.supabaseUrl,
|
||||
anonKey: Env.supabaseAnonKey,
|
||||
url: getIt<AppFlowyCloudSharedEnv>().supabaseConfig.url,
|
||||
anonKey: getIt<AppFlowyCloudSharedEnv>().supabaseConfig.anon_key,
|
||||
debug: kDebugMode,
|
||||
localStorage: const SupabaseLocalStorage(),
|
||||
);
|
||||
|
@ -68,6 +68,7 @@ class AFCloudAuthService implements AuthService {
|
||||
final completer = Completer<Either<FlowyError, UserProfilePB>>();
|
||||
_deeplinkSubscription = _appLinks.uriLinkStream.listen(
|
||||
(Uri? uri) async {
|
||||
Log.info('onDeepLink: ${uri.toString()}');
|
||||
await _handleUri(uri, completer);
|
||||
},
|
||||
onError: (Object err, StackTrace stackTrace) {
|
||||
@ -108,6 +109,9 @@ class AFCloudAuthService implements AuthService {
|
||||
.then((value) => value.swap());
|
||||
_deeplinkSubscription?.cancel();
|
||||
completer.complete(result);
|
||||
} else {
|
||||
Log.error('onDeepLinkError: Unexpect deep link: ${uri.toString()}');
|
||||
completer.complete(left(AuthError.signInWithOauthError));
|
||||
}
|
||||
} else {
|
||||
Log.error('onDeepLinkError: Unexpect empty deep link callback');
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/core/frameless_window.dart';
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/user/presentation/widgets/widgets.dart';
|
||||
@ -46,9 +47,12 @@ class DesktopSignInScreen extends StatelessWidget {
|
||||
|
||||
// third-party sign in.
|
||||
const VSpace(20),
|
||||
const _OrDivider(),
|
||||
const VSpace(10),
|
||||
const ThirdPartySignInButtons(),
|
||||
|
||||
if (isAuthEnabled) ...[
|
||||
const _OrDivider(),
|
||||
const VSpace(10),
|
||||
const ThirdPartySignInButtons(),
|
||||
],
|
||||
const VSpace(20),
|
||||
// loading status
|
||||
const VSpace(indicatorMinHeight),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
@ -85,7 +86,7 @@ class MobileSignInScreen extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const VSpace(spacing),
|
||||
const ThirdPartySignInButtons(),
|
||||
if (isAuthEnabled) const ThirdPartySignInButtons(),
|
||||
const VSpace(spacing),
|
||||
],
|
||||
),
|
||||
|
@ -67,7 +67,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
||||
),
|
||||
const VSpace(32),
|
||||
SizedBox(
|
||||
width: size.width * 0.5,
|
||||
width: size.width * 0.7,
|
||||
child: FolderWidget(
|
||||
createFolderCallback: () async {
|
||||
_didCustomizeFolder = true;
|
||||
|
@ -95,10 +95,10 @@ class SplashScreen extends StatelessWidget {
|
||||
|
||||
void _handleUnauthenticated(BuildContext context, Unauthenticated result) {
|
||||
Log.trace(
|
||||
'_handleUnauthenticated -> cloud is enabled: $isCloudEnabled',
|
||||
'_handleUnauthenticated -> cloud is enabled: $isAuthEnabled',
|
||||
);
|
||||
// replace Splash screen as root page
|
||||
if (isCloudEnabled) {
|
||||
if (isAuthEnabled) {
|
||||
context.go(SignInScreen.routeName);
|
||||
} else {
|
||||
// if the env is not configured, we will skip to the 'skip login screen'.
|
||||
|
@ -0,0 +1,91 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/cloud_setting_listener.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
part 'appflowy_cloud_setting_bloc.freezed.dart';
|
||||
|
||||
class AppFlowyCloudSettingBloc
|
||||
extends Bloc<AppFlowyCloudSettingEvent, AppFlowyCloudSettingState> {
|
||||
final UserCloudConfigListener _listener;
|
||||
AppFlowyCloudSettingBloc(CloudSettingPB setting)
|
||||
: _listener = UserCloudConfigListener(),
|
||||
super(AppFlowyCloudSettingState.initial(setting)) {
|
||||
on<AppFlowyCloudSettingEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_listener.start(
|
||||
onSettingChanged: (result) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
result.fold(
|
||||
(setting) =>
|
||||
add(AppFlowyCloudSettingEvent.didReceiveSetting(setting)),
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
enableSync: (isEnable) async {
|
||||
final config = UpdateCloudConfigPB.create()..enableSync = isEnable;
|
||||
await UserEventSetCloudConfig(config).send();
|
||||
},
|
||||
didReceiveSetting: (CloudSettingPB setting) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
setting: setting,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AppFlowyCloudSettingEvent with _$AppFlowyCloudSettingEvent {
|
||||
const factory AppFlowyCloudSettingEvent.initial() = _Initial;
|
||||
const factory AppFlowyCloudSettingEvent.enableSync(bool isEnable) =
|
||||
_EnableSync;
|
||||
const factory AppFlowyCloudSettingEvent.didReceiveSetting(
|
||||
CloudSettingPB setting,
|
||||
) = _DidUpdateSetting;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AppFlowyCloudSettingState with _$AppFlowyCloudSettingState {
|
||||
const factory AppFlowyCloudSettingState({
|
||||
required CloudSettingPB setting,
|
||||
}) = _AppFlowyCloudSettingState;
|
||||
|
||||
factory AppFlowyCloudSettingState.initial(CloudSettingPB setting) =>
|
||||
AppFlowyCloudSettingState(
|
||||
setting: setting,
|
||||
);
|
||||
}
|
||||
|
||||
Either<String, ()> validateUrl(String url) {
|
||||
try {
|
||||
// Use Uri.parse to validate the url.
|
||||
final uri = Uri.parse(url);
|
||||
if (uri.isScheme('HTTP') || uri.isScheme('HTTPS')) {
|
||||
return right(());
|
||||
} else {
|
||||
return left(LocaleKeys.settings_menu_invalidCloudURLScheme.tr());
|
||||
}
|
||||
} catch (e) {
|
||||
return left(e.toString());
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
import 'package:appflowy/env/backend_env.dart';
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
part 'appflowy_cloud_urls_bloc.freezed.dart';
|
||||
|
||||
class AppFlowyCloudURLsBloc
|
||||
extends Bloc<AppFlowyCloudURLsEvent, AppFlowyCloudURLsState> {
|
||||
AppFlowyCloudURLsBloc() : super(AppFlowyCloudURLsState.initial()) {
|
||||
on<AppFlowyCloudURLsEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {},
|
||||
updateServerUrl: (url) {
|
||||
emit(state.copyWith(updatedServerUrl: url));
|
||||
},
|
||||
confirmUpdate: () async {
|
||||
if (state.updatedServerUrl.isEmpty) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
updatedServerUrl: "",
|
||||
urlError: none(),
|
||||
restartApp: true,
|
||||
),
|
||||
);
|
||||
await setAppFlowyCloudBaseUrl(none());
|
||||
} else {
|
||||
validateUrl(state.updatedServerUrl).fold(
|
||||
(error) => emit(state.copyWith(urlError: Some(error))),
|
||||
(_) async {
|
||||
if (state.config.base_url != state.updatedServerUrl) {
|
||||
await setAppFlowyCloudBaseUrl(Some(state.updatedServerUrl));
|
||||
add(const AppFlowyCloudURLsEvent.didSaveConfig());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
didSaveConfig: () {
|
||||
emit(
|
||||
state.copyWith(
|
||||
urlError: none(),
|
||||
restartApp: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AppFlowyCloudURLsEvent with _$AppFlowyCloudURLsEvent {
|
||||
const factory AppFlowyCloudURLsEvent.initial() = _Initial;
|
||||
const factory AppFlowyCloudURLsEvent.updateServerUrl(String text) =
|
||||
_ServerUrl;
|
||||
const factory AppFlowyCloudURLsEvent.confirmUpdate() = _UpdateConfig;
|
||||
const factory AppFlowyCloudURLsEvent.didSaveConfig() = _DidSaveConfig;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AppFlowyCloudURLsState with _$AppFlowyCloudURLsState {
|
||||
const factory AppFlowyCloudURLsState({
|
||||
required AppFlowyCloudConfiguration config,
|
||||
required String updatedServerUrl,
|
||||
required Option<String> urlError,
|
||||
required bool restartApp,
|
||||
}) = _AppFlowyCloudURLsState;
|
||||
|
||||
factory AppFlowyCloudURLsState.initial() => AppFlowyCloudURLsState(
|
||||
config: getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig,
|
||||
urlError: none(),
|
||||
updatedServerUrl:
|
||||
getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url,
|
||||
restartApp: false,
|
||||
);
|
||||
}
|
||||
|
||||
Either<String, ()> validateUrl(String url) {
|
||||
try {
|
||||
// Use Uri.parse to validate the url.
|
||||
final uri = Uri.parse(url);
|
||||
if (uri.isScheme('HTTP') || uri.isScheme('HTTPS')) {
|
||||
return right(());
|
||||
} else {
|
||||
return left(LocaleKeys.settings_menu_invalidCloudURLScheme.tr());
|
||||
}
|
||||
} catch (e) {
|
||||
return left(e.toString());
|
||||
}
|
||||
}
|
@ -65,7 +65,7 @@ class ApplicationDataStorage {
|
||||
|
||||
final response = await getIt<KeyValueStorage>().get(KVKeys.pathLocation);
|
||||
String path = await response.fold(
|
||||
(error) async {
|
||||
() async {
|
||||
// return the default path if the path is not set
|
||||
final directory = await appFlowyApplicationDataDirectory();
|
||||
return directory.path;
|
||||
|
@ -0,0 +1,38 @@
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'cloud_setting_bloc.freezed.dart';
|
||||
|
||||
class CloudSettingBloc extends Bloc<CloudSettingEvent, CloudSettingState> {
|
||||
CloudSettingBloc(CloudType cloudType)
|
||||
: super(CloudSettingState.initial(cloudType)) {
|
||||
on<CloudSettingEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {},
|
||||
updateCloudType: (CloudType newCloudType) async {
|
||||
await setCloudType(newCloudType);
|
||||
emit(state.copyWith(cloudType: newCloudType));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CloudSettingEvent with _$CloudSettingEvent {
|
||||
const factory CloudSettingEvent.initial() = _Initial;
|
||||
const factory CloudSettingEvent.updateCloudType(CloudType newCloudType) =
|
||||
_UpdateCloudType;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CloudSettingState with _$CloudSettingState {
|
||||
const factory CloudSettingState({
|
||||
required CloudType cloudType,
|
||||
}) = _CloudSettingState;
|
||||
|
||||
factory CloudSettingState.initial(CloudType cloudType) => CloudSettingState(
|
||||
cloudType: cloudType,
|
||||
);
|
||||
}
|
@ -10,21 +10,18 @@ import 'package:dartz/dartz.dart';
|
||||
import '../../../core/notification/user_notification.dart';
|
||||
|
||||
class UserCloudConfigListener {
|
||||
final String userId;
|
||||
StreamSubscription<SubscribeObject>? _subscription;
|
||||
void Function(Either<CloudSettingPB, FlowyError>)? _onSettingChanged;
|
||||
|
||||
UserNotificationParser? _userParser;
|
||||
UserCloudConfigListener({
|
||||
required this.userId,
|
||||
});
|
||||
UserCloudConfigListener();
|
||||
|
||||
void start({
|
||||
void Function(Either<CloudSettingPB, FlowyError>)? onSettingChanged,
|
||||
}) {
|
||||
_onSettingChanged = onSettingChanged;
|
||||
_userParser = UserNotificationParser(
|
||||
id: userId,
|
||||
id: 'user_cloud_config',
|
||||
callback: _userNotificationCallback,
|
||||
);
|
||||
_subscription = RustStreamReceiver.listen((observable) {
|
||||
|
@ -22,7 +22,7 @@ class CreateFileSettingsCubit extends Cubit<bool> {
|
||||
(value) => bool.parse(value),
|
||||
);
|
||||
settingsOrFailure.fold(
|
||||
(_) => emit(false),
|
||||
() => emit(false),
|
||||
(settings) => emit(settings),
|
||||
);
|
||||
}
|
||||
|
@ -1,86 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/application/defines.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import 'cloud_setting_listener.dart';
|
||||
|
||||
part 'setting_supabase_bloc.freezed.dart';
|
||||
|
||||
class CloudSettingBloc extends Bloc<CloudSettingEvent, CloudSettingState> {
|
||||
final UserCloudConfigListener _listener;
|
||||
|
||||
CloudSettingBloc({
|
||||
required String userId,
|
||||
required CloudSettingPB config,
|
||||
}) : _listener = UserCloudConfigListener(userId: userId),
|
||||
super(CloudSettingState.initial(config)) {
|
||||
on<CloudSettingEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_listener.start(
|
||||
onSettingChanged: (result) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.fold(
|
||||
(config) => add(CloudSettingEvent.didReceiveConfig(config)),
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
enableSync: (bool enable) async {
|
||||
final update = UpdateCloudConfigPB.create()..enableSync = enable;
|
||||
updateCloudConfig(update);
|
||||
},
|
||||
didReceiveConfig: (CloudSettingPB config) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
config: config,
|
||||
loadingState: LoadingState.finish(left(unit)),
|
||||
),
|
||||
);
|
||||
},
|
||||
enableEncrypt: (bool enable) {
|
||||
final update = UpdateCloudConfigPB.create()..enableEncrypt = enable;
|
||||
updateCloudConfig(update);
|
||||
emit(state.copyWith(loadingState: const LoadingState.loading()));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateCloudConfig(UpdateCloudConfigPB config) async {
|
||||
await UserEventSetCloudConfig(config).send();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CloudSettingEvent with _$CloudSettingEvent {
|
||||
const factory CloudSettingEvent.initial() = _Initial;
|
||||
const factory CloudSettingEvent.didReceiveConfig(
|
||||
CloudSettingPB config,
|
||||
) = _DidSyncSupabaseConfig;
|
||||
const factory CloudSettingEvent.enableSync(bool enable) = _EnableSync;
|
||||
const factory CloudSettingEvent.enableEncrypt(bool enable) = _EnableEncrypt;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CloudSettingState with _$CloudSettingState {
|
||||
const factory CloudSettingState({
|
||||
required CloudSettingPB config,
|
||||
required Either<Unit, String> successOrFailure,
|
||||
required LoadingState loadingState,
|
||||
}) = _CloudSettingState;
|
||||
|
||||
factory CloudSettingState.initial(CloudSettingPB config) => CloudSettingState(
|
||||
config: config,
|
||||
successOrFailure: left(unit),
|
||||
loadingState: LoadingState.finish(left(unit)),
|
||||
);
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
import 'package:appflowy/env/backend_env.dart';
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/defines.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import 'cloud_setting_listener.dart';
|
||||
|
||||
part 'supabase_cloud_setting_bloc.freezed.dart';
|
||||
|
||||
class SupabaseCloudSettingBloc
|
||||
extends Bloc<SupabaseCloudSettingEvent, SupabaseCloudSettingState> {
|
||||
final UserCloudConfigListener _listener;
|
||||
|
||||
SupabaseCloudSettingBloc({
|
||||
required CloudSettingPB setting,
|
||||
}) : _listener = UserCloudConfigListener(),
|
||||
super(SupabaseCloudSettingState.initial(setting)) {
|
||||
on<SupabaseCloudSettingEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_listener.start(
|
||||
onSettingChanged: (result) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
result.fold(
|
||||
(setting) =>
|
||||
add(SupabaseCloudSettingEvent.didReceiveSetting(setting)),
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
enableSync: (bool enable) async {
|
||||
final update = UpdateCloudConfigPB.create()..enableSync = enable;
|
||||
updateCloudConfig(update);
|
||||
},
|
||||
didReceiveSetting: (CloudSettingPB setting) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
setting: setting,
|
||||
loadingState: LoadingState.finish(left(unit)),
|
||||
),
|
||||
);
|
||||
},
|
||||
enableEncrypt: (bool enable) {
|
||||
final update = UpdateCloudConfigPB.create()..enableEncrypt = enable;
|
||||
updateCloudConfig(update);
|
||||
emit(state.copyWith(loadingState: const LoadingState.loading()));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateCloudConfig(UpdateCloudConfigPB setting) async {
|
||||
await UserEventSetCloudConfig(setting).send();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SupabaseCloudSettingEvent with _$SupabaseCloudSettingEvent {
|
||||
const factory SupabaseCloudSettingEvent.initial() = _Initial;
|
||||
const factory SupabaseCloudSettingEvent.didReceiveSetting(
|
||||
CloudSettingPB setting,
|
||||
) = _DidSyncSupabaseConfig;
|
||||
const factory SupabaseCloudSettingEvent.enableSync(bool enable) = _EnableSync;
|
||||
const factory SupabaseCloudSettingEvent.enableEncrypt(bool enable) =
|
||||
_EnableEncrypt;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SupabaseCloudSettingState with _$SupabaseCloudSettingState {
|
||||
const factory SupabaseCloudSettingState({
|
||||
required LoadingState loadingState,
|
||||
required SupabaseConfiguration config,
|
||||
required CloudSettingPB setting,
|
||||
}) = _SupabaseCloudSettingState;
|
||||
|
||||
factory SupabaseCloudSettingState.initial(CloudSettingPB setting) =>
|
||||
SupabaseCloudSettingState(
|
||||
loadingState: LoadingState.finish(left(unit)),
|
||||
setting: setting,
|
||||
config: getIt<AppFlowyCloudSharedEnv>().supabaseConfig,
|
||||
);
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
import 'package:appflowy/env/backend_env.dart';
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import 'appflowy_cloud_setting_bloc.dart';
|
||||
|
||||
part 'supabase_cloud_urls_bloc.freezed.dart';
|
||||
|
||||
class SupabaseCloudURLsBloc
|
||||
extends Bloc<SupabaseCloudURLsEvent, SupabaseCloudURLsState> {
|
||||
SupabaseCloudURLsBloc() : super(SupabaseCloudURLsState.initial()) {
|
||||
on<SupabaseCloudURLsEvent>((event, emit) async {
|
||||
await event.when(
|
||||
updateUrl: (String url) {
|
||||
emit(state.copyWith(updatedUrl: url));
|
||||
},
|
||||
updateAnonKey: (String anonKey) {
|
||||
emit(state.copyWith(upatedAnonKey: anonKey));
|
||||
},
|
||||
confirmUpdate: () async {
|
||||
if (state.updatedUrl.isEmpty) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
urlError: none(),
|
||||
anonKeyError: none(),
|
||||
restartApp: true,
|
||||
),
|
||||
);
|
||||
await setSupbaseServer(none(), none());
|
||||
} else {
|
||||
// The anon key can't be empty if the url is not empty.
|
||||
if (state.upatedAnonKey.isEmpty) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
urlError: none(),
|
||||
anonKeyError: some(
|
||||
LocaleKeys.settings_menu_cloudSupabaseAnonKeyCanNotBeEmpty
|
||||
.tr(),
|
||||
),
|
||||
restartApp: false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
validateUrl(state.updatedUrl).fold(
|
||||
(error) => emit(state.copyWith(urlError: Some(error))),
|
||||
(_) async {
|
||||
await setSupbaseServer(
|
||||
Some(state.updatedUrl),
|
||||
Some(state.upatedAnonKey),
|
||||
);
|
||||
|
||||
add(const SupabaseCloudURLsEvent.didSaveConfig());
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
didSaveConfig: () {
|
||||
emit(
|
||||
state.copyWith(
|
||||
urlError: none(),
|
||||
anonKeyError: none(),
|
||||
restartApp: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateCloudConfig(UpdateCloudConfigPB setting) async {
|
||||
await UserEventSetCloudConfig(setting).send();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SupabaseCloudURLsEvent with _$SupabaseCloudURLsEvent {
|
||||
const factory SupabaseCloudURLsEvent.updateUrl(String text) = _UpdateUrl;
|
||||
const factory SupabaseCloudURLsEvent.updateAnonKey(String text) =
|
||||
_UpdateAnonKey;
|
||||
const factory SupabaseCloudURLsEvent.confirmUpdate() = _UpdateConfig;
|
||||
const factory SupabaseCloudURLsEvent.didSaveConfig() = _DidSaveConfig;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SupabaseCloudURLsState with _$SupabaseCloudURLsState {
|
||||
const factory SupabaseCloudURLsState({
|
||||
required SupabaseConfiguration config,
|
||||
required String updatedUrl,
|
||||
required String upatedAnonKey,
|
||||
required Option<String> urlError,
|
||||
required Option<String> anonKeyError,
|
||||
required bool restartApp,
|
||||
}) = _SupabaseCloudURLsState;
|
||||
|
||||
factory SupabaseCloudURLsState.initial() {
|
||||
final config = getIt<AppFlowyCloudSharedEnv>().supabaseConfig;
|
||||
return SupabaseCloudURLsState(
|
||||
updatedUrl: config.url,
|
||||
upatedAnonKey: config.anon_key,
|
||||
urlError: none(),
|
||||
anonKeyError: none(),
|
||||
restartApp: false,
|
||||
config: config,
|
||||
);
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ class FolderBloc extends Bloc<FolderEvent, FolderState> {
|
||||
Future<void> _setFolderExpandStatus(bool isExpanded) async {
|
||||
final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);
|
||||
final map = result.fold(
|
||||
(l) => {},
|
||||
() => {},
|
||||
(r) => jsonDecode(r),
|
||||
);
|
||||
if (isExpanded) {
|
||||
@ -50,7 +50,7 @@ class FolderBloc extends Bloc<FolderEvent, FolderState> {
|
||||
|
||||
Future<bool> _getFolderExpandStatus() async {
|
||||
return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
|
||||
return result.fold((l) => true, (r) {
|
||||
return result.fold(() => true, (r) {
|
||||
final map = jsonDecode(r);
|
||||
return map[state.type.name] ?? true;
|
||||
});
|
||||
|
@ -179,7 +179,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
Future<void> _setViewIsExpanded(ViewPB view, bool isExpanded) async {
|
||||
final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);
|
||||
final map = result.fold(
|
||||
(l) => {},
|
||||
() => {},
|
||||
(r) => jsonDecode(r),
|
||||
);
|
||||
if (isExpanded) {
|
||||
@ -192,7 +192,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
|
||||
Future<bool> _getViewIsExpanded(ViewPB view) {
|
||||
return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
|
||||
return result.fold((l) => false, (r) {
|
||||
return result.fold(() => false, (r) {
|
||||
final map = jsonDecode(r);
|
||||
return map[view.id] ?? false;
|
||||
});
|
||||
|
@ -21,7 +21,7 @@ Future<void> createViewAndShowRenameDialogIfNeeded(
|
||||
KVKeys.showRenameDialogWhenCreatingNewFile,
|
||||
(value) => bool.parse(value),
|
||||
);
|
||||
final showRenameDialog = value.fold((l) => false, (r) => r);
|
||||
final showRenameDialog = value.fold(() => false, (r) => r);
|
||||
if (context.mounted && showRenameDialog) {
|
||||
NavigatorTextFieldDialog(
|
||||
title: dialogTitle,
|
||||
|
@ -89,7 +89,7 @@ class SidebarUser extends StatelessWidget {
|
||||
Log.warn("Can't pop dialog context");
|
||||
}
|
||||
},
|
||||
didOpenUser: () async {
|
||||
restartApp: () async {
|
||||
// Pop the dialog using the dialog context
|
||||
Navigator.of(dialogContext).pop();
|
||||
await runAppFlowy();
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart';
|
||||
@ -14,6 +13,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'widgets/setting_cloud.dart';
|
||||
|
||||
const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12);
|
||||
const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0);
|
||||
@ -21,13 +21,13 @@ const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0);
|
||||
class SettingsDialog extends StatelessWidget {
|
||||
final VoidCallback dismissDialog;
|
||||
final VoidCallback didLogout;
|
||||
final VoidCallback didOpenUser;
|
||||
final VoidCallback restartApp;
|
||||
final UserProfilePB user;
|
||||
SettingsDialog(
|
||||
this.user, {
|
||||
required this.dismissDialog,
|
||||
required this.didLogout,
|
||||
required this.didOpenUser,
|
||||
required this.restartApp,
|
||||
Key? key,
|
||||
}) : super(key: ValueKey(user.id));
|
||||
|
||||
@ -100,12 +100,14 @@ class SettingsDialog extends StatelessWidget {
|
||||
user,
|
||||
didLogin: () => dismissDialog(),
|
||||
didLogout: didLogout,
|
||||
didOpenUser: didOpenUser,
|
||||
didOpenUser: restartApp,
|
||||
);
|
||||
case SettingsPage.notifications:
|
||||
return const SettingsNotificationsView();
|
||||
case SettingsPage.cloud:
|
||||
return SettingCloudView(userId: user.id.toString());
|
||||
return SettingCloud(
|
||||
didResetServerUrl: () => restartApp(),
|
||||
);
|
||||
case SettingsPage.shortcuts:
|
||||
return const SettingsCustomizeShortcutsWrapper();
|
||||
default:
|
||||
|
@ -0,0 +1,259 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appflowy_cloud_setting_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.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/user_setting.pb.dart';
|
||||
import 'package:dartz/dartz.dart' show Either;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SettingAppFlowyCloudView extends StatelessWidget {
|
||||
final VoidCallback didResetServerUrl;
|
||||
const SettingAppFlowyCloudView({required this.didResetServerUrl, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Either<CloudSettingPB, FlowyError>>(
|
||||
future: UserEventGetCloudConfig().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data != null &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
return snapshot.data!.fold(
|
||||
(setting) => _renderContent(setting),
|
||||
(err) => FlowyErrorPage.message(err.toString(), howToFix: ""),
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
BlocProvider<AppFlowyCloudSettingBloc> _renderContent(
|
||||
CloudSettingPB setting,
|
||||
) {
|
||||
return BlocProvider(
|
||||
create: (context) => AppFlowyCloudSettingBloc(setting)
|
||||
..add(const AppFlowyCloudSettingEvent.initial()),
|
||||
child: Column(
|
||||
children: [
|
||||
const AppFlowyCloudEnableSync(),
|
||||
const VSpace(40),
|
||||
AppFlowyCloudURLs(didUpdateUrls: () => didResetServerUrl()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppFlowyCloudURLs extends StatelessWidget {
|
||||
final VoidCallback didUpdateUrls;
|
||||
const AppFlowyCloudURLs({
|
||||
required this.didUpdateUrls,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
AppFlowyCloudURLsBloc()..add(const AppFlowyCloudURLsEvent.initial()),
|
||||
child: BlocListener<AppFlowyCloudURLsBloc, AppFlowyCloudURLsState>(
|
||||
listener: (context, state) {
|
||||
if (state.restartApp) {
|
||||
didUpdateUrls();
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<AppFlowyCloudURLsBloc, AppFlowyCloudURLsState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
const AppFlowySelfhostTip(),
|
||||
CloudURLInput(
|
||||
title: LocaleKeys.settings_menu_cloudURL.tr(),
|
||||
url: state.config.base_url,
|
||||
hint: LocaleKeys.settings_menu_cloudURLHint.tr(),
|
||||
onChanged: (text) {
|
||||
context.read<AppFlowyCloudURLsBloc>().add(
|
||||
AppFlowyCloudURLsEvent.updateServerUrl(
|
||||
text,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const VSpace(20),
|
||||
FlowyButton(
|
||||
isSelected: true,
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 30,
|
||||
vertical: 10,
|
||||
),
|
||||
text: FlowyText(
|
||||
LocaleKeys.settings_menu_restartApp.tr(),
|
||||
),
|
||||
onTap: () {
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.settings_menu_restartAppTip.tr(),
|
||||
confirm: () => context.read<AppFlowyCloudURLsBloc>().add(
|
||||
const AppFlowyCloudURLsEvent.confirmUpdate(),
|
||||
),
|
||||
).show(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppFlowySelfhostTip extends StatelessWidget {
|
||||
final url =
|
||||
"https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy#build-appflowy-with-a-self-hosted-server";
|
||||
const AppFlowySelfhostTip({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: 0.6,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: LocaleKeys.settings_menu_selfHostStart.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!,
|
||||
),
|
||||
TextSpan(
|
||||
text: " ${LocaleKeys.settings_menu_selfHostContent.tr()} ",
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: FontSizes.s14,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()..onTap = () => _launchURL(),
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.settings_menu_selfHostEnd.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _launchURL() async {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
Log.error("Could not launch $url");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class CloudURLInput extends StatefulWidget {
|
||||
final String title;
|
||||
final String url;
|
||||
final String hint;
|
||||
|
||||
final Function(String) onChanged;
|
||||
|
||||
const CloudURLInput({
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.hint,
|
||||
required this.onChanged,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
CloudURLInputState createState() => CloudURLInputState();
|
||||
}
|
||||
|
||||
class CloudURLInputState extends State<CloudURLInput> {
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.url);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: _controller,
|
||||
style: const TextStyle(fontSize: 12.0),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 6),
|
||||
labelText: widget.title,
|
||||
labelStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontWeight: FontWeight.w400, fontSize: 16),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.onBackground),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
hintText: widget.hint,
|
||||
errorText: context
|
||||
.read<AppFlowyCloudURLsBloc>()
|
||||
.state
|
||||
.urlError
|
||||
.fold(() => null, (error) => error),
|
||||
),
|
||||
onChanged: widget.onChanged,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class AppFlowyCloudEnableSync extends StatelessWidget {
|
||||
const AppFlowyCloudEnableSync({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AppFlowyCloudSettingBloc, AppFlowyCloudSettingState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
onChanged: (bool value) {
|
||||
context.read<AppFlowyCloudSettingBloc>().add(
|
||||
AppFlowyCloudSettingEvent.enableSync(value),
|
||||
);
|
||||
},
|
||||
value: state.setting.enableSync,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/cloud_setting_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_local_cloud.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'setting_appflowy_cloud.dart';
|
||||
import 'setting_supabase_cloud.dart';
|
||||
|
||||
class SettingCloud extends StatelessWidget {
|
||||
final VoidCallback didResetServerUrl;
|
||||
const SettingCloud({required this.didResetServerUrl, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: getCloudType(),
|
||||
builder: (BuildContext context, AsyncSnapshot<CloudType> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final cloudType = snapshot.data!;
|
||||
return BlocProvider(
|
||||
create: (context) => CloudSettingBloc(cloudType),
|
||||
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.settings_menu_cloudServerType.tr(),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message:
|
||||
LocaleKeys.settings_menu_cloudServerTypeTip.tr(),
|
||||
child: CloudTypeSwitcher(
|
||||
cloudType: state.cloudType,
|
||||
onSelected: (newCloudType) {
|
||||
context.read<CloudSettingBloc>().add(
|
||||
CloudSettingEvent.updateCloudType(
|
||||
newCloudType,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_viewFromCloudType(state.cloudType),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _viewFromCloudType(CloudType cloudType) {
|
||||
switch (cloudType) {
|
||||
case CloudType.local:
|
||||
return SettingLocalCloud(didResetServerUrl: didResetServerUrl);
|
||||
case CloudType.supabase:
|
||||
return SettingSupabaseCloudView(
|
||||
didResetServerUrl: didResetServerUrl,
|
||||
);
|
||||
case CloudType.appflowyCloud:
|
||||
return SettingAppFlowyCloudView(
|
||||
didResetServerUrl: didResetServerUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CloudTypeSwitcher extends StatelessWidget {
|
||||
final CloudType cloudType;
|
||||
final Function(CloudType) onSelected;
|
||||
const CloudTypeSwitcher({
|
||||
required this.cloudType,
|
||||
required this.onSelected,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
child: FlowyTextButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
|
||||
titleFromCloudType(cloudType),
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return CloudTypeItem(
|
||||
cloudType: CloudType.values[index],
|
||||
currentCloudtype: cloudType,
|
||||
onSelected: onSelected,
|
||||
);
|
||||
},
|
||||
itemCount: CloudType.values.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CloudTypeItem extends StatelessWidget {
|
||||
final CloudType cloudType;
|
||||
final CloudType currentCloudtype;
|
||||
final Function(CloudType) onSelected;
|
||||
|
||||
const CloudTypeItem({
|
||||
required this.cloudType,
|
||||
required this.currentCloudtype,
|
||||
required this.onSelected,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
titleFromCloudType(cloudType),
|
||||
),
|
||||
rightIcon: currentCloudtype == cloudType
|
||||
? const FlowySvg(FlowySvgs.check_s)
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentCloudtype != cloudType) {
|
||||
onSelected(cloudType);
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String titleFromCloudType(CloudType cloudType) {
|
||||
switch (cloudType) {
|
||||
case CloudType.local:
|
||||
return LocaleKeys.settings_menu_cloudLocal.tr();
|
||||
case CloudType.supabase:
|
||||
return LocaleKeys.settings_menu_cloudSupabase.tr();
|
||||
case CloudType.appflowyCloud:
|
||||
return LocaleKeys.settings_menu_cloudAppFlowy.tr();
|
||||
}
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/setting_supabase_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
|
||||
import 'package:dartz/dartz.dart' show Either;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingCloudView extends StatelessWidget {
|
||||
final String userId;
|
||||
const SettingCloudView({required this.userId, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Either<CloudSettingPB, FlowyError>>(
|
||||
future: UserEventGetCloudConfig().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data != null &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
return snapshot.data!.fold(
|
||||
(config) {
|
||||
return BlocProvider(
|
||||
create: (context) => CloudSettingBloc(
|
||||
userId: userId,
|
||||
config: config,
|
||||
)..add(const CloudSettingEvent.initial()),
|
||||
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
const EnableSync(),
|
||||
// Currently the appflowy cloud is not support end-to-end encryption.
|
||||
if (!isAppFlowyCloudEnabled) const EnableEncrypt(),
|
||||
if (isAppFlowyCloudEnabled)
|
||||
AppFlowyCloudInformationWidget(
|
||||
url: state.config.serverUrl,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
return FlowyErrorPage.message(err.toString(), howToFix: "");
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppFlowyCloudInformationWidget extends StatelessWidget {
|
||||
final String url;
|
||||
|
||||
const AppFlowyCloudInformationWidget({required this.url, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
// Wrap the Opacity widget with Expanded
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: FlowyText(
|
||||
"${LocaleKeys.settings_menu_cloudURL.tr()}: $url",
|
||||
maxLines: null, // Allow the text to wrap
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EnableEncrypt extends StatelessWidget {
|
||||
const EnableEncrypt({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
||||
builder: (context, state) {
|
||||
final indicator = state.loadingState.when(
|
||||
loading: () => const CircularProgressIndicator.adaptive(),
|
||||
finish: (successOrFail) => const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FlowyText.medium(LocaleKeys.settings_menu_enableEncrypt.tr()),
|
||||
const Spacer(),
|
||||
indicator,
|
||||
const HSpace(3),
|
||||
Switch(
|
||||
onChanged: state.config.enableEncrypt
|
||||
? null
|
||||
: (bool value) {
|
||||
context
|
||||
.read<CloudSettingBloc>()
|
||||
.add(CloudSettingEvent.enableEncrypt(value));
|
||||
},
|
||||
value: state.config.enableEncrypt,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IntrinsicHeight(
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.settings_menu_enableEncryptPrompt.tr(),
|
||||
maxLines: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(6),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.settings_menu_clickToCopySecret.tr(),
|
||||
child: FlowyButton(
|
||||
disable: !(state.config.enableEncrypt),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: Corners.s5Border,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
text: FlowyText.medium(state.config.encryptSecret),
|
||||
onTap: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: state.config.encryptSecret),
|
||||
);
|
||||
showMessageToast(LocaleKeys.message_copy_success.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EnableSync extends StatelessWidget {
|
||||
const EnableSync({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
onChanged: (bool value) {
|
||||
context.read<CloudSettingBloc>().add(
|
||||
CloudSettingEvent.enableSync(value),
|
||||
);
|
||||
},
|
||||
value: state.config.enableSync,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingLocalCloud extends StatelessWidget {
|
||||
final VoidCallback didResetServerUrl;
|
||||
const SettingLocalCloud({
|
||||
required this.didResetServerUrl,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyButton(
|
||||
isSelected: true,
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 30,
|
||||
vertical: 10,
|
||||
),
|
||||
text: FlowyText(
|
||||
LocaleKeys.settings_menu_restartApp.tr(),
|
||||
),
|
||||
onTap: () {
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.settings_menu_restartAppTip.tr(),
|
||||
confirm: didResetServerUrl,
|
||||
).show(context);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,356 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/supabase_cloud_setting_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/supabase_cloud_urls_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.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/user_setting.pb.dart';
|
||||
import 'package:dartz/dartz.dart' show Either;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SettingSupabaseCloudView extends StatelessWidget {
|
||||
final VoidCallback didResetServerUrl;
|
||||
const SettingSupabaseCloudView({required this.didResetServerUrl, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Either<CloudSettingPB, FlowyError>>(
|
||||
future: UserEventGetCloudConfig().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data != null &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
return snapshot.data!.fold(
|
||||
(setting) {
|
||||
return BlocProvider(
|
||||
create: (context) => SupabaseCloudSettingBloc(
|
||||
setting: setting,
|
||||
)..add(const SupabaseCloudSettingEvent.initial()),
|
||||
child: Column(
|
||||
children: [
|
||||
BlocBuilder<SupabaseCloudSettingBloc,
|
||||
SupabaseCloudSettingState>(
|
||||
builder: (context, state) {
|
||||
return const Column(
|
||||
children: [
|
||||
SupabaseEnableSync(),
|
||||
EnableEncrypt(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const VSpace(40),
|
||||
const SupabaseSelfhostTip(),
|
||||
SupabaseCloudURLs(
|
||||
didUpdateUrls: didResetServerUrl,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
return FlowyErrorPage.message(err.toString(), howToFix: "");
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SupabaseCloudURLs extends StatelessWidget {
|
||||
final VoidCallback didUpdateUrls;
|
||||
const SupabaseCloudURLs({
|
||||
required this.didUpdateUrls,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SupabaseCloudURLsBloc(),
|
||||
child: BlocListener<SupabaseCloudURLsBloc, SupabaseCloudURLsState>(
|
||||
listener: (context, state) {
|
||||
if (state.restartApp) {
|
||||
didUpdateUrls();
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<SupabaseCloudURLsBloc, SupabaseCloudURLsState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
SupabaseInput(
|
||||
title: LocaleKeys.settings_menu_cloudSupabaseUrl.tr(),
|
||||
url: state.config.url,
|
||||
hint: LocaleKeys.settings_menu_cloudURLHint.tr(),
|
||||
onChanged: (text) {
|
||||
context
|
||||
.read<SupabaseCloudURLsBloc>()
|
||||
.add(SupabaseCloudURLsEvent.updateUrl(text));
|
||||
},
|
||||
error: state.urlError.fold(() => null, (a) => a),
|
||||
),
|
||||
SupabaseInput(
|
||||
title: LocaleKeys.settings_menu_cloudSupabaseAnonKey.tr(),
|
||||
url: state.config.anon_key,
|
||||
hint: LocaleKeys.settings_menu_cloudURLHint.tr(),
|
||||
onChanged: (text) {
|
||||
context
|
||||
.read<SupabaseCloudURLsBloc>()
|
||||
.add(SupabaseCloudURLsEvent.updateAnonKey(text));
|
||||
},
|
||||
error: state.anonKeyError.fold(() => null, (a) => a),
|
||||
),
|
||||
const VSpace(20),
|
||||
FlowyButton(
|
||||
isSelected: true,
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 30,
|
||||
vertical: 10,
|
||||
),
|
||||
text: FlowyText(
|
||||
LocaleKeys.settings_menu_restartApp.tr(),
|
||||
),
|
||||
onTap: () {
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.settings_menu_restartAppTip.tr(),
|
||||
confirm: () => context
|
||||
.read<SupabaseCloudURLsBloc>()
|
||||
.add(const SupabaseCloudURLsEvent.confirmUpdate()),
|
||||
).show(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EnableEncrypt extends StatelessWidget {
|
||||
const EnableEncrypt({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SupabaseCloudSettingBloc, SupabaseCloudSettingState>(
|
||||
builder: (context, state) {
|
||||
final indicator = state.loadingState.when(
|
||||
loading: () => const CircularProgressIndicator.adaptive(),
|
||||
finish: (successOrFail) => const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FlowyText.medium(LocaleKeys.settings_menu_enableEncrypt.tr()),
|
||||
const Spacer(),
|
||||
indicator,
|
||||
const HSpace(3),
|
||||
Switch(
|
||||
onChanged: state.setting.enableEncrypt
|
||||
? null
|
||||
: (bool value) {
|
||||
context.read<SupabaseCloudSettingBloc>().add(
|
||||
SupabaseCloudSettingEvent.enableEncrypt(value),
|
||||
);
|
||||
},
|
||||
value: state.setting.enableEncrypt,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IntrinsicHeight(
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.settings_menu_enableEncryptPrompt.tr(),
|
||||
maxLines: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(6),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.settings_menu_clickToCopySecret.tr(),
|
||||
child: FlowyButton(
|
||||
disable: !(state.setting.enableEncrypt),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: Corners.s5Border,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
text: FlowyText.medium(state.setting.encryptSecret),
|
||||
onTap: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: state.setting.encryptSecret),
|
||||
);
|
||||
showMessageToast(LocaleKeys.message_copy_success.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SupabaseEnableSync extends StatelessWidget {
|
||||
const SupabaseEnableSync({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SupabaseCloudSettingBloc, SupabaseCloudSettingState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
onChanged: (bool value) {
|
||||
context.read<SupabaseCloudSettingBloc>().add(
|
||||
SupabaseCloudSettingEvent.enableSync(value),
|
||||
);
|
||||
},
|
||||
value: state.setting.enableSync,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class SupabaseInput extends StatefulWidget {
|
||||
final String title;
|
||||
final String url;
|
||||
final String hint;
|
||||
final String? error;
|
||||
final Function(String) onChanged;
|
||||
|
||||
const SupabaseInput({
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.hint,
|
||||
required this.onChanged,
|
||||
required this.error,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
SupabaseInputState createState() => SupabaseInputState();
|
||||
}
|
||||
|
||||
class SupabaseInputState extends State<SupabaseInput> {
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.url);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: _controller,
|
||||
style: const TextStyle(fontSize: 12.0),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 6),
|
||||
labelText: widget.title,
|
||||
labelStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontWeight: FontWeight.w400, fontSize: 16),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.onBackground),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
hintText: widget.hint,
|
||||
errorText: widget.error,
|
||||
),
|
||||
onChanged: widget.onChanged,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SupabaseSelfhostTip extends StatelessWidget {
|
||||
final url =
|
||||
"https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy-using-supabase";
|
||||
const SupabaseSelfhostTip({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: 0.6,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: LocaleKeys.settings_menu_selfHostStart.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!,
|
||||
),
|
||||
TextSpan(
|
||||
text: " ${LocaleKeys.settings_menu_selfHostContent.tr()} ",
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: FontSizes.s14,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()..onTap = () => _launchURL(),
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.settings_menu_selfHostEnd.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _launchURL() async {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
Log.error("Could not launch $url");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/sign_in_bloc.dart';
|
||||
@ -55,7 +56,7 @@ class SettingThirdPartyLogin extends StatelessWidget {
|
||||
const VSpace(6),
|
||||
promptMessage,
|
||||
const VSpace(6),
|
||||
const ThirdPartySignInButtons(),
|
||||
if (isAuthEnabled) const ThirdPartySignInButtons(),
|
||||
const VSpace(6),
|
||||
],
|
||||
);
|
||||
|
@ -1,11 +1,8 @@
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingsMenu extends StatelessWidget {
|
||||
const SettingsMenu({
|
||||
@ -19,9 +16,6 @@ class SettingsMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool showSyncSetting = isCloudEnabled &&
|
||||
context.read<SettingsDialogBloc>().state.userProfile.authType !=
|
||||
AuthTypePB.Local;
|
||||
return Column(
|
||||
children: [
|
||||
SettingsMenuElement(
|
||||
@ -63,17 +57,14 @@ class SettingsMenu extends StatelessWidget {
|
||||
icon: Icons.notifications_outlined,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
// Only show supabase setting if supabase is enabled and the current auth type is not local
|
||||
if (showSyncSetting) ...[
|
||||
const SizedBox(height: 10),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.cloud,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_cloudSetting.tr(),
|
||||
icon: Icons.sync,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.cloud,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_cloudSetting.tr(),
|
||||
icon: Icons.sync,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.shortcuts,
|
||||
|
@ -54,7 +54,7 @@ class SettingsUserView extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildUserIconSetting(context),
|
||||
if (isCloudEnabled && user.authType != AuthTypePB.Local) ...[
|
||||
if (isAuthEnabled && user.authType != AuthTypePB.Local) ...[
|
||||
const VSpace(12),
|
||||
UserEmailInput(user.email),
|
||||
],
|
||||
@ -190,7 +190,7 @@ class SettingsUserView extends StatelessWidget {
|
||||
BuildContext context,
|
||||
SettingsUserState state,
|
||||
) {
|
||||
if (!isCloudEnabled) {
|
||||
if (!isAuthEnabled) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
|
1
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
1
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -2148,6 +2148,7 @@ dependencies = [
|
||||
"client-api",
|
||||
"collab-database",
|
||||
"collab-document",
|
||||
"collab-persistence",
|
||||
"fancy-regex 0.11.0",
|
||||
"flowy-codegen",
|
||||
"flowy-derive",
|
||||
|
@ -266,9 +266,27 @@
|
||||
"selfEncryptionLogoutPrompt": "Are you sure you want to log out? Please ensure you have copied the encryption secret",
|
||||
"syncSetting": "Sync Setting",
|
||||
"cloudSetting": "Cloud Setting",
|
||||
"cloudURL": "Server URL",
|
||||
"enableSync": "Enable sync",
|
||||
"enableEncrypt": "Encrypt data",
|
||||
"cloudURL": "Base URL",
|
||||
"invalidCloudURLScheme": "Invalid Scheme",
|
||||
"cloudServerType": "Cloud server",
|
||||
"cloudServerTypeTip": "Please note that it might log out your current account after switching the cloud server",
|
||||
"cloudLocal": "Local",
|
||||
"cloudSupabase": "Supabase",
|
||||
"cloudSupabaseUrl": "Supabase URL",
|
||||
"cloudSupabaseAnonKey": "Supabase anon key",
|
||||
"cloudSupabaseAnonKeyCanNotBeEmpty": "The anon key can't be empty if the supabase url is not empty",
|
||||
"cloudAppFlowy": "AppFlowy Cloud",
|
||||
"clickToCopy": "Click to copy",
|
||||
"selfHostStart": "If you don't have a server, please refer to the",
|
||||
"selfHostContent": "document",
|
||||
"selfHostEnd": "for guidance on how to self-host your own server",
|
||||
"cloudURLHint": "Input the base URL of your server",
|
||||
"cloudWSURL": "Websocket URL",
|
||||
"cloudWSURLHint": "Input the websocket address of your server",
|
||||
"restartApp": "Restart",
|
||||
"restartAppTip": "Restart the application for the changes to take effect. Please note that this might log out your current account",
|
||||
"enableEncryptPrompt": "Activate encryption to secure your data with this secret. Store it safely; once enabled, it can't be turned off. If lost, your data becomes irretrievable. Click to copy",
|
||||
"inputEncryptPrompt": "Please enter your encryption secret for",
|
||||
"clickToCopySecret": "Click to copy secret",
|
||||
|
1
frontend/rust-lib/Cargo.lock
generated
1
frontend/rust-lib/Cargo.lock
generated
@ -1952,6 +1952,7 @@ dependencies = [
|
||||
"client-api",
|
||||
"collab-database",
|
||||
"collab-document",
|
||||
"collab-persistence",
|
||||
"fancy-regex 0.11.0",
|
||||
"flowy-codegen",
|
||||
"flowy-derive",
|
||||
|
@ -25,13 +25,7 @@ impl AppFlowyDartConfiguration {
|
||||
pub fn write_env_from(env_str: &str) {
|
||||
let configuration = Self::from_str(env_str);
|
||||
configuration.cloud_type.write_env();
|
||||
let is_valid = configuration.appflowy_cloud_config.write_env().is_ok();
|
||||
// Note on Configuration Priority:
|
||||
// If both Supabase config and AppFlowy cloud config are provided in the '.env' file,
|
||||
// the AppFlowy cloud config will be prioritized and the Supabase config ignored.
|
||||
// Ensure only one of these configurations is active at any given time.
|
||||
if !is_valid {
|
||||
let _ = configuration.supabase_config.write_env();
|
||||
}
|
||||
configuration.appflowy_cloud_config.write_env();
|
||||
configuration.supabase_config.write_env();
|
||||
}
|
||||
}
|
||||
|
@ -55,14 +55,8 @@ pub extern "C" fn init_sdk(data: *mut c_char) -> i64 {
|
||||
let configuration = AppFlowyDartConfiguration::from_str(serde_str);
|
||||
|
||||
configuration.cloud_type.write_env();
|
||||
let is_valid = configuration.appflowy_cloud_config.write_env().is_ok();
|
||||
// Note on Configuration Priority:
|
||||
// If both Supabase config and AppFlowy cloud config are provided in the '.env' file,
|
||||
// the AppFlowy cloud config will be prioritized and the Supabase config ignored.
|
||||
// Ensure only one of these configurations is active at any given time.
|
||||
if !is_valid {
|
||||
let _ = configuration.supabase_config.write_env();
|
||||
}
|
||||
configuration.appflowy_cloud_config.write_env();
|
||||
configuration.supabase_config.write_env();
|
||||
|
||||
let log_crates = vec!["flowy-ffi".to_string()];
|
||||
let config = AppFlowyCoreConfig::new(
|
||||
|
@ -213,7 +213,7 @@ impl AFCloudTest {
|
||||
test.set_auth_type(AuthTypePB::AFCloud);
|
||||
test
|
||||
.server_provider
|
||||
.set_authenticator(Authenticator::AFCloud);
|
||||
.set_authenticator(Authenticator::AppFlowyCloud);
|
||||
|
||||
Some(Self { inner: test })
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ impl fmt::Debug for AppFlowyCoreConfig {
|
||||
if let Some(config) = &self.cloud_config {
|
||||
debug.field("base_url", &config.base_url);
|
||||
debug.field("ws_url", &config.ws_base_url);
|
||||
debug.field("gotrue_url", &config.gotrue_url);
|
||||
}
|
||||
debug.finish()
|
||||
}
|
||||
@ -43,8 +44,12 @@ impl fmt::Debug for AppFlowyCoreConfig {
|
||||
fn migrate_local_version_data_folder(root: &str, url: &str) -> String {
|
||||
// Isolate the user data folder by using the base url of AppFlowy cloud. This is to avoid
|
||||
// the user data folder being shared by different AppFlowy cloud.
|
||||
let server_base64 = URL_SAFE_ENGINE.encode(url);
|
||||
let storage_path = format!("{}_{}", root, server_base64);
|
||||
let storage_path = if !url.is_empty() {
|
||||
let server_base64 = URL_SAFE_ENGINE.encode(url);
|
||||
format!("{}_{}", root, server_base64)
|
||||
} else {
|
||||
root.to_string()
|
||||
};
|
||||
|
||||
// Copy the user data folder from the root path to the isolated path
|
||||
// The root path without any suffix is the created by the local version AppFlowy
|
||||
|
@ -14,7 +14,6 @@ use flowy_server_config::af_cloud_config::AFCloudConfiguration;
|
||||
use flowy_server_config::supabase_config::SupabaseConfiguration;
|
||||
use flowy_sqlite::kv::StorePreferences;
|
||||
use flowy_user::services::database::{get_user_profile, get_user_workspace, open_user_db};
|
||||
use flowy_user_deps::cloud::UserCloudService;
|
||||
use flowy_user_deps::entities::*;
|
||||
|
||||
use crate::AppFlowyCoreConfig;
|
||||
@ -56,8 +55,6 @@ pub struct ServerProvider {
|
||||
providers: RwLock<HashMap<ServerType, Arc<dyn AppFlowyServer>>>,
|
||||
pub(crate) encryption: RwLock<Arc<dyn AppFlowyEncryption>>,
|
||||
pub(crate) store_preferences: Weak<StorePreferences>,
|
||||
pub(crate) cache_user_service: RwLock<HashMap<ServerType, Arc<dyn UserCloudService>>>,
|
||||
|
||||
pub(crate) enable_sync: RwLock<bool>,
|
||||
pub(crate) uid: Arc<RwLock<Option<i64>>>,
|
||||
}
|
||||
@ -76,7 +73,6 @@ impl ServerProvider {
|
||||
enable_sync: RwLock::new(true),
|
||||
encryption: RwLock::new(Arc::new(encryption)),
|
||||
store_preferences,
|
||||
cache_user_service: Default::default(),
|
||||
uid: Default::default(),
|
||||
}
|
||||
}
|
||||
@ -148,7 +144,7 @@ impl From<Authenticator> for ServerType {
|
||||
fn from(auth_provider: Authenticator) -> Self {
|
||||
match auth_provider {
|
||||
Authenticator::Local => ServerType::Local,
|
||||
Authenticator::AFCloud => ServerType::AFCloud,
|
||||
Authenticator::AppFlowyCloud => ServerType::AFCloud,
|
||||
Authenticator::Supabase => ServerType::Supabase,
|
||||
}
|
||||
}
|
||||
@ -158,7 +154,7 @@ impl From<ServerType> for Authenticator {
|
||||
fn from(ty: ServerType) -> Self {
|
||||
match ty {
|
||||
ServerType::Local => Authenticator::Local,
|
||||
ServerType::AFCloud => Authenticator::AFCloud,
|
||||
ServerType::AFCloud => Authenticator::AppFlowyCloud,
|
||||
ServerType::Supabase => Authenticator::Supabase,
|
||||
}
|
||||
}
|
||||
|
@ -113,16 +113,8 @@ impl UserCloudServiceProvider for ServerProvider {
|
||||
/// Returns the [UserCloudService] base on the current [ServerType].
|
||||
/// Creates a new [AppFlowyServer] if it doesn't exist.
|
||||
fn get_user_service(&self) -> Result<Arc<dyn UserCloudService>, FlowyError> {
|
||||
if let Some(user_service) = self.cache_user_service.read().get(&self.get_server_type()) {
|
||||
return Ok(user_service.clone());
|
||||
}
|
||||
|
||||
let server_type = self.get_server_type();
|
||||
let user_service = self.get_server(&server_type)?.user_service();
|
||||
self
|
||||
.cache_user_service
|
||||
.write()
|
||||
.insert(server_type, user_service.clone());
|
||||
Ok(user_service)
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ r2d2 = { version = "0.8", optional = true }
|
||||
url = { version = "2.2", optional = true }
|
||||
collab-database = { version = "0.1.0", optional = true }
|
||||
collab-document = { version = "0.1.0", optional = true }
|
||||
collab-persistence = { version = "0.1.0", optional = true }
|
||||
tokio-postgres = { version = "0.7.8", optional = true }
|
||||
client-api = { version = "0.1.0", optional = true }
|
||||
|
||||
@ -36,7 +37,7 @@ impl_from_dispatch_error = ["lib-dispatch"]
|
||||
impl_from_serde = []
|
||||
impl_from_reqwest = ["reqwest"]
|
||||
impl_from_sqlite = ["flowy-sqlite", "r2d2"]
|
||||
impl_from_collab = ["collab-database", "collab-document", "impl_from_reqwest"]
|
||||
impl_from_collab = ["collab-database", "collab-document", "impl_from_reqwest", "collab-persistence"]
|
||||
impl_from_postgres = ["tokio-postgres"]
|
||||
impl_from_url = ["url"]
|
||||
impl_from_appflowy_cloud = ["client-api"]
|
||||
|
@ -1,8 +1,18 @@
|
||||
use collab_database::error::DatabaseError;
|
||||
use collab_document::error::DocumentError;
|
||||
use collab_persistence::PersistenceError;
|
||||
|
||||
use crate::FlowyError;
|
||||
use crate::{ErrorCode, FlowyError};
|
||||
|
||||
impl From<PersistenceError> for FlowyError {
|
||||
fn from(err: PersistenceError) -> Self {
|
||||
match err {
|
||||
PersistenceError::RocksdbCorruption(_) => FlowyError::new(ErrorCode::RocksdbCorruption, err),
|
||||
PersistenceError::RocksdbIOError(_) => FlowyError::new(ErrorCode::RocksdbIOError, err),
|
||||
_ => FlowyError::new(ErrorCode::RocksdbInternal, err),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<DatabaseError> for FlowyError {
|
||||
fn from(error: DatabaseError) -> Self {
|
||||
FlowyError::internal().with_context(error)
|
||||
|
@ -2,7 +2,7 @@ use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_error::{ErrorCode, FlowyError};
|
||||
|
||||
pub const APPFLOWY_CLOUD_BASE_URL: &str = "APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL";
|
||||
pub const APPFLOWY_CLOUD_WS_BASE_URL: &str = "APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL";
|
||||
@ -60,25 +60,10 @@ impl AFCloudConfiguration {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), FlowyError> {
|
||||
if self.base_url.is_empty() || self.ws_base_url.is_empty() || self.gotrue_url.is_empty() {
|
||||
return Err(FlowyError::new(
|
||||
ErrorCode::InvalidAuthConfig,
|
||||
format!(
|
||||
"Invalid APPFLOWY_CLOUD_BASE_URL: {}, APPFLOWY_CLOUD_WS_BASE_URL: {}, APPFLOWY_CLOUD_GOTRUE_URL: {}",
|
||||
self.base_url, self.ws_base_url, self.gotrue_url,
|
||||
)),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write the configuration to the environment variables.
|
||||
pub fn write_env(&self) -> FlowyResult<()> {
|
||||
self.validate()?;
|
||||
pub fn write_env(&self) {
|
||||
std::env::set_var(APPFLOWY_CLOUD_BASE_URL, &self.base_url);
|
||||
std::env::set_var(APPFLOWY_CLOUD_WS_BASE_URL, &self.ws_base_url);
|
||||
std::env::set_var(APPFLOWY_CLOUD_GOTRUE_URL, &self.gotrue_url);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_error::{ErrorCode, FlowyError};
|
||||
|
||||
pub const SUPABASE_URL: &str = "APPFLOWY_CLOUD_ENV_SUPABASE_URL";
|
||||
pub const SUPABASE_ANON_KEY: &str = "APPFLOWY_CLOUD_ENV_SUPABASE_ANON_KEY";
|
||||
@ -33,21 +33,9 @@ impl SupabaseConfiguration {
|
||||
Ok(Self { url, anon_key })
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), FlowyError> {
|
||||
if self.url.is_empty() || self.anon_key.is_empty() {
|
||||
return Err(FlowyError::new(
|
||||
ErrorCode::InvalidAuthConfig,
|
||||
"Missing SUPABASE_URL or SUPABASE_ANON_KEY",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write the configuration to the environment variables.
|
||||
pub fn write_env(&self) -> FlowyResult<()> {
|
||||
self.validate()?;
|
||||
pub fn write_env(&self) {
|
||||
std::env::set_var(SUPABASE_URL, &self.url);
|
||||
std::env::set_var(SUPABASE_ANON_KEY, &self.anon_key);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ pub fn user_profile_from_af_profile(
|
||||
openai_key: openai_key.unwrap_or_default(),
|
||||
stability_ai_key: stability_ai_key.unwrap_or_default(),
|
||||
workspace_id: profile.latest_workspace_id.to_string(),
|
||||
authenticator: Authenticator::AFCloud,
|
||||
authenticator: Authenticator::AppFlowyCloud,
|
||||
encryption_type,
|
||||
uid: profile.uid,
|
||||
updated_at: profile.updated_at,
|
||||
|
@ -327,7 +327,7 @@ pub enum Authenticator {
|
||||
Local = 0,
|
||||
/// Currently not supported. It will be supported in the future when the
|
||||
/// [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Server) ready.
|
||||
AFCloud = 1,
|
||||
AppFlowyCloud = 1,
|
||||
/// It uses Supabase as the backend.
|
||||
Supabase = 2,
|
||||
}
|
||||
@ -348,7 +348,7 @@ impl From<i32> for Authenticator {
|
||||
fn from(value: i32) -> Self {
|
||||
match value {
|
||||
0 => Authenticator::Local,
|
||||
1 => Authenticator::AFCloud,
|
||||
1 => Authenticator::AppFlowyCloud,
|
||||
2 => Authenticator::Supabase,
|
||||
_ => Authenticator::Local,
|
||||
}
|
||||
|
@ -244,3 +244,15 @@ impl std::default::Default for NotificationSettingsPB {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct AppFlowyCloudSettingPB {
|
||||
#[pb(index = 1)]
|
||||
pub base_url: bool,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub ws_addr: bool,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub gotrue_url: String,
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ use std::sync::Weak;
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
|
||||
use serde_json::Value;
|
||||
use tracing::event;
|
||||
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_sqlite::kv::StorePreferences;
|
||||
@ -86,7 +87,7 @@ pub async fn get_user_profile_handler(
|
||||
) -> DataResult<UserProfilePB, FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let uid = manager.get_session()?.user_id;
|
||||
let mut user_profile = manager.get_user_profile(uid).await?;
|
||||
let mut user_profile = manager.get_user_profile_from_disk(uid).await?;
|
||||
|
||||
let weak_manager = Arc::downgrade(&manager);
|
||||
let cloned_user_profile = user_profile.clone();
|
||||
@ -285,6 +286,7 @@ pub async fn sign_in_with_provider_handler(
|
||||
let manager = upgrade_manager(manager)?;
|
||||
tracing::debug!("Sign in with provider: {:?}", data.provider.as_str());
|
||||
let sign_in_url = manager.generate_oauth_url(data.provider.as_str()).await?;
|
||||
event!(tracing::Level::DEBUG, "Sign in url: {}", sign_in_url);
|
||||
data_result_ok(OauthProviderDataPB {
|
||||
oauth_url: sign_in_url,
|
||||
})
|
||||
@ -332,7 +334,7 @@ pub async fn check_encrypt_secret_handler(
|
||||
) -> DataResult<UserEncryptionConfigurationPB, FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let uid = manager.get_session()?.user_id;
|
||||
let profile = manager.get_user_profile(uid).await?;
|
||||
let profile = manager.get_user_profile_from_disk(uid).await?;
|
||||
|
||||
let is_need_secret = match profile.encryption_type {
|
||||
EncryptionType::NoEncryption => false,
|
||||
@ -402,7 +404,8 @@ pub async fn set_cloud_config_handler(
|
||||
};
|
||||
|
||||
send_notification(
|
||||
&session.user_id.to_string(),
|
||||
// Don't change this key. it's also used in the frontend
|
||||
"user_cloud_config",
|
||||
UserNotification::DidUpdateCloudConfig,
|
||||
)
|
||||
.payload(payload)
|
||||
|
@ -16,6 +16,7 @@ use tracing::{debug, error, event, info, instrument};
|
||||
use collab_integrate::collab_builder::AppFlowyCollabBuilder;
|
||||
use collab_integrate::RocksCollabDB;
|
||||
use flowy_error::{internal_error, ErrorCode, FlowyResult};
|
||||
use flowy_server_config::AuthenticatorType;
|
||||
use flowy_sqlite::kv::StorePreferences;
|
||||
use flowy_sqlite::schema::user_table;
|
||||
use flowy_sqlite::ConnectionPool;
|
||||
@ -157,7 +158,27 @@ impl UserManager {
|
||||
*self.collab_interact.write().await = Arc::new(collab_interact);
|
||||
|
||||
if let Ok(session) = self.get_session() {
|
||||
let user = self.get_user_profile(session.user_id).await?;
|
||||
let user = self.get_user_profile_from_disk(session.user_id).await?;
|
||||
|
||||
// Get the current authenticator from the environment variable
|
||||
let current_authenticator = match AuthenticatorType::from_env() {
|
||||
AuthenticatorType::Local => Authenticator::Local,
|
||||
AuthenticatorType::Supabase => Authenticator::Supabase,
|
||||
AuthenticatorType::AppFlowyCloud => Authenticator::AppFlowyCloud,
|
||||
};
|
||||
|
||||
// If the current authenticator is different from the authenticator in the session and it's
|
||||
// not a local authenticator, we need to sign out the user.
|
||||
if user.authenticator != Authenticator::Local && user.authenticator != current_authenticator {
|
||||
event!(
|
||||
tracing::Level::INFO,
|
||||
"Authenticator changed from {:?} to {:?}",
|
||||
user.authenticator,
|
||||
current_authenticator
|
||||
);
|
||||
self.sign_out().await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
event!(
|
||||
tracing::Level::INFO,
|
||||
@ -201,9 +222,7 @@ impl UserManager {
|
||||
// Do the user data migration if needed
|
||||
event!(tracing::Level::INFO, "Prepare user data migration");
|
||||
match (
|
||||
self
|
||||
.database
|
||||
.get_collab_db(session.user_id, &self.user_config.device_id),
|
||||
self.database.get_collab_db(session.user_id),
|
||||
self.database.get_pool(session.user_id),
|
||||
) {
|
||||
(Ok(collab_db), Ok(sqlite_pool)) => {
|
||||
@ -257,7 +276,7 @@ impl UserManager {
|
||||
pub fn get_collab_db(&self, uid: i64) -> Result<Weak<RocksCollabDB>, FlowyError> {
|
||||
self
|
||||
.database
|
||||
.get_collab_db(uid, "")
|
||||
.get_collab_db(uid)
|
||||
.map(|collab_db| Arc::downgrade(&collab_db))
|
||||
}
|
||||
|
||||
@ -477,7 +496,7 @@ impl UserManager {
|
||||
let session = self.get_session()?;
|
||||
upsert_user_profile_change(session.user_id, self.db_pool(session.user_id)?, changeset)?;
|
||||
|
||||
let profile = self.get_user_profile(session.user_id).await?;
|
||||
let profile = self.get_user_profile_from_disk(session.user_id).await?;
|
||||
self
|
||||
.update_user(session.user_id, profile.token, params)
|
||||
.await?;
|
||||
@ -507,7 +526,7 @@ impl UserManager {
|
||||
}
|
||||
|
||||
/// Fetches the user profile for the given user ID.
|
||||
pub async fn get_user_profile(&self, uid: i64) -> Result<UserProfile, FlowyError> {
|
||||
pub async fn get_user_profile_from_disk(&self, uid: i64) -> Result<UserProfile, FlowyError> {
|
||||
let user: UserProfile = user_table::dsl::user_table
|
||||
.filter(user_table::id.eq(&uid.to_string()))
|
||||
.first::<UserTable>(&*(self.db_connection(uid)?))
|
||||
@ -524,14 +543,18 @@ impl UserManager {
|
||||
|
||||
#[tracing::instrument(level = "info", skip_all, err)]
|
||||
pub async fn refresh_user_profile(&self, old_user_profile: &UserProfile) -> FlowyResult<()> {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
// If the user is a local user, no need to refresh the user profile
|
||||
if old_user_profile.authenticator.is_local() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
// Add debounce to avoid too many requests
|
||||
if now - self.refresh_user_profile_since.load(Ordering::SeqCst) < 5 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.refresh_user_profile_since.store(now, Ordering::SeqCst);
|
||||
|
||||
let uid = old_user_profile.uid;
|
||||
let result: Result<UserProfile, FlowyError> = self
|
||||
.cloud_services
|
||||
@ -541,27 +564,6 @@ impl UserManager {
|
||||
|
||||
match result {
|
||||
Ok(new_user_profile) => {
|
||||
// If the authentication type has changed, it indicates that the user has signed in
|
||||
// using a different release package but is sharing the same data folder.
|
||||
// In such cases, notify the frontend to log out.
|
||||
if old_user_profile.authenticator != Authenticator::Local
|
||||
&& new_user_profile.authenticator != old_user_profile.authenticator
|
||||
{
|
||||
event!(
|
||||
tracing::Level::INFO,
|
||||
"User login with different authenticator: {:?} -> {:?}",
|
||||
old_user_profile.authenticator,
|
||||
new_user_profile.authenticator
|
||||
);
|
||||
|
||||
send_auth_state_notification(AuthStateChangedPB {
|
||||
state: AuthStatePB::InvalidAuth,
|
||||
message: "User login with different cloud".to_string(),
|
||||
})
|
||||
.send();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If the user profile is updated, save the new user profile
|
||||
if new_user_profile.updated_at > old_user_profile.updated_at {
|
||||
validate_encryption_sign(old_user_profile, &new_user_profile.encryption_type.sign());
|
||||
@ -578,6 +580,15 @@ impl UserManager {
|
||||
tracing::Level::ERROR,
|
||||
"User is unauthorized, sign out the user"
|
||||
);
|
||||
|
||||
self.add_historical_user(
|
||||
uid,
|
||||
&self.user_config.device_id,
|
||||
old_user_profile.name.clone(),
|
||||
&old_user_profile.authenticator,
|
||||
self.user_dir(uid),
|
||||
);
|
||||
|
||||
self.sign_out().await?;
|
||||
send_auth_state_notification(AuthStateChangedPB {
|
||||
state: AuthStatePB::InvalidAuth,
|
||||
@ -592,7 +603,7 @@ impl UserManager {
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub fn user_dir(&self, uid: i64) -> String {
|
||||
self.user_paths.user_dir(uid)
|
||||
self.user_paths.user_data_dir(uid)
|
||||
}
|
||||
|
||||
pub fn user_setting(&self) -> Result<UserSettingPB, FlowyError> {
|
||||
@ -713,7 +724,9 @@ impl UserManager {
|
||||
&self,
|
||||
oauth_provider: &str,
|
||||
) -> Result<String, FlowyError> {
|
||||
self.update_authenticator(&Authenticator::AFCloud).await;
|
||||
self
|
||||
.update_authenticator(&Authenticator::AppFlowyCloud)
|
||||
.await;
|
||||
let auth_service = self.cloud_services.get_user_service()?;
|
||||
let url = auth_service
|
||||
.generate_oauth_url_with_provider(oauth_provider)
|
||||
@ -757,7 +770,7 @@ impl UserManager {
|
||||
let session = self.get_session()?;
|
||||
if session.user_id == user_update.uid {
|
||||
debug!("Receive user update: {:?}", user_update);
|
||||
let user_profile = self.get_user_profile(user_update.uid).await?;
|
||||
let user_profile = self.get_user_profile_from_disk(user_update.uid).await?;
|
||||
if !validate_encryption_sign(&user_profile, &user_update.encryption_sign) {
|
||||
return Ok(());
|
||||
}
|
||||
@ -778,12 +791,8 @@ impl UserManager {
|
||||
old_user: &MigrationUser,
|
||||
new_user: &MigrationUser,
|
||||
) -> Result<(), FlowyError> {
|
||||
let old_collab_db = self
|
||||
.database
|
||||
.get_collab_db(old_user.session.user_id, &self.user_config.device_id)?;
|
||||
let new_collab_db = self
|
||||
.database
|
||||
.get_collab_db(new_user.session.user_id, &self.user_config.device_id)?;
|
||||
let old_collab_db = self.database.get_collab_db(old_user.session.user_id)?;
|
||||
let new_collab_db = self.database.get_collab_db(new_user.session.user_id)?;
|
||||
migration_anon_user_on_sign_up(old_user, &old_collab_db, new_user, &new_collab_db)?;
|
||||
|
||||
if let Err(err) = sync_user_data_to_cloud(
|
||||
@ -859,24 +868,26 @@ impl UserPaths {
|
||||
fn new(root: String) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
fn user_dir(&self, uid: i64) -> String {
|
||||
|
||||
/// Returns the path to the user's data directory.
|
||||
fn user_data_dir(&self, uid: i64) -> String {
|
||||
format!("{}/{}", self.root, uid)
|
||||
}
|
||||
}
|
||||
|
||||
impl UserDBPath for UserPaths {
|
||||
fn user_db_path(&self, uid: i64) -> PathBuf {
|
||||
PathBuf::from(self.user_dir(uid))
|
||||
PathBuf::from(self.user_data_dir(uid))
|
||||
}
|
||||
|
||||
fn collab_db_path(&self, uid: i64) -> PathBuf {
|
||||
let mut path = PathBuf::from(self.user_dir(uid));
|
||||
let mut path = PathBuf::from(self.user_data_dir(uid));
|
||||
path.push("collab_db");
|
||||
path
|
||||
}
|
||||
|
||||
fn collab_db_history(&self, uid: i64, create_if_not_exist: bool) -> std::io::Result<PathBuf> {
|
||||
let path = PathBuf::from(self.user_dir(uid)).join("collab_db_history");
|
||||
let path = PathBuf::from(self.user_data_dir(uid)).join("collab_db_history");
|
||||
if !path.exists() && create_if_not_exist {
|
||||
fs::create_dir_all(&path)?;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use parking_lot::RwLock;
|
||||
use tracing::{error, event, info, instrument};
|
||||
|
||||
use collab_integrate::{PersistenceError, RocksCollabDB, YrsDocAction};
|
||||
use flowy_error::{ErrorCode, FlowyError};
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_sqlite::schema::user_workspace_table;
|
||||
use flowy_sqlite::ConnectionPool;
|
||||
use flowy_sqlite::{
|
||||
@ -50,40 +50,47 @@ impl UserDB {
|
||||
/// attempts to restore the database from the latest backup.
|
||||
/// - If the CollabDB does not exist, it immediately attempts to restore from the latest backup.
|
||||
///
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn backup_or_restore(&self, uid: i64, workspace_id: &str) {
|
||||
// Obtain the path for the collaboration database.
|
||||
let collab_db_path = self.paths.collab_db_path(uid);
|
||||
|
||||
// Obtain the history folder path, proceed if successful.
|
||||
if let Ok(history_folder) = self.paths.collab_db_history(uid, true) {
|
||||
// Initialize the backup utility for the collaboration database.
|
||||
let zip_backup = CollabDBZipBackup::new(collab_db_path.clone(), history_folder);
|
||||
|
||||
if collab_db_path.exists() {
|
||||
// Validate the existing collaboration database.
|
||||
let is_ok = validate_collab_db(&collab_db_path, uid, workspace_id);
|
||||
let zip_backup = CollabDBZipBackup::new(collab_db_path, history_folder);
|
||||
|
||||
if is_ok {
|
||||
// If the database opens successfully, it attempts to back it up in the background.
|
||||
// If database is valid, update the shared map and initiate backup.
|
||||
// Asynchronous backup operation.
|
||||
af_spawn(async move {
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
if let Err(err) = zip_backup.backup() {
|
||||
error!("backup collab db failed, {:?}", err);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
if let Err(err) = tokio::task::spawn_blocking(move || zip_backup.backup()).await {
|
||||
error!("Backup of collab db failed: {:?}", err);
|
||||
}
|
||||
});
|
||||
} else if let Err(err) = zip_backup.restore_latest_backup() {
|
||||
error!("restore collab db failed, {:?}", err);
|
||||
}
|
||||
} else {
|
||||
let zip_backup = CollabDBZipBackup::new(collab_db_path, history_folder);
|
||||
if let Err(err) = zip_backup.restore_latest_backup() {
|
||||
error!("restore collab db failed, {:?}", err);
|
||||
// If validation fails, attempt to restore from the latest backup.
|
||||
error!("Restoring collab db failed: {:?}", err);
|
||||
}
|
||||
} else if let Err(err) = zip_backup.restore_latest_backup() {
|
||||
// If collab database does not exist, attempt to restore from the latest backup.
|
||||
error!("Restoring collab db failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn restore_if_need(&self, uid: i64, workspace_id: &str) {
|
||||
if let Ok(history_folder) = self.paths.collab_db_history(uid, false) {
|
||||
let collab_db_path = self.paths.collab_db_path(uid);
|
||||
let is_ok = validate_collab_db(&collab_db_path, uid, workspace_id);
|
||||
let zip_backup = CollabDBZipBackup::new(collab_db_path, history_folder);
|
||||
|
||||
if !is_ok {
|
||||
let zip_backup = CollabDBZipBackup::new(collab_db_path, history_folder);
|
||||
if let Err(err) = zip_backup.restore_latest_backup() {
|
||||
error!("restore collab db failed, {:?}", err);
|
||||
}
|
||||
@ -118,15 +125,11 @@ impl UserDB {
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub(crate) fn get_collab_db(
|
||||
&self,
|
||||
user_id: i64,
|
||||
_device_id: &str,
|
||||
) -> Result<Arc<RocksCollabDB>, FlowyError> {
|
||||
pub(crate) fn get_collab_db(&self, user_id: i64) -> Result<Arc<RocksCollabDB>, FlowyError> {
|
||||
let collab_db = open_collab_db(
|
||||
self.paths.user_db_path(user_id),
|
||||
// self.paths.user_db_path(user_id),
|
||||
self.paths.collab_db_path(user_id),
|
||||
self.paths.collab_db_history(user_id, false).ok(),
|
||||
// self.paths.collab_db_history(user_id, false).ok(),
|
||||
user_id,
|
||||
)?;
|
||||
Ok(collab_db)
|
||||
@ -175,31 +178,24 @@ pub fn get_user_workspace(
|
||||
/// Open a collab db for the user. If the db is already opened, return the opened db.
|
||||
///
|
||||
fn open_collab_db(
|
||||
_collab_backup_db_path: impl AsRef<Path>,
|
||||
collab_db_path: impl AsRef<Path>,
|
||||
_collab_db_history: Option<PathBuf>,
|
||||
uid: i64,
|
||||
) -> Result<Arc<RocksCollabDB>, FlowyError> {
|
||||
) -> Result<Arc<RocksCollabDB>, PersistenceError> {
|
||||
if let Some(collab_db) = COLLAB_DB_MAP.read().get(&uid) {
|
||||
return Ok(collab_db.clone());
|
||||
}
|
||||
|
||||
let mut write_guard = COLLAB_DB_MAP.write();
|
||||
info!(
|
||||
"open collab db for user {} at path: {:?}",
|
||||
uid,
|
||||
collab_db_path.as_ref()
|
||||
);
|
||||
let db = match RocksCollabDB::open(&collab_db_path) {
|
||||
Ok(db) => Ok(db),
|
||||
Err(err) => {
|
||||
tracing::error!("open collab db error, {:?}", err);
|
||||
match err {
|
||||
PersistenceError::RocksdbCorruption(_) => {
|
||||
// try restore from the backup db
|
||||
Err(FlowyError::new(ErrorCode::RocksdbCorruption, err))
|
||||
},
|
||||
PersistenceError::RocksdbIOError(_) => {
|
||||
//
|
||||
Err(FlowyError::new(ErrorCode::RocksdbIOError, err))
|
||||
},
|
||||
_ => Err(FlowyError::new(ErrorCode::RocksdbInternal, err)),
|
||||
}
|
||||
error!("open collab db error, {:?}", err);
|
||||
Err(err)
|
||||
},
|
||||
}?;
|
||||
|
||||
@ -337,12 +333,17 @@ pub(crate) fn validate_collab_db(
|
||||
// Attempt to open the collaboration database using the workspace_id. The workspace_id must already
|
||||
// exist in the collab database. If it does not, it may be indicative of corruption in the collab database
|
||||
// due to other factors.
|
||||
let result = RocksCollabDB::open(&collab_db_path).map(|db| {
|
||||
let read_txn = db.read_txn();
|
||||
read_txn.is_exist(uid, workspace_id)
|
||||
});
|
||||
match result {
|
||||
Ok(is_ok) => is_ok,
|
||||
info!(
|
||||
"open collab db to validate data integration for user {} at path: {:?}",
|
||||
uid,
|
||||
collab_db_path.as_ref()
|
||||
);
|
||||
|
||||
match open_collab_db(&collab_db_path, uid) {
|
||||
Ok(db) => {
|
||||
let read_txn = db.read_txn();
|
||||
read_txn.is_exist(uid, workspace_id)
|
||||
},
|
||||
// return false if the error is not related to corruption
|
||||
Err(err) => !matches!(
|
||||
err,
|
||||
|
@ -155,7 +155,7 @@ impl From<AuthTypePB> for Authenticator {
|
||||
match pb {
|
||||
AuthTypePB::Supabase => Authenticator::Supabase,
|
||||
AuthTypePB::Local => Authenticator::Local,
|
||||
AuthTypePB::AFCloud => Authenticator::AFCloud,
|
||||
AuthTypePB::AFCloud => Authenticator::AppFlowyCloud,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -165,7 +165,7 @@ impl From<Authenticator> for AuthTypePB {
|
||||
match auth_type {
|
||||
Authenticator::Supabase => AuthTypePB::Supabase,
|
||||
Authenticator::Local => AuthTypePB::Local,
|
||||
Authenticator::AFCloud => AuthTypePB::AFCloud,
|
||||
Authenticator::AppFlowyCloud => AuthTypePB::AFCloud,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,10 @@ impl UserManager {
|
||||
// Only migrate the data if the user is login in as a guest and sign up as a new user if the current
|
||||
// auth type is not [AuthType::Local].
|
||||
let session = self.get_session().ok()?;
|
||||
let user_profile = self.get_user_profile(session.user_id).await.ok()?;
|
||||
let user_profile = self
|
||||
.get_user_profile_from_disk(session.user_id)
|
||||
.await
|
||||
.ok()?;
|
||||
if user_profile.authenticator == Authenticator::Local && !auth_type.is_local() {
|
||||
Some(MigrationUser {
|
||||
user_profile,
|
||||
@ -44,7 +47,7 @@ impl UserManager {
|
||||
uid: i64,
|
||||
device_id: &str,
|
||||
user_name: String,
|
||||
auth_type: &Authenticator,
|
||||
authenticator: &Authenticator,
|
||||
storage_path: String,
|
||||
) {
|
||||
let mut logger_users = self
|
||||
@ -54,7 +57,7 @@ impl UserManager {
|
||||
logger_users.add_user(HistoricalUser {
|
||||
user_id: uid,
|
||||
user_name,
|
||||
auth_type: auth_type.clone(),
|
||||
auth_type: authenticator.clone(),
|
||||
sign_in_timestamp: timestamp(),
|
||||
storage_path,
|
||||
device_id: device_id.to_string(),
|
||||
|
Loading…
Reference in New Issue
Block a user