mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support-OAuth-login (#4899)
* feat: support-OAuth-login * fix: modified ubuntu version and devtool * fix: ts lint error
This commit is contained in:
parent
57e3a2ce68
commit
6d4cfe7316
9
.github/workflows/tauri_ci.yaml
vendored
9
.github/workflows/tauri_ci.yaml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest]
|
||||
platform: [ubuntu-20.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
@ -32,7 +32,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
@ -80,7 +80,7 @@ jobs:
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@ -110,4 +110,5 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
args: "--debug"
|
10
.github/workflows/tauri_release.yml
vendored
10
.github/workflows/tauri_release.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
- platform: macos-latest
|
||||
args: "--target x86_64-apple-darwin"
|
||||
target: "macos-x86_64"
|
||||
- platform: ubuntu-latest
|
||||
- platform: ubuntu-20.04
|
||||
args: "--target x86_64-unknown-linux-gnu"
|
||||
target: "linux-x86_64"
|
||||
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
@ -88,7 +88,7 @@ jobs:
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@ -140,14 +140,14 @@ jobs:
|
||||
|
||||
- name: Upload Deb package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.deb
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
|
||||
|
||||
- name: Upload AppImage package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.AppImage
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
|
||||
|
4
frontend/appflowy_tauri/.gitignore
vendored
4
frontend/appflowy_tauri/.gitignore
vendored
@ -28,4 +28,6 @@ dist-ssr
|
||||
**/src/appflowy_app/i18n/translations/
|
||||
|
||||
coverage
|
||||
**/AppFlowy-Collab
|
||||
**/AppFlowy-Collab
|
||||
|
||||
.env
|
2
frontend/appflowy_tauri/src-tauri/.gitignore
vendored
2
frontend/appflowy_tauri/src-tauri/.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
.env
|
||||
|
92
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
92
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -182,6 +182,7 @@ name = "appflowy_tauri"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"dotenv",
|
||||
"flowy-config",
|
||||
"flowy-core",
|
||||
"flowy-date",
|
||||
@ -194,6 +195,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-utils",
|
||||
"tracing",
|
||||
"uuid",
|
||||
@ -1439,6 +1441,15 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
@ -1449,6 +1460,18 @@ dependencies = [
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
@ -1466,6 +1489,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "1.0.6"
|
||||
@ -3088,6 +3117,19 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interprocess"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"rustc_version",
|
||||
"to_method",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.8.0"
|
||||
@ -3831,6 +3873,28 @@ dependencies = [
|
||||
"objc_id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459"
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d"
|
||||
dependencies = [
|
||||
"objc-sys",
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666"
|
||||
|
||||
[[package]]
|
||||
name = "objc_exception"
|
||||
version = "0.1.2"
|
||||
@ -3934,6 +3998,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "0.9.2"
|
||||
@ -6016,6 +6086,22 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4536f5f6602e8fdfaa7b3b185076c2a0704f8eb7015f4e58461eb483ec3ed1f8"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"interprocess",
|
||||
"log",
|
||||
"objc2",
|
||||
"once_cell",
|
||||
"tauri-utils",
|
||||
"windows-sys 0.48.0",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.14.1"
|
||||
@ -6242,6 +6328,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "to_method"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.36.0"
|
||||
|
@ -67,7 +67,10 @@ flowy-document = { path = "../../rust-lib/flowy-document", features = [
|
||||
flowy-notification = { path = "../../rust-lib/flowy-notification", features = [
|
||||
"tauri_ts",
|
||||
] }
|
||||
|
||||
uuid = "1.5.0"
|
||||
tauri-plugin-deep-link = "0.1.2"
|
||||
dotenv = "0.15.0"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
19
frontend/appflowy_tauri/src-tauri/Info.plist
Normal file
19
frontend/appflowy_tauri/src-tauri/Info.plist
Normal file
@ -0,0 +1,19 @@
|
||||
<!-- Add this file next to your tauri.conf.json file -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<!-- Obviously needs to be replaced with your app's bundle identifier -->
|
||||
<string>appflowy-flutter</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>appflowy-flutter</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
4
frontend/appflowy_tauri/src-tauri/env.development
Normal file
4
frontend/appflowy_tauri/src-tauri/env.development
Normal file
@ -0,0 +1,4 @@
|
||||
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://test.appflowy.cloud
|
||||
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://test.appflowy.cloud/ws/v1
|
||||
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://test.appflowy.cloud/gotrue
|
||||
APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2
|
4
frontend/appflowy_tauri/src-tauri/env.production
Normal file
4
frontend/appflowy_tauri/src-tauri/env.production
Normal file
@ -0,0 +1,4 @@
|
||||
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://beta.appflowy.cloud
|
||||
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://beta.appflowy.cloud/ws/v1
|
||||
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://beta.appflowy.cloud/gotrue
|
||||
APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2
|
@ -3,10 +3,33 @@ use flowy_core::{AppFlowyCore, DEFAULT_NAME};
|
||||
use lib_dispatch::runtime::AFPluginRuntime;
|
||||
use std::sync::Arc;
|
||||
|
||||
use dotenv::dotenv;
|
||||
|
||||
pub fn read_env() {
|
||||
dotenv().ok();
|
||||
|
||||
let env = if cfg!(debug_assertions) {
|
||||
include_str!("../env.development")
|
||||
} else {
|
||||
include_str!("../env.production")
|
||||
};
|
||||
|
||||
for line in env.lines() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
// Check if the environment variable is not already set in the system
|
||||
let current_value = std::env::var(key).unwrap_or_default();
|
||||
if current_value.is_empty() {
|
||||
std::env::set_var(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_flowy_core() -> AppFlowyCore {
|
||||
let config_json = include_str!("../tauri.conf.json");
|
||||
let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap();
|
||||
|
||||
let app_version = config.package.version.clone().map(|v| v.to_string()).unwrap_or_else(|| "0.0.0".to_string());
|
||||
let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap();
|
||||
if cfg!(debug_assertions) {
|
||||
data_path.push("data_dev");
|
||||
@ -18,10 +41,11 @@ pub fn init_flowy_core() -> AppFlowyCore {
|
||||
let application_path = data_path.to_str().unwrap().to_string();
|
||||
let device_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
read_env();
|
||||
std::env::set_var("RUST_LOG", "trace");
|
||||
// TODO(nathan): pass the real version here
|
||||
|
||||
let config = AppFlowyCoreConfig::new(
|
||||
"1.0.0".to_string(),
|
||||
app_version,
|
||||
custom_application_path,
|
||||
application_path,
|
||||
device_id,
|
||||
|
@ -3,6 +3,10 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const DEEP_LINK_SCHEME: &str = "appflowy-flutter";
|
||||
pub const OPEN_DEEP_LINK: &str = "open_deep_link";
|
||||
|
||||
mod init;
|
||||
mod notification;
|
||||
mod request;
|
||||
@ -12,8 +16,11 @@ use init::*;
|
||||
use notification::*;
|
||||
use request::*;
|
||||
use tauri::Manager;
|
||||
extern crate dotenv;
|
||||
|
||||
fn main() {
|
||||
tauri_plugin_deep_link::prepare(DEEP_LINK_SCHEME);
|
||||
|
||||
let flowy_core = init_flowy_core();
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![invoke_request])
|
||||
@ -26,6 +33,7 @@ fn main() {
|
||||
unregister_all_notification_sender();
|
||||
register_notification_sender(TSNotificationSender::new(app_handler.clone()));
|
||||
// tauri::async_runtime::spawn(async move {});
|
||||
|
||||
window.listen_global(AF_EVENT, move |event| {
|
||||
on_event(app_handler.clone(), event);
|
||||
});
|
||||
@ -33,18 +41,29 @@ fn main() {
|
||||
.setup(|_app| {
|
||||
let splashscreen_window = _app.get_window("splashscreen").unwrap();
|
||||
let window = _app.get_window("main").unwrap();
|
||||
let handle = _app.handle();
|
||||
|
||||
// we perform the initialization code on a new task so the app doesn't freeze
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// initialize your app here instead of sleeping :)
|
||||
println!("Initializing...");
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
println!("Done initializing.");
|
||||
|
||||
// After it's done, close the splashscreen and display the main window
|
||||
splashscreen_window.close().unwrap();
|
||||
window.show().unwrap();
|
||||
// If you need macOS support this must be called in .setup() !
|
||||
// Otherwise this could be called right after prepare() but then you don't have access to tauri APIs
|
||||
// On macOS You still have to install a .app bundle you got from tauri build --debug for this to work!
|
||||
tauri_plugin_deep_link::register(
|
||||
DEEP_LINK_SCHEME,
|
||||
move |request| {
|
||||
dbg!(&request);
|
||||
handle.emit_all(OPEN_DEEP_LINK, request).unwrap();
|
||||
},
|
||||
)
|
||||
.unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "AppFlowy",
|
||||
"version": "0.0.0"
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
||||
import { Theme as ThemeType, ThemeMode } from '$app/stores/reducers/current-user/slice';
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { getDesignTokens } from '$app/utils/mui';
|
||||
@ -10,6 +10,8 @@ import { UserService } from '$app/application/user/user.service';
|
||||
export function useUserSetting() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { i18n } = useTranslation();
|
||||
const loginState = useAppSelector((state) => state.currentUser.loginState);
|
||||
|
||||
const { themeMode = ThemeMode.System, theme: themeType = ThemeType.Default } = useAppSelector((state) => {
|
||||
return {
|
||||
themeMode: state.currentUser.userSetting.themeMode,
|
||||
@ -22,6 +24,7 @@ export function useUserSetting() {
|
||||
(themeMode === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginState !== LoginState.Success && loginState !== undefined) return;
|
||||
void (async () => {
|
||||
const settings = await UserService.getAppearanceSetting();
|
||||
|
||||
@ -29,7 +32,7 @@ export function useUserSetting() {
|
||||
dispatch(currentUserActions.setUserSetting(settings));
|
||||
await i18n.changeLanguage(settings.language);
|
||||
})();
|
||||
}, [dispatch, i18n]);
|
||||
}, [dispatch, i18n, loginState]);
|
||||
|
||||
useEffect(() => {
|
||||
const html = document.documentElement;
|
||||
|
@ -8,6 +8,7 @@ import { useUserSetting } from '$app/AppMain.hooks';
|
||||
import TrashPage from '$app/views/TrashPage';
|
||||
import DocumentPage from '$app/views/DocumentPage';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import AppFlowyDevTool from '$app/components/_shared/devtool/AppFlowyDevTool';
|
||||
|
||||
function AppMain() {
|
||||
const { muiTheme } = useUserSetting();
|
||||
@ -22,6 +23,7 @@ function AppMain() {
|
||||
</Route>
|
||||
</Routes>
|
||||
<Toaster />
|
||||
{process.env.NODE_ENV === 'development' && <AppFlowyDevTool />}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
UserEventOpenWorkspace,
|
||||
UserEventRenameWorkspace,
|
||||
UserEventChangeWorkspaceIcon,
|
||||
UserEventGetAllWorkspace,
|
||||
} from '@/services/backend/events/flowy-user';
|
||||
import {
|
||||
FolderEventCreateView,
|
||||
@ -62,17 +63,13 @@ export async function getWorkspaceChildViews(id: string) {
|
||||
}
|
||||
|
||||
export async function getWorkspaces() {
|
||||
const result = await FolderEventReadCurrentWorkspace();
|
||||
const result = await UserEventGetAllWorkspace();
|
||||
|
||||
if (result.ok) {
|
||||
const item = result.val;
|
||||
|
||||
return [
|
||||
{
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
},
|
||||
];
|
||||
return result.val.items.map((workspace) => ({
|
||||
id: workspace.workspace_id,
|
||||
name: workspace.name,
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
@ -92,12 +89,7 @@ export async function getCurrentWorkspace() {
|
||||
const result = await FolderEventReadCurrentWorkspace();
|
||||
|
||||
if (result.ok) {
|
||||
const workspace = result.val;
|
||||
|
||||
return {
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
};
|
||||
return result.val.id;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -111,9 +103,7 @@ export async function createCurrentWorkspaceChildView(
|
||||
const result = await FolderEventCreateView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
const view = result.val;
|
||||
|
||||
return view;
|
||||
return result.val;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
|
@ -1,33 +1,63 @@
|
||||
import { SignInPayloadPB, SignUpPayloadPB } from '@/services/backend';
|
||||
import {
|
||||
UserEventSignInWithEmailPassword,
|
||||
SignUpPayloadPB,
|
||||
OauthProviderPB,
|
||||
ProviderTypePB,
|
||||
OauthSignInPB,
|
||||
AuthenticatorPB,
|
||||
SignInPayloadPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
UserEventSignOut,
|
||||
UserEventSignUp,
|
||||
UserEventGetOauthURLWithProvider,
|
||||
UserEventOauthSignIn,
|
||||
UserEventSignInWithEmailPassword,
|
||||
} from '@/services/backend/events/flowy-user';
|
||||
import { nanoid } from '@reduxjs/toolkit';
|
||||
import { Log } from '$app/utils/log';
|
||||
|
||||
export const AuthService = {
|
||||
signIn: async (params: { email: string; password: string }) => {
|
||||
const payload = SignInPayloadPB.fromObject({ email: params.email, password: params.password });
|
||||
getOAuthURL: async (provider: ProviderTypePB) => {
|
||||
const providerDataRes = await UserEventGetOauthURLWithProvider(
|
||||
OauthProviderPB.fromObject({
|
||||
provider,
|
||||
})
|
||||
);
|
||||
|
||||
const res = await UserEventSignInWithEmailPassword(payload);
|
||||
|
||||
if (res.ok) {
|
||||
return res.val;
|
||||
if (!providerDataRes.ok) {
|
||||
Log.error(providerDataRes.val.msg);
|
||||
throw new Error(providerDataRes.val.msg);
|
||||
}
|
||||
|
||||
Log.error(res.val.msg);
|
||||
throw new Error(res.val.msg);
|
||||
const providerData = providerDataRes.val;
|
||||
|
||||
return providerData.oauth_url;
|
||||
},
|
||||
|
||||
signUp: async (params: { name: string; email: string; password: string }) => {
|
||||
const deviceId = nanoid(8);
|
||||
signInWithOAuth: async ({ uri, deviceId }: { uri: string; deviceId: string }) => {
|
||||
const payload = OauthSignInPB.fromObject({
|
||||
authenticator: AuthenticatorPB.AppFlowyCloud,
|
||||
map: {
|
||||
sign_in_url: uri,
|
||||
device_id: deviceId,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await UserEventOauthSignIn(payload);
|
||||
|
||||
if (!res.ok) {
|
||||
Log.error(res.val.msg);
|
||||
throw new Error(res.val.msg);
|
||||
}
|
||||
|
||||
return res.val;
|
||||
},
|
||||
|
||||
signUp: async (params: { deviceId: string; name: string; email: string; password: string }) => {
|
||||
const payload = SignUpPayloadPB.fromObject({
|
||||
name: params.name,
|
||||
email: params.email,
|
||||
password: params.password,
|
||||
device_id: deviceId,
|
||||
device_id: params.deviceId,
|
||||
});
|
||||
|
||||
const res = await UserEventSignUp(payload);
|
||||
@ -43,4 +73,20 @@ export const AuthService = {
|
||||
signOut: () => {
|
||||
return UserEventSignOut();
|
||||
},
|
||||
|
||||
signIn: async (email: string, password: string) => {
|
||||
const payload = SignInPayloadPB.fromObject({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
const res = await UserEventSignInWithEmailPassword(payload);
|
||||
|
||||
if (!res.ok) {
|
||||
Log.error(res.val.msg);
|
||||
throw new Error(res.val.msg);
|
||||
}
|
||||
|
||||
return res.val;
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1,61 @@
|
||||
import * as React from 'react';
|
||||
import SpeedDial from '@mui/material/SpeedDial';
|
||||
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
|
||||
import SpeedDialAction from '@mui/material/SpeedDialAction';
|
||||
import { useMemo } from 'react';
|
||||
import { CloseOutlined, BuildOutlined, LoginOutlined, VisibilityOff } from '@mui/icons-material';
|
||||
import ManualSignInDialog from '$app/components/_shared/devtool/ManualSignInDialog';
|
||||
import { Portal } from '@mui/material';
|
||||
|
||||
function AppFlowyDevTool() {
|
||||
const [openManualSignIn, setOpenManualSignIn] = React.useState(false);
|
||||
const [hidden, setHidden] = React.useState(false);
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <LoginOutlined />,
|
||||
name: 'Manual SignIn',
|
||||
onClick: () => {
|
||||
setOpenManualSignIn(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <VisibilityOff />,
|
||||
name: 'Hide Dev Tool',
|
||||
onClick: () => {
|
||||
setHidden(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<SpeedDial
|
||||
hidden={hidden}
|
||||
direction={'left'}
|
||||
draggable={true}
|
||||
title={'AppFlowy Dev Tool'}
|
||||
ariaLabel='SpeedDial basic example'
|
||||
sx={{ position: 'absolute', zIndex: 1500, top: 64, right: 16 }}
|
||||
icon={<SpeedDialIcon className={'text-content-on-fill'} openIcon={<CloseOutlined />} icon={<BuildOutlined />} />}
|
||||
>
|
||||
{actions.map((action) => (
|
||||
<SpeedDialAction onClick={action.onClick} key={action.name} icon={action.icon} tooltipTitle={action.name} />
|
||||
))}
|
||||
|
||||
{openManualSignIn && (
|
||||
<ManualSignInDialog
|
||||
open={openManualSignIn}
|
||||
onClose={() => {
|
||||
setOpenManualSignIn(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SpeedDial>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppFlowyDevTool;
|
@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import { CircularProgress, DialogActions, DialogProps, Tab, Tabs, TextareaAutosize } from '@mui/material';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||
import TextField from '@mui/material/TextField';
|
||||
|
||||
function ManualSignInDialog(props: DialogProps) {
|
||||
const [uri, setUri] = React.useState('');
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { signInWithOAuth, signInWithEmailPassword } = useAuth();
|
||||
const [tab, setTab] = React.useState(0);
|
||||
const [email, setEmail] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [domain, setDomain] = React.useState('');
|
||||
const handleSignIn = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (tab === 1) {
|
||||
if (!email || !password) return;
|
||||
await signInWithEmailPassword(email, password, domain);
|
||||
} else {
|
||||
await signInWithOAuth(uri);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
props?.onClose?.({}, 'backdropClick');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...props}
|
||||
sx={{
|
||||
zIndex: 1500,
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void handleSignIn();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className={'pt-3'}>
|
||||
<Tabs
|
||||
className={'mb-4'}
|
||||
defaultValue={0}
|
||||
value={tab}
|
||||
onChange={(_, value) => {
|
||||
setTab(value);
|
||||
}}
|
||||
>
|
||||
<Tab value={0} label={'OAuth URI'} />
|
||||
<Tab value={1} label={'Email & Password'} />
|
||||
</Tabs>
|
||||
{tab === 1 ? (
|
||||
<div className={'flex flex-col gap-3'}>
|
||||
<TextField
|
||||
label={'Email'}
|
||||
size={'small'}
|
||||
required={true}
|
||||
placeholder={'name@gmail.com'}
|
||||
type={'email'}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
size={'small'}
|
||||
required={true}
|
||||
label={'Password'}
|
||||
placeholder={'Password'}
|
||||
type={'password'}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
size={'small'}
|
||||
label={'Domain(Optional)'}
|
||||
placeholder={'test.appflowy.cloud'}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<TextareaAutosize
|
||||
value={uri}
|
||||
autoFocus
|
||||
className={'max-h-[300px] w-[400px] overflow-hidden rounded-md border border-line-border p-2 text-xs'}
|
||||
placeholder={'Paste the OAuth URI here'}
|
||||
minRows={3}
|
||||
spellCheck={false}
|
||||
onChange={(e) => {
|
||||
setUri(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions className={'mb-4 w-full px-6'}>
|
||||
<Button
|
||||
size={'small'}
|
||||
variant={'outlined'}
|
||||
color={'inherit'}
|
||||
onClick={() => props?.onClose?.({}, 'backdropClick')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={loading} size={'small'} className={'w-auto'} variant={'outlined'} onClick={handleSignIn}>
|
||||
{loading ? <CircularProgress size={14} /> : 'Sign In'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ManualSignInDialog;
|
@ -1,26 +0,0 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import GoogleIcon from '$app/assets/settings/google.png';
|
||||
import GithubIcon from '$app/assets/settings/github.png';
|
||||
import DiscordIcon from '$app/assets/settings/discord.png';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LoginButtonGroup = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={'flex w-full flex-col items-center gap-4'}>
|
||||
<Button className={'w-full rounded-lg border-text-title py-3 text-sm'} color={'inherit'} variant={'outlined'}>
|
||||
<img src={GoogleIcon} alt={'Google'} className={'mr-2 h-6 w-6'} />
|
||||
{t('button.signInGoogle')}
|
||||
</Button>
|
||||
<Button className={'w-full rounded-lg border-text-title py-3 text-sm'} color={'inherit'} variant={'outlined'}>
|
||||
<img src={GithubIcon} alt={'Github'} className={'mr-2 h-6 w-6'} />
|
||||
{t('button.signInGithub')}
|
||||
</Button>
|
||||
<Button className={'w-full rounded-lg border-text-title py-3 text-sm'} color={'inherit'} variant={'outlined'}>
|
||||
<img src={DiscordIcon} alt={'Discord'} className={'mr-2 h-6 w-6'} />
|
||||
{t('button.signInDiscord')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from './LoginButtonGroup';
|
@ -0,0 +1,51 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import GoogleIcon from '$app/assets/settings/google.png';
|
||||
import GithubIcon from '$app/assets/settings/github.png';
|
||||
import DiscordIcon from '$app/assets/settings/discord.png';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||
import { ProviderTypePB } from '@/services/backend';
|
||||
|
||||
export const LoginButtonGroup = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { signIn } = useAuth();
|
||||
|
||||
return (
|
||||
<div className={'flex w-full flex-col items-center gap-4'}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
void signIn(ProviderTypePB.Google);
|
||||
}}
|
||||
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||
color={'inherit'}
|
||||
variant={'outlined'}
|
||||
>
|
||||
<img src={GoogleIcon} alt={'Google'} className={'mr-2 h-6 w-6'} />
|
||||
{t('button.signInGoogle')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
void signIn(ProviderTypePB.Github);
|
||||
}}
|
||||
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||
color={'inherit'}
|
||||
variant={'outlined'}
|
||||
>
|
||||
<img src={GithubIcon} alt={'Github'} className={'mr-2 h-6 w-6'} />
|
||||
{t('button.signInGithub')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
void signIn(ProviderTypePB.Discord);
|
||||
}}
|
||||
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||
color={'inherit'}
|
||||
variant={'outlined'}
|
||||
>
|
||||
<img src={DiscordIcon} alt={'Discord'} className={'mr-2 h-6 w-6'} />
|
||||
{t('button.signInDiscord')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -3,15 +3,24 @@ import { useAuth } from './auth.hooks';
|
||||
import Layout from '$app/components/layout/Layout';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Welcome } from '$app/components/auth/Welcome';
|
||||
import { ReactComponent as AppflowyLogo } from '$app/assets/logo.svg';
|
||||
import { isTauri } from '$app/utils/env';
|
||||
import { notify } from '$app/components/_shared/notify';
|
||||
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
||||
import { CircularProgress, Portal } from '@mui/material';
|
||||
import { ReactComponent as Logo } from '$app/assets/logo.svg';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
|
||||
export const ProtectedRoutes = () => {
|
||||
const { currentUser, checkUser, subscribeToUser } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { currentUser, checkUser, subscribeToUser, signInWithOAuth } = useAuth();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoading = currentUser?.loginState === LoginState.Loading;
|
||||
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const checkUserStatus = useCallback(async () => {
|
||||
await checkUser();
|
||||
setIsLoading(false);
|
||||
setChecked(true);
|
||||
}, [checkUser]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -24,21 +33,73 @@ export const ProtectedRoutes = () => {
|
||||
}
|
||||
}, [currentUser.isAuthenticated, subscribeToUser]);
|
||||
|
||||
if (isLoading) {
|
||||
// It's better to make a fading effect to disappear the loading page
|
||||
return <StartLoading />;
|
||||
} else {
|
||||
return <SplashScreen isAuthenticated={currentUser.isAuthenticated} />;
|
||||
}
|
||||
const onDeepLink = useCallback(async () => {
|
||||
if (!isTauri()) return;
|
||||
const { event } = await import('@tauri-apps/api');
|
||||
|
||||
// On macOS You still have to install a .app bundle you got from tauri build --debug for this to work!
|
||||
return await event.listen('open_deep_link', async (e) => {
|
||||
const payload = e.payload as string;
|
||||
|
||||
const [, hash] = payload.split('//#');
|
||||
const obj = parseHash(hash);
|
||||
|
||||
if (!obj.access_token) {
|
||||
notify.error('Failed to sign in, the access token is missing');
|
||||
dispatch(currentUserActions.setLoginState(LoginState.Error));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await signInWithOAuth(payload);
|
||||
} catch (e) {
|
||||
notify.error('Failed to sign in, please try again');
|
||||
}
|
||||
});
|
||||
}, [dispatch, signInWithOAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
void onDeepLink();
|
||||
}, [onDeepLink]);
|
||||
|
||||
return (
|
||||
<div className={'relative h-screen w-screen'}>
|
||||
{checked ? (
|
||||
<SplashScreen isAuthenticated={currentUser.isAuthenticated} />
|
||||
) : (
|
||||
<div className={'flex h-screen w-screen items-center justify-center'}>
|
||||
<Logo className={'h-20 w-20'} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && <StartLoading />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StartLoading = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const preventDefault = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
dispatch(currentUserActions.resetLoginState());
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', preventDefault, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', preventDefault, true);
|
||||
};
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<div className='flex h-screen w-full flex-col items-center justify-center'>
|
||||
<div className='h-40 w-40 justify-center'>
|
||||
<AppflowyLogo className={'h-24 w-24'} />
|
||||
<Portal>
|
||||
<div className={'fixed inset-0 z-[1400] flex h-full w-full items-center justify-center bg-bg-mask bg-opacity-50'}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
@ -53,3 +114,14 @@ const SplashScreen = ({ isAuthenticated }: { isAuthenticated: boolean }) => {
|
||||
return <Welcome />;
|
||||
}
|
||||
};
|
||||
|
||||
function parseHash(hash: string) {
|
||||
const hashParams = new URLSearchParams(hash);
|
||||
const hashObject: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of hashParams) {
|
||||
hashObject[key] = value;
|
||||
}
|
||||
|
||||
return hashObject;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ReactComponent as AppflowyLogo } from '$app/assets/logo.svg';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoginButtonGroup } from '$app/components/_shared/login';
|
||||
import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||
import { AuthenticatorPB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user';
|
||||
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
||||
import { AuthenticatorPB, ProviderTypePB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user';
|
||||
import { UserService } from '$app/application/user/user.service';
|
||||
import { AuthService } from '$app/application/user/auth.service';
|
||||
import { useAppSelector, useAppDispatch } from '$app/stores/store';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { getCurrentWorkspaceSetting } from '$app/application/folder/workspace.service';
|
||||
import { useCallback } from 'react';
|
||||
import { subscribeNotifications } from '$app/application/notification';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
|
||||
export const useAuth = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@ -31,72 +32,49 @@ export const useAuth = () => {
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// Check if the user is authenticated
|
||||
const checkUser = useCallback(async () => {
|
||||
const userProfile = await UserService.getUserProfile();
|
||||
const setUser = useCallback(
|
||||
async (userProfile?: Partial<UserProfilePB>) => {
|
||||
if (!userProfile) return;
|
||||
|
||||
if (!userProfile) return;
|
||||
const workspaceSetting = await getCurrentWorkspaceSetting();
|
||||
|
||||
const isLocal = userProfile.authenticator === AuthenticatorPB.Local;
|
||||
|
||||
dispatch(
|
||||
currentUserActions.checkUser({
|
||||
id: userProfile.id,
|
||||
token: userProfile.token,
|
||||
email: userProfile.email,
|
||||
displayName: userProfile.name,
|
||||
iconUrl: userProfile.icon_url,
|
||||
isAuthenticated: true,
|
||||
workspaceSetting: workspaceSetting,
|
||||
isLocal,
|
||||
})
|
||||
);
|
||||
|
||||
return userProfile;
|
||||
}, [dispatch]);
|
||||
|
||||
const register = useCallback(
|
||||
async (email: string, password: string, name: string): Promise<UserProfilePB> => {
|
||||
const userProfile = await AuthService.signUp({ email, password, name });
|
||||
|
||||
// Get the workspace setting after user registered. The workspace setting
|
||||
// contains the latest visiting page and the current workspace data.
|
||||
const workspaceSetting = await getCurrentWorkspaceSetting();
|
||||
|
||||
const isLocal = userProfile.authenticator === AuthenticatorPB.Local;
|
||||
|
||||
dispatch(
|
||||
currentUserActions.updateUser({
|
||||
id: userProfile.id,
|
||||
token: userProfile.token,
|
||||
email: userProfile.email,
|
||||
displayName: userProfile.name,
|
||||
iconUrl: userProfile.icon_url,
|
||||
isAuthenticated: true,
|
||||
workspaceSetting,
|
||||
workspaceSetting: workspaceSetting,
|
||||
isLocal,
|
||||
})
|
||||
);
|
||||
|
||||
return userProfile;
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const login = useCallback(
|
||||
async (email: string, password: string): Promise<UserProfilePB> => {
|
||||
const user = await AuthService.signIn({ email, password });
|
||||
const { id, token, name } = user;
|
||||
// Check if the user is authenticated
|
||||
const checkUser = useCallback(async () => {
|
||||
const userProfile = await UserService.getUserProfile();
|
||||
|
||||
dispatch(
|
||||
currentUserActions.updateUser({
|
||||
id: id,
|
||||
token: token,
|
||||
email,
|
||||
displayName: name,
|
||||
isAuthenticated: true,
|
||||
})
|
||||
);
|
||||
return user;
|
||||
await setUser(userProfile);
|
||||
|
||||
return userProfile;
|
||||
}, [setUser]);
|
||||
|
||||
const register = useCallback(
|
||||
async (email: string, password: string, name: string): Promise<UserProfilePB> => {
|
||||
const deviceId = currentUser?.deviceId ?? nanoid(8);
|
||||
const userProfile = await AuthService.signUp({ deviceId, email, password, name });
|
||||
|
||||
await setUser(userProfile);
|
||||
|
||||
return userProfile;
|
||||
},
|
||||
[dispatch]
|
||||
[setUser, currentUser?.deviceId]
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
@ -112,5 +90,97 @@ export const useAuth = () => {
|
||||
await register(fakeEmail, fakePassword, fakeName);
|
||||
}, [register]);
|
||||
|
||||
return { currentUser, checkUser, register, login, logout, subscribeToUser, signInAsAnonymous };
|
||||
const signIn = useCallback(
|
||||
async (provider: ProviderTypePB) => {
|
||||
dispatch(currentUserActions.setLoginState(LoginState.Loading));
|
||||
try {
|
||||
const url = await AuthService.getOAuthURL(provider);
|
||||
|
||||
await open(url);
|
||||
} catch {
|
||||
dispatch(currentUserActions.setLoginState(LoginState.Error));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const signInWithOAuth = useCallback(
|
||||
async (uri: string) => {
|
||||
dispatch(currentUserActions.setLoginState(LoginState.Loading));
|
||||
try {
|
||||
const deviceId = currentUser?.deviceId ?? nanoid(8);
|
||||
|
||||
await AuthService.signInWithOAuth({ uri, deviceId });
|
||||
const userProfile = await UserService.getUserProfile();
|
||||
|
||||
await setUser(userProfile);
|
||||
|
||||
return userProfile;
|
||||
} catch (e) {
|
||||
dispatch(currentUserActions.setLoginState(LoginState.Error));
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[dispatch, currentUser?.deviceId, setUser]
|
||||
);
|
||||
|
||||
// Only for development purposes
|
||||
const signInWithEmailPassword = useCallback(
|
||||
async (email: string, password: string, domain?: string) => {
|
||||
dispatch(currentUserActions.setLoginState(LoginState.Loading));
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://${domain ? domain : 'test.appflowy.cloud'}/gotrue/token?grant_type=password`,
|
||||
{
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
redirect: 'follow',
|
||||
referrerPolicy: 'no-referrer',
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
let uri = `appflowy-flutter://#`;
|
||||
const params: string[] = [];
|
||||
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (typeof data[key] === 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
params.push(`${key}=${data[key]}`);
|
||||
});
|
||||
uri += params.join('&');
|
||||
|
||||
return signInWithOAuth(uri);
|
||||
} catch (e) {
|
||||
dispatch(currentUserActions.setLoginState(LoginState.Error));
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[dispatch, signInWithOAuth]
|
||||
);
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
checkUser,
|
||||
register,
|
||||
logout,
|
||||
subscribeToUser,
|
||||
signInAsAnonymous,
|
||||
signIn,
|
||||
signInWithOAuth,
|
||||
signInWithEmailPassword,
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { workspaceActions, WorkspaceItem } from '$app_reducers/workspace/slice';
|
||||
import { Page, pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||
@ -10,29 +10,29 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export function useLoadWorkspaces() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { workspaces, currentWorkspace } = useAppSelector((state) => state.workspace);
|
||||
const { workspaces, currentWorkspaceId } = useAppSelector((state) => state.workspace);
|
||||
|
||||
const currentWorkspace = useMemo(() => {
|
||||
return workspaces.find((workspace) => workspace.id === currentWorkspaceId);
|
||||
}, [workspaces, currentWorkspaceId]);
|
||||
|
||||
const initializeWorkspaces = useCallback(async () => {
|
||||
const workspaces = await workspaceService.getWorkspaces();
|
||||
const currentWorkspace = await workspaceService.getCurrentWorkspace();
|
||||
|
||||
const currentWorkspaceId = await workspaceService.getCurrentWorkspace();
|
||||
|
||||
dispatch(
|
||||
workspaceActions.initWorkspaces({
|
||||
workspaces,
|
||||
currentWorkspace,
|
||||
currentWorkspaceId,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
await initializeWorkspaces();
|
||||
})();
|
||||
}, [initializeWorkspaces]);
|
||||
|
||||
return {
|
||||
workspaces,
|
||||
currentWorkspace,
|
||||
initializeWorkspaces,
|
||||
};
|
||||
}
|
||||
|
||||
@ -82,8 +82,10 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
|
||||
{
|
||||
[FolderNotification.DidUpdateWorkspace]: async (changeset) => {
|
||||
dispatch(
|
||||
workspaceActions.updateCurrentWorkspace({
|
||||
workspaceActions.updateWorkspace({
|
||||
id: String(changeset.id),
|
||||
name: changeset.name,
|
||||
icon: changeset.icon_url,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
@ -43,8 +43,20 @@ function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: bo
|
||||
>
|
||||
<Tooltip disableInteractive={true} placement={'top-start'} title={t('sideBar.clickToHidePersonal')}>
|
||||
<div className={'flex items-center gap-2 rounded px-2 py-1 text-xs font-medium hover:bg-fill-list-active'}>
|
||||
<WorkplaceAvatar workplaceName={workspace.name} width={18} height={18} className={'text-[70%]'} />
|
||||
{workspace.name}
|
||||
{!workspace.name ? (
|
||||
t('sideBar.personal')
|
||||
) : (
|
||||
<>
|
||||
<WorkplaceAvatar
|
||||
icon={workspace.icon}
|
||||
workplaceName={workspace.name}
|
||||
width={18}
|
||||
height={18}
|
||||
className={'text-[70%]'}
|
||||
/>
|
||||
{workspace.name}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{showAdd && (
|
||||
|
@ -1,11 +1,21 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import NewPageButton from '$app/components/layout/workspace_manager/NewPageButton';
|
||||
import { useLoadWorkspaces } from '$app/components/layout/workspace_manager/Workspace.hooks';
|
||||
import Workspace from './Workspace';
|
||||
import TrashButton from '$app/components/layout/workspace_manager/TrashButton';
|
||||
import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||
import { LoginState } from '$app_reducers/current-user/slice';
|
||||
|
||||
function WorkspaceManager() {
|
||||
const { workspaces, currentWorkspace } = useLoadWorkspaces();
|
||||
const { workspaces, currentWorkspace, initializeWorkspaces } = useLoadWorkspaces();
|
||||
|
||||
const loginState = useAppSelector((state) => state.currentUser.loginState);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginState === LoginState.Success || loginState === undefined) {
|
||||
void initializeWorkspaces();
|
||||
}
|
||||
}, [initializeWorkspaces, loginState]);
|
||||
|
||||
return (
|
||||
<div className={'workspaces flex h-full select-none flex-col justify-between'}>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { LoginButtonGroup } from '$app/components/_shared/login';
|
||||
import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup';
|
||||
|
||||
export const Login = ({ onBack }: { onBack?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import Dialog, { DialogProps } from '@mui/material/Dialog';
|
||||
import { Settings } from '$app/components/settings/Settings';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { ReactComponent as CloseIcon } from '$app/assets/close.svg';
|
||||
@ -14,10 +14,15 @@ import { SettingsRoutes } from '$app/components/settings/workplace/const';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { Login } from '$app/components/settings/Login';
|
||||
import SwipeableViews from 'react-swipeable-views';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const SettingsDialog = (props: DialogProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [routes, setRoutes] = useState<SettingsRoutes[]>([]);
|
||||
|
||||
const loginState = useAppSelector((state) => state.currentUser.loginState);
|
||||
const lastLoginStateRef = useRef(loginState);
|
||||
const { t } = useTranslation();
|
||||
const handleForward = useCallback((route: SettingsRoutes) => {
|
||||
setRoutes((prev) => {
|
||||
@ -29,14 +34,28 @@ export const SettingsDialog = (props: DialogProps) => {
|
||||
setRoutes((prevState) => {
|
||||
return prevState.slice(0, -1);
|
||||
});
|
||||
}, []);
|
||||
dispatch(currentUserActions.resetLoginState());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClose = () => {
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(currentUserActions.resetLoginState());
|
||||
props?.onClose?.({}, 'backdropClick');
|
||||
};
|
||||
}, [dispatch, props]);
|
||||
|
||||
const currentRoute = routes[routes.length - 1];
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (lastLoginStateRef.current === LoginState.Loading && loginState === LoginState.Success) {
|
||||
navigate('/');
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
|
||||
lastLoginStateRef.current = loginState;
|
||||
}, [loginState, handleClose, navigate]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...props}
|
||||
|
@ -4,9 +4,13 @@ import Button from '@mui/material/Button';
|
||||
import { Divider } from '@mui/material';
|
||||
import { DeleteAccount } from '$app/components/settings/my_account/DeleteAccount';
|
||||
import { SettingsRoutes } from '$app/components/settings/workplace/const';
|
||||
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||
|
||||
export const AccountLogin = ({ onForward }: { onForward?: (route: SettingsRoutes) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentUser, logout } = useAuth();
|
||||
|
||||
const isLocal = currentUser.isLocal;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -15,12 +19,17 @@ export const AccountLogin = ({ onForward }: { onForward?: (route: SettingsRoutes
|
||||
{t('newSettings.myAccount.accountLogin')}
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onForward?.(SettingsRoutes.LOGIN);
|
||||
onClick={async () => {
|
||||
if (isLocal) {
|
||||
onForward?.(SettingsRoutes.LOGIN);
|
||||
return;
|
||||
}
|
||||
|
||||
await logout();
|
||||
}}
|
||||
variant={'contained'}
|
||||
>
|
||||
{t('button.login')}
|
||||
{!isLocal ? t('button.logout') : t('button.login')}
|
||||
</Button>
|
||||
<Divider className={'my-4'} />
|
||||
<DeleteAccount />
|
||||
|
@ -1,39 +1,65 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Divider, OutlinedInput } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { changeWorkspaceIcon, renameWorkspace } from '$app/application/folder/workspace.service';
|
||||
import { notify } from '$app/components/_shared/notify';
|
||||
import { WorkplaceAvatar } from '$app/components/_shared/avatar';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { PopoverCommonProps } from '$app/components/editor/components/tools/popover';
|
||||
import EmojiPicker from '$app/components/_shared/emoji_picker/EmojiPicker';
|
||||
import { workspaceActions } from '$app_reducers/workspace/slice';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
|
||||
export const WorkplaceDisplay = () => {
|
||||
const { t } = useTranslation();
|
||||
const isLocal = useAppSelector((state) => state.currentUser.isLocal);
|
||||
const workspace = useAppSelector((state) => state.workspace.currentWorkspace);
|
||||
const { workspaces, currentWorkspaceId } = useAppSelector((state) => state.workspace);
|
||||
const workspace = useMemo(
|
||||
() => workspaces.find((workspace) => workspace.id === currentWorkspaceId),
|
||||
[workspaces, currentWorkspaceId]
|
||||
);
|
||||
const [name, setName] = useState(workspace?.name ?? '');
|
||||
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null);
|
||||
const openEmojiPicker = Boolean(emojiPickerAnchor);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const debounceUpdateWorkspace = useMemo(() => {
|
||||
return debounce(async ({ id, name, icon }: { id: string; name?: string; icon?: string }) => {
|
||||
if (!id || !name) return;
|
||||
|
||||
if (icon) {
|
||||
try {
|
||||
await changeWorkspaceIcon(id, icon);
|
||||
} catch {
|
||||
notify.error(t('newSettings.workplace.updateIconError'));
|
||||
}
|
||||
}
|
||||
|
||||
if (name) {
|
||||
try {
|
||||
await renameWorkspace(id, name);
|
||||
} catch {
|
||||
notify.error(t('newSettings.workplace.renameError'));
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}, [t]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!workspace || !name) return;
|
||||
try {
|
||||
await renameWorkspace(workspace.id, name);
|
||||
} catch {
|
||||
notify.error(t('newSettings.workplace.renameError'));
|
||||
}
|
||||
dispatch(workspaceActions.updateWorkspace({ id: workspace.id, name }));
|
||||
|
||||
await debounceUpdateWorkspace({ id: workspace.id, name });
|
||||
};
|
||||
|
||||
const handleEmojiSelect = async (icon: string) => {
|
||||
if (!workspace) return;
|
||||
try {
|
||||
await changeWorkspaceIcon(workspace.id, icon);
|
||||
} catch {
|
||||
notify.error(t('newSettings.workplace.updateIconError'));
|
||||
}
|
||||
dispatch(workspaceActions.updateWorkspace({ id: workspace.id, icon }));
|
||||
|
||||
await debounceUpdateWorkspace({ id: workspace.id, icon });
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@ -93,6 +119,7 @@ export const WorkplaceDisplay = () => {
|
||||
workplaceName={name}
|
||||
width={62}
|
||||
height={62}
|
||||
icon={workspace?.icon}
|
||||
className={'rounded-lg border border-bg-body p-[2px] hover:opacity-90'}
|
||||
/>
|
||||
</Button>
|
||||
|
@ -17,8 +17,15 @@ export enum Theme {
|
||||
Lavender = 'lavender',
|
||||
}
|
||||
|
||||
export enum LoginState {
|
||||
Loading = 'loading',
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export interface ICurrentUser {
|
||||
id?: number;
|
||||
deviceId?: string;
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
token?: string;
|
||||
@ -27,6 +34,7 @@ export interface ICurrentUser {
|
||||
workspaceSetting?: WorkspaceSettingPB;
|
||||
userSetting: UserSetting;
|
||||
isLocal: boolean;
|
||||
loginState?: LoginState;
|
||||
}
|
||||
|
||||
const initialState: ICurrentUser | null = {
|
||||
@ -39,17 +47,11 @@ export const currentUserSlice = createSlice({
|
||||
name: 'currentUser',
|
||||
initialState: initialState,
|
||||
reducers: {
|
||||
checkUser: (state, action: PayloadAction<Partial<ICurrentUser>>) => {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
},
|
||||
|
||||
updateUser: (state, action: PayloadAction<Partial<ICurrentUser>>) => {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
loginState: LoginState.Success,
|
||||
};
|
||||
},
|
||||
logout: () => {
|
||||
@ -61,6 +63,14 @@ export const currentUserSlice = createSlice({
|
||||
...action.payload,
|
||||
};
|
||||
},
|
||||
|
||||
setLoginState: (state, action: PayloadAction<LoginState>) => {
|
||||
state.loginState = action.payload;
|
||||
},
|
||||
|
||||
resetLoginState: (state) => {
|
||||
state.loginState = undefined;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -3,16 +3,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
export interface WorkspaceItem {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface WorkspaceState {
|
||||
workspaces: WorkspaceItem[];
|
||||
currentWorkspace: WorkspaceItem | null;
|
||||
currentWorkspaceId: string | null;
|
||||
}
|
||||
|
||||
const initialState: WorkspaceState = {
|
||||
workspaces: [],
|
||||
currentWorkspace: null,
|
||||
currentWorkspaceId: null,
|
||||
};
|
||||
|
||||
export const workspaceSlice = createSlice({
|
||||
@ -23,18 +24,21 @@ export const workspaceSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
workspaces: WorkspaceItem[];
|
||||
currentWorkspace: WorkspaceItem | null;
|
||||
currentWorkspaceId: string | null;
|
||||
}>
|
||||
) => {
|
||||
return action.payload;
|
||||
},
|
||||
|
||||
updateCurrentWorkspace: (state, action: PayloadAction<Partial<WorkspaceItem>>) => {
|
||||
if (!state.currentWorkspace) return;
|
||||
state.currentWorkspace = {
|
||||
...state.currentWorkspace,
|
||||
...action.payload,
|
||||
};
|
||||
updateWorkspace: (state, action: PayloadAction<Partial<WorkspaceItem>>) => {
|
||||
const index = state.workspaces.findIndex((workspace) => workspace.id === action.payload.id);
|
||||
|
||||
if (index !== -1) {
|
||||
state.workspaces[index] = {
|
||||
...state.workspaces[index],
|
||||
...action.payload,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -22,10 +22,29 @@ export const colorMap = {
|
||||
[ColorEnum.Blue]: 'var(--tint-blue)',
|
||||
};
|
||||
|
||||
// Convert ARGB to RGBA
|
||||
// Flutter uses ARGB, but CSS uses RGBA
|
||||
function argbToRgba(color: string): string {
|
||||
const hex = color.replace(/^#|0x/, '');
|
||||
|
||||
const hasAlpha = hex.length === 8;
|
||||
|
||||
if (!hasAlpha) {
|
||||
return color.replace('0x', '#');
|
||||
}
|
||||
|
||||
const r = parseInt(hex.slice(2, 4), 16);
|
||||
const g = parseInt(hex.slice(4, 6), 16);
|
||||
const b = parseInt(hex.slice(6, 8), 16);
|
||||
const a = hasAlpha ? parseInt(hex.slice(0, 2), 16) / 255 : 1;
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
export function renderColor(color: string) {
|
||||
if (colorMap[color as ColorEnum]) {
|
||||
return colorMap[color as ColorEnum];
|
||||
}
|
||||
|
||||
return color.replace('0x', '#');
|
||||
return argbToRgba(color);
|
||||
}
|
||||
|
@ -695,6 +695,7 @@ impl UserManager {
|
||||
|
||||
save_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?;
|
||||
event!(tracing::Level::INFO, "Save new user profile to disk");
|
||||
|
||||
self.authenticate_user.set_session(Some(session.clone()))?;
|
||||
self
|
||||
.save_user(uid, (user_profile, authenticator.clone()).into())
|
||||
|
Loading…
Reference in New Issue
Block a user