diff --git a/packages/common/infra/package.json b/packages/common/infra/package.json index d780d7cdf6..4c32da0b13 100644 --- a/packages/common/infra/package.json +++ b/packages/common/infra/package.json @@ -40,6 +40,11 @@ "import": "./dist/type.js", "require": "./dist/type.cjs" }, + "./app-config-storage": { + "type": "./dist/src/app-config-storage.d.ts", + "import": "./dist/app-config-storage.js", + "require": "./dist/app-config-storage.cjs" + }, "./__internal__/*": { "type": "./dist/src/__internal__/*.d.ts", "import": "./dist/__internal__/*.js", diff --git a/packages/common/infra/src/app-config-storage.ts b/packages/common/infra/src/app-config-storage.ts new file mode 100644 index 0000000000..87c167c7da --- /dev/null +++ b/packages/common/infra/src/app-config-storage.ts @@ -0,0 +1,88 @@ +import { z } from 'zod'; + +const _appConfigSchema = z.object({ + /** whether to show onboarding first */ + onBoarding: z.boolean().optional().default(true), +}); +export type AppConfigSchema = z.infer; +export const defaultAppConfig = _appConfigSchema.parse({}); + +const _storage: Record = {}; +let _inMemoryId = 0; + +interface StorageOptions { + /** default config */ + config: T; + get?: () => T; + set?: (data: T) => void; +} + +/** + * Storage for app configuration, stored in memory by default + */ +class Storage { + private _cfg: T; + private readonly _id = _inMemoryId++; + private readonly _options; + + constructor(options: StorageOptions) { + this._options = { + get: () => _storage[this._id], + set: (data: T) => (_storage[this._id] = data), + ...options, + }; + this._cfg = this.get() ?? options.config; + } + + /** + * update entire config + * @param data + */ + set(data: T) { + try { + this._options.set(data); + } catch (err) { + console.error('failed to save config', err); + } + this._cfg = data; + } + + get(): T; + get(key: keyof T): T[keyof T]; + /** + * get config, if key is provided, return the value of the key + * @param key + * @returns + */ + get(key?: keyof T): T | T[keyof T] { + if (!key) { + try { + const cfg = this._options.get(); + if (!cfg) { + this.set(this._options.config); + return this._options.config; + } + return cfg; + } catch (err) { + return this._cfg; + } + } else { + return this.get()[key]; + } + } + + /** + * update a key in config + * @param key + * @param value + */ + patch(key: keyof T, value: any) { + this.set({ ...this.get(), [key]: value }); + } + + get value(): T { + return this.get(); + } +} + +export class AppConfigStorage extends Storage {} diff --git a/packages/common/infra/src/handler.ts b/packages/common/infra/src/handler.ts index 3be490d310..722fc13890 100644 --- a/packages/common/infra/src/handler.ts +++ b/packages/common/infra/src/handler.ts @@ -1,5 +1,6 @@ import type { ClipboardHandlers, + ConfigStorageHandlers, DBHandlers, DebugHandlers, DialogHandlers, @@ -49,3 +50,8 @@ export abstract class WorkspaceHandlerManager extends HandlerManager< 'workspace', WorkspaceHandlers > {} + +export abstract class ConfigStorageHandlerManager extends HandlerManager< + 'configStorage', + ConfigStorageHandlers +> {} diff --git a/packages/common/infra/src/type.ts b/packages/common/infra/src/type.ts index 732c8d1d4d..cdda8bfe28 100644 --- a/packages/common/infra/src/type.ts +++ b/packages/common/infra/src/type.ts @@ -3,6 +3,7 @@ import type Buffer from 'buffer'; import type { WritableAtom } from 'jotai'; import { z } from 'zod'; +import type { AppConfigSchema } from './app-config-storage.js'; import type { TypedEventEmitter } from './core/event-emitter.js'; type Buffer = Buffer.Buffer; @@ -175,6 +176,7 @@ export type UIHandlers = { handleCloseApp: () => Promise; getGoogleOauthCode: () => Promise; getChallengeResponse: (resource: string) => Promise; + handleOpenMainApp: () => Promise; }; export type ClipboardHandlers = { @@ -211,6 +213,11 @@ export type WorkspaceHandlers = { clone: (id: string, newId: string) => Promise; }; +export type ConfigStorageHandlers = { + set: (config: AppConfigSchema | Partial) => Promise; + get: () => Promise; +}; + export type UnwrapManagerHandlerToServerSide< ElectronEvent extends { frameId: number; diff --git a/packages/common/infra/vite.config.ts b/packages/common/infra/vite.config.ts index 3f9455e2c3..03baf6f2a7 100644 --- a/packages/common/infra/vite.config.ts +++ b/packages/common/infra/vite.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ type: resolve(root, 'src/type.ts'), 'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'), 'preload/electron': resolve(root, 'src/preload/electron.ts'), + 'app-config-storage': resolve(root, 'src/app-config-storage.ts'), '__internal__/plugin': resolve(root, 'src/__internal__/plugin.ts'), }, formats: ['es', 'cjs'], diff --git a/packages/frontend/core/src/bootstrap/setup.ts b/packages/frontend/core/src/bootstrap/setup.ts index e7fa269453..4129e608f9 100644 --- a/packages/frontend/core/src/bootstrap/setup.ts +++ b/packages/frontend/core/src/bootstrap/setup.ts @@ -10,6 +10,7 @@ import { useNavigationType, } from 'react-router-dom'; +import { appConfigProxy } from '../hooks/use-app-config-storage'; import { performanceLogger } from '../shared'; const performanceSetupLogger = performanceLogger.namespace('setup'); @@ -47,5 +48,12 @@ export function setup() { }); } + // load persistent config for electron + // TODO: should be sync, but it's not necessary for now + environment.isDesktop && + appConfigProxy + .getSync() + .catch(() => console.error('failed to load app config')); + performanceSetupLogger.info('done'); } diff --git a/packages/frontend/core/src/hooks/use-app-config-storage.ts b/packages/frontend/core/src/hooks/use-app-config-storage.ts new file mode 100644 index 0000000000..35b94fc292 --- /dev/null +++ b/packages/frontend/core/src/hooks/use-app-config-storage.ts @@ -0,0 +1,83 @@ +import { + type AppConfigSchema, + AppConfigStorage, + defaultAppConfig, +} from '@toeverything/infra/app-config-storage'; +import { type Dispatch, useEffect, useState } from 'react'; +import { useMemo } from 'react'; + +/** + * Helper class to get/set app config from main process + */ +class AppConfigProxy { + value: AppConfigSchema = defaultAppConfig; + + async getSync(): Promise { + return (this.value = await window.apis.configStorage.get()); + } + + async setSync(): Promise { + await window.apis.configStorage.set(this.value); + } + + get(): AppConfigSchema { + return this.value; + } + + set(data: AppConfigSchema) { + this.value = data; + this.setSync().catch(console.error); + } +} +export const appConfigProxy = new AppConfigProxy(); + +const storage = environment.isDesktop + ? new AppConfigStorage({ + config: defaultAppConfig, + get: () => appConfigProxy.get(), + set: v => appConfigProxy.set(v), + }) + : new AppConfigStorage({ + config: defaultAppConfig, + get: () => JSON.parse(localStorage.getItem('app_config') ?? 'null'), + set: config => localStorage.setItem('app_config', JSON.stringify(config)), + }); + +export const appConfigStorage = storage; + +export function useAppConfigStorage(): [ + AppConfigSchema, + Dispatch, +]; +export function useAppConfigStorage( + key: keyof AppConfigSchema +): [AppConfigSchema[typeof key], Dispatch]; + +/** + * Get reactive app config + * @param key + * @returns + */ +export function useAppConfigStorage(key?: keyof AppConfigSchema) { + const [_config, _setConfig] = useState(storage.get()); + + useEffect(() => { + storage.set(_config); + }, [_config]); + + const value = useMemo(() => (key ? _config[key] : _config), [_config, key]); + + const setValue = useMemo(() => { + if (key) { + return (value: AppConfigSchema[typeof key]) => { + _setConfig(cfg => ({ ...cfg, [key]: value })); + }; + } else { + return (config: AppConfigSchema) => { + _setConfig(config); + }; + } + }, [_setConfig, key]); + + return [value, setValue]; +} diff --git a/packages/frontend/core/src/pages/index.tsx b/packages/frontend/core/src/pages/index.tsx index eb873e866e..110903a03f 100644 --- a/packages/frontend/core/src/pages/index.tsx +++ b/packages/frontend/core/src/pages/index.tsx @@ -2,9 +2,11 @@ import { Menu } from '@affine/component/ui/menu'; import { workspaceListAtom } from '@affine/workspace/atom'; import { useAtomValue } from 'jotai'; import { lazy, useEffect } from 'react'; +import { type LoaderFunction, redirect } from 'react-router-dom'; import { createFirstAppData } from '../bootstrap/first-app-data'; import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list'; +import { appConfigStorage } from '../hooks/use-app-config-storage'; import { useNavigateHelper } from '../hooks/use-navigate-helper'; import { WorkspaceSubPath } from '../shared'; @@ -14,6 +16,13 @@ const AllWorkspaceModals = lazy(() => })) ); +export const loader: LoaderFunction = async () => { + if (!environment.isDesktop && appConfigStorage.get('onBoarding')) { + return redirect('/onboarding'); + } + return null; +}; + export const Component = () => { const list = useAtomValue(workspaceListAtom); const { openPage } = useNavigateHelper(); diff --git a/packages/frontend/core/src/pages/onboarding.tsx b/packages/frontend/core/src/pages/onboarding.tsx new file mode 100644 index 0000000000..ccdb8ce6f0 --- /dev/null +++ b/packages/frontend/core/src/pages/onboarding.tsx @@ -0,0 +1,52 @@ +import { Button } from '@affine/component/ui/button'; +import { redirect } from 'react-router-dom'; + +import { + appConfigStorage, + useAppConfigStorage, +} from '../hooks/use-app-config-storage'; +import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper'; + +export const loader = () => { + if (!environment.isDesktop && !appConfigStorage.get('onBoarding')) { + // onboarding is off, redirect to index + return redirect('/'); + } + + return null; +}; + +export const Component = () => { + const { jumpToIndex } = useNavigateHelper(); + const [onBoarding, setOnboarding] = useAppConfigStorage('onBoarding'); + + const openApp = () => { + if (environment.isDesktop) { + window.apis.ui.handleOpenMainApp().catch(err => { + console.log('failed to open main app', err); + }); + } else { + jumpToIndex(RouteLogic.REPLACE); + setOnboarding(false); + } + }; + + return ( +
+ + onboarding page, onboarding mode is {onBoarding ? 'on' : 'off'} + +
+ ); +}; diff --git a/packages/frontend/core/src/router.ts b/packages/frontend/core/src/router.ts index e5ca5890cd..99ac0e2f2e 100644 --- a/packages/frontend/core/src/router.ts +++ b/packages/frontend/core/src/router.ts @@ -65,6 +65,10 @@ export const routes = [ path: '/desktop-signin', lazy: () => import('./pages/desktop-signin'), }, + { + path: '/onboarding', + lazy: () => import('./pages/onboarding'), + }, { path: '*', lazy: () => import('./pages/404'), diff --git a/packages/frontend/electron/src/main/config-storage/handlers.ts b/packages/frontend/electron/src/main/config-storage/handlers.ts new file mode 100644 index 0000000000..dcae3c570c --- /dev/null +++ b/packages/frontend/electron/src/main/config-storage/handlers.ts @@ -0,0 +1,7 @@ +import type { NamespaceHandlers } from '../type'; +import { persistentConfig } from './persist'; + +export const configStorageHandlers = { + get: async () => persistentConfig.get(), + set: async (_, v) => persistentConfig.set(v), +} satisfies NamespaceHandlers; diff --git a/packages/frontend/electron/src/main/config-storage/index.ts b/packages/frontend/electron/src/main/config-storage/index.ts new file mode 100644 index 0000000000..6c6f862d01 --- /dev/null +++ b/packages/frontend/electron/src/main/config-storage/index.ts @@ -0,0 +1 @@ +export * from './handlers'; diff --git a/packages/frontend/electron/src/main/config-storage/persist.ts b/packages/frontend/electron/src/main/config-storage/persist.ts new file mode 100644 index 0000000000..fe3621ee8a --- /dev/null +++ b/packages/frontend/electron/src/main/config-storage/persist.ts @@ -0,0 +1,16 @@ +import { + AppConfigStorage, + defaultAppConfig, +} from '@toeverything/infra/app-config-storage'; +import { app } from 'electron'; +import fs from 'fs'; +import path from 'path'; + +const FILENAME = 'config.json'; +const FILEPATH = path.join(app.getPath('userData'), FILENAME); + +export const persistentConfig = new AppConfigStorage({ + config: defaultAppConfig, + get: () => JSON.parse(fs.readFileSync(FILEPATH, 'utf-8')), + set: data => fs.writeFileSync(FILEPATH, JSON.stringify(data, null, 2)), +}); diff --git a/packages/frontend/electron/src/main/constants.ts b/packages/frontend/electron/src/main/constants.ts new file mode 100644 index 0000000000..b421456e52 --- /dev/null +++ b/packages/frontend/electron/src/main/constants.ts @@ -0,0 +1 @@ +export const mainWindowOrigin = process.env.DEV_SERVER_URL || 'file://.'; diff --git a/packages/frontend/electron/src/main/deep-link.ts b/packages/frontend/electron/src/main/deep-link.ts index d9d263cbd7..93ee4739b0 100644 --- a/packages/frontend/electron/src/main/deep-link.ts +++ b/packages/frontend/electron/src/main/deep-link.ts @@ -3,11 +3,11 @@ import path from 'node:path'; import { type App, type BrowserWindow, ipcMain } from 'electron'; import { buildType, CLOUD_BASE_URL, isDev } from './config'; +import { mainWindowOrigin } from './constants'; import { logger } from './logger'; import { getMainWindow, handleOpenUrlInHiddenWindow, - mainWindowOrigin, removeCookie, setCookie, } from './main-window'; diff --git a/packages/frontend/electron/src/main/events.ts b/packages/frontend/electron/src/main/events.ts index dacb86107d..473366e5d8 100644 --- a/packages/frontend/electron/src/main/events.ts +++ b/packages/frontend/electron/src/main/events.ts @@ -2,7 +2,7 @@ import { app, BrowserWindow } from 'electron'; import { applicationMenuEvents } from './application-menu'; import { logger } from './logger'; -import { uiEvents } from './ui'; +import { uiEvents } from './ui/events'; import { updaterEvents } from './updater/event'; export const allEvents = { diff --git a/packages/frontend/electron/src/main/handlers.ts b/packages/frontend/electron/src/main/handlers.ts index 0f3fe497bf..985ce0946f 100644 --- a/packages/frontend/electron/src/main/handlers.ts +++ b/packages/frontend/electron/src/main/handlers.ts @@ -1,5 +1,6 @@ import type { ClipboardHandlerManager, + ConfigStorageHandlerManager, DebugHandlerManager, ExportHandlerManager, UIHandlerManager, @@ -9,9 +10,10 @@ import type { import { ipcMain } from 'electron'; import { clipboardHandlers } from './clipboard'; +import { configStorageHandlers } from './config-storage'; import { exportHandlers } from './export'; import { getLogFilePath, logger, revealLogFile } from './logger'; -import { uiHandlers } from './ui'; +import { uiHandlers } from './ui/handlers'; import { updaterHandlers } from './updater'; export const debugHandlers = { @@ -44,6 +46,10 @@ type AllHandlers = { Electron.IpcMainInvokeEvent, UpdaterHandlerManager >; + configStorage: UnwrapManagerHandlerToServerSide< + Electron.IpcMainInvokeEvent, + ConfigStorageHandlerManager + >; }; // Note: all of these handlers will be the single-source-of-truth for the apis exposed to the renderer process @@ -53,6 +59,7 @@ export const allHandlers = { clipboard: clipboardHandlers, export: exportHandlers, updater: updaterHandlers, + configStorage: configStorageHandlers, } satisfies AllHandlers; export const registerHandlers = () => { diff --git a/packages/frontend/electron/src/main/index.ts b/packages/frontend/electron/src/main/index.ts index 321c04d790..eb2788f327 100644 --- a/packages/frontend/electron/src/main/index.ts +++ b/packages/frontend/electron/src/main/index.ts @@ -11,9 +11,10 @@ import { registerEvents } from './events'; import { registerHandlers } from './handlers'; import { ensureHelperProcess } from './helper-process'; import { logger } from './logger'; -import { initMainWindow as initMainWindow } from './main-window'; import { registerProtocol } from './protocol'; import { registerUpdater } from './updater'; +import { launch } from './windows-manager/launcher'; +import { launchStage } from './windows-manager/stage'; app.enableSandbox(); @@ -26,11 +27,9 @@ if (overrideSession) { } if (require('electron-squirrel-startup')) app.quit(); -// allow tests to overwrite app name through passing args -if (process.argv.includes('--app-name')) { - const appNameIndex = process.argv.indexOf('--app-name'); - const appName = process.argv[appNameIndex + 1]; - app.setName(appName); + +if (process.env.SKIP_ONBOARDING) { + launchStage.value = 'main'; } /** @@ -53,13 +52,9 @@ app.on('window-all-closed', () => { }); /** - * @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate' + * @see https://www.electronjs.org/docs/latest/api/app#event-activate-macos Event: 'activate' */ -app.on('activate', () => { - initMainWindow().catch(e => - console.error('Failed to restore or create window:', e) - ); -}); +app.on('activate', launch); setupDeepLink(app); @@ -72,7 +67,7 @@ app .then(registerHandlers) .then(registerEvents) .then(ensureHelperProcess) - .then(initMainWindow) + .then(launch) .then(createApplicationMenu) .then(registerUpdater) .catch(e => console.error('Failed create window:', e)); diff --git a/packages/frontend/electron/src/main/main-window.ts b/packages/frontend/electron/src/main/main-window.ts index ad31168b6a..1bf82f90b9 100644 --- a/packages/frontend/electron/src/main/main-window.ts +++ b/packages/frontend/electron/src/main/main-window.ts @@ -5,6 +5,7 @@ import electronWindowState from 'electron-window-state'; import { join } from 'path'; import { isMacOS, isWindows } from '../shared/utils'; +import { mainWindowOrigin } from './constants'; import { ensureHelperProcess } from './helper-process'; import { logger } from './logger'; import { uiSubjects } from './ui/subject'; @@ -15,8 +16,6 @@ const IS_DEV: boolean = const DEV_TOOL = process.env.DEV_TOOL === 'true'; -export const mainWindowOrigin = process.env.DEV_SERVER_URL || 'file://.'; - // todo: not all window need all of the exposed meta const getWindowAdditionalArguments = async () => { const { getExposedMeta } = await import('./exposed'); diff --git a/packages/frontend/electron/src/main/onboarding.ts b/packages/frontend/electron/src/main/onboarding.ts new file mode 100644 index 0000000000..c7e1297453 --- /dev/null +++ b/packages/frontend/electron/src/main/onboarding.ts @@ -0,0 +1,78 @@ +import { assert } from 'console'; +import { BrowserWindow } from 'electron'; +import { join } from 'path'; + +import { mainWindowOrigin } from './constants'; +// import { getExposedMeta } from './exposed'; +import { ensureHelperProcess } from './helper-process'; +import { logger } from './logger'; + +// todo: not all window need all of the exposed meta +const getWindowAdditionalArguments = async () => { + const { getExposedMeta } = await import('./exposed'); + const mainExposedMeta = getExposedMeta(); + const helperProcessManager = await ensureHelperProcess(); + const helperExposedMeta = await helperProcessManager.rpc?.getMeta(); + return [ + `--main-exposed-meta=` + JSON.stringify(mainExposedMeta), + `--helper-exposed-meta=` + JSON.stringify(helperExposedMeta), + ]; +}; + +async function createOnboardingWindow(additionalArguments: string[]) { + logger.info('creating onboarding window'); + + const helperProcessManager = await ensureHelperProcess(); + const helperExposedMeta = await helperProcessManager.rpc?.getMeta(); + + assert(helperExposedMeta, 'helperExposedMeta should be defined'); + + const browserWindow = new BrowserWindow({ + width: 800, + height: 600, + frame: false, + show: false, + closable: false, + minimizable: false, + maximizable: false, + fullscreenable: false, + skipTaskbar: true, + // transparent: true, + webPreferences: { + webgl: true, + preload: join(__dirname, './preload.js'), + additionalArguments: additionalArguments, + }, + }); + + browserWindow.on('ready-to-show', () => { + browserWindow.show(); + }); + + await browserWindow.loadURL( + `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}onboarding` + ); + + return browserWindow; +} + +let onBoardingWindow$: Promise | undefined; + +export async function getOrCreateOnboardingWindow() { + const additionalArguments = await getWindowAdditionalArguments(); + if ( + !onBoardingWindow$ || + (await onBoardingWindow$.then(w => w.isDestroyed())) + ) { + onBoardingWindow$ = createOnboardingWindow(additionalArguments); + } + + return onBoardingWindow$; +} + +export async function getOnboardingWindow() { + if (!onBoardingWindow$) return; + const window = await onBoardingWindow$; + if (window.isDestroyed()) return; + return window; +} diff --git a/packages/frontend/electron/src/main/ui/handlers.ts b/packages/frontend/electron/src/main/ui/handlers.ts index f1e9096c5c..45f15f583a 100644 --- a/packages/frontend/electron/src/main/ui/handlers.ts +++ b/packages/frontend/electron/src/main/ui/handlers.ts @@ -2,9 +2,12 @@ import { app, nativeTheme } from 'electron'; import { getLinkPreview } from 'link-preview-js'; import { isMacOS } from '../../shared/utils'; +import { persistentConfig } from '../config-storage/persist'; import { logger } from '../logger'; -import { getMainWindow } from '../main-window'; +import { getMainWindow, initMainWindow } from '../main-window'; +import { getOnboardingWindow } from '../onboarding'; import type { NamespaceHandlers } from '../type'; +import { launchStage } from '../windows-manager/stage'; import { getChallengeResponse } from './challenge'; import { getGoogleOauthCode } from './google-auth'; @@ -46,6 +49,16 @@ export const uiHandlers = { getChallengeResponse: async (_, challenge: string) => { return getChallengeResponse(challenge); }, + handleOpenMainApp: async () => { + if (launchStage.value === 'onboarding') { + launchStage.value = 'main'; + persistentConfig.patch('onBoarding', false); + } + initMainWindow().catch(logger.error); + getOnboardingWindow() + .then(w => w?.destroy()) + .catch(logger.error); + }, getBookmarkDataByLink: async (_, link: string) => { if ( (link.startsWith('https://x.com/') || diff --git a/packages/frontend/electron/src/main/windows-manager/launcher.ts b/packages/frontend/electron/src/main/windows-manager/launcher.ts new file mode 100644 index 0000000000..cef7903e94 --- /dev/null +++ b/packages/frontend/electron/src/main/windows-manager/launcher.ts @@ -0,0 +1,26 @@ +import { initMainWindow } from '../main-window'; +import { + getOnboardingWindow, + getOrCreateOnboardingWindow, +} from '../onboarding'; +import { launchStage } from './stage'; + +/** + * Launch app depending on launch stage + */ +export function launch() { + const stage = launchStage.value; + if (stage === 'main') { + initMainWindow().catch(e => { + console.error('Failed to restore or create window:', e); + }); + + getOnboardingWindow() + .then(w => w?.destroy()) + .catch(e => console.error('Failed to destroy onboarding window:', e)); + } + if (stage === 'onboarding') + getOrCreateOnboardingWindow().catch(e => { + console.error('Failed to restore or create onboarding window:', e); + }); +} diff --git a/packages/frontend/electron/src/main/windows-manager/stage.ts b/packages/frontend/electron/src/main/windows-manager/stage.ts new file mode 100644 index 0000000000..5ef6da5c87 --- /dev/null +++ b/packages/frontend/electron/src/main/windows-manager/stage.ts @@ -0,0 +1,6 @@ +import { persistentConfig } from '../config-storage/persist'; +import type { LaunchStage } from './types'; + +export const launchStage: { value: LaunchStage } = { + value: persistentConfig.get('onBoarding') ? 'onboarding' : 'main', +}; diff --git a/packages/frontend/electron/src/main/windows-manager/types.ts b/packages/frontend/electron/src/main/windows-manager/types.ts new file mode 100644 index 0000000000..af9629d71f --- /dev/null +++ b/packages/frontend/electron/src/main/windows-manager/types.ts @@ -0,0 +1 @@ +export type LaunchStage = 'main' | 'onboarding'; diff --git a/packages/frontend/electron/src/main/windows-manager/windows.ts b/packages/frontend/electron/src/main/windows-manager/windows.ts new file mode 100644 index 0000000000..cbc5a19c78 --- /dev/null +++ b/packages/frontend/electron/src/main/windows-manager/windows.ts @@ -0,0 +1,9 @@ +import type { BrowserWindow } from 'electron'; + +import type { LaunchStage } from './types'; + +export const windows$: Record | undefined> = + { + main: undefined, + onboarding: undefined, + }; diff --git a/tests/affine-cloud/e2e/collaboration.spec.ts b/tests/affine-cloud/e2e/collaboration.spec.ts index 280776f99b..8c065aea13 100644 --- a/tests/affine-cloud/e2e/collaboration.spec.ts +++ b/tests/affine-cloud/e2e/collaboration.spec.ts @@ -1,4 +1,4 @@ -import { test } from '@affine-test/kit/playwright'; +import { skipOnboarding, test } from '@affine-test/kit/playwright'; import { addUserToWorkspace, createRandomUser, @@ -56,6 +56,7 @@ test('can enable share page', async ({ page, browser }) => { // check share page is accessible { const context = await browser.newContext(); + await skipOnboarding(context); const url: string = await page.evaluate(() => navigator.clipboard.readText() ); @@ -97,6 +98,7 @@ test('share page with default edgeless', async ({ page, browser }) => { // check share page is accessible { const context = await browser.newContext(); + await skipOnboarding(context); const url: string = await page.evaluate(() => navigator.clipboard.readText() ); @@ -135,6 +137,7 @@ test('can collaborate with other user and name should display when editing', asy const workspaceId = currentUrl.split('/')[4]; const userB = await createRandomUser(); const context = await browser.newContext(); + await skipOnboarding(context); const page2 = await context.newPage(); await loginUser(page2, userB.email); await addUserToWorkspace(workspaceId, userB.id, 1 /* READ */); @@ -205,6 +208,7 @@ test('can sync collections between different browser', async ({ { const context = await browser.newContext(); + await skipOnboarding(context); const page2 = await context.newPage(); await loginUser(page2, user.email); await page2.goto(page.url()); @@ -268,6 +272,7 @@ test('can sync svg between different browsers', async ({ page, browser }) => { { const context = await browser.newContext(); + await skipOnboarding(context); const page2 = await context.newPage(); await loginUser(page2, user.email); await page2.goto(page.url()); diff --git a/tests/affine-local/e2e/image-preview.spec.ts b/tests/affine-local/e2e/image-preview.spec.ts index 6afd7f007b..a1e2e1245b 100644 --- a/tests/affine-local/e2e/image-preview.spec.ts +++ b/tests/affine-local/e2e/image-preview.spec.ts @@ -1,12 +1,12 @@ /* eslint-disable unicorn/prefer-dom-node-dataset */ +import { test } from '@affine-test/kit/playwright'; import { openHomePage } from '@affine-test/kit/utils/load-page'; import { clickNewPageButton, getBlockSuiteEditorTitle, waitForEditorLoad, } from '@affine-test/kit/utils/page-logic'; -import type { Page } from '@playwright/test'; -import { expect, test } from '@playwright/test'; +import { expect, type Page } from '@playwright/test'; import fs from 'fs'; async function importImage(page: Page, url: string) { diff --git a/tests/affine-local/e2e/theme.spec.ts b/tests/affine-local/e2e/theme.spec.ts index ae383963f5..18bfe6d545 100644 --- a/tests/affine-local/e2e/theme.spec.ts +++ b/tests/affine-local/e2e/theme.spec.ts @@ -5,12 +5,12 @@ import { openHomePage } from '@affine-test/kit/utils/load-page'; import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic'; import { expect } from '@playwright/test'; +test.use({ + colorScheme: 'light', +}); + // default could be anything, according to the system -test('default white', async ({ browser }) => { - const context = await browser.newContext({ - colorScheme: 'light', - }); - const page = await context.newPage(); +test('default white', async ({ page }) => { await openHomePage(page); await waitForEditorLoad(page); const root = page.locator('html'); diff --git a/tests/affine-migration/e2e/basic.spec.ts b/tests/affine-migration/e2e/basic.spec.ts index 5faee08998..cad1bbfb17 100644 --- a/tests/affine-migration/e2e/basic.spec.ts +++ b/tests/affine-migration/e2e/basic.spec.ts @@ -1,11 +1,12 @@ import { patchDataEnhancement } from '@affine-test/kit/e2e-enhance/initializer'; import { SnapshotStorage } from '@affine-test/kit/e2e-enhance/snapshot'; +import { test } from '@affine-test/kit/playwright'; import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor'; import { coreUrl } from '@affine-test/kit/utils/load-page'; import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic'; import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar'; import type { Page } from '@playwright/test'; -import { expect, test } from '@playwright/test'; +import { expect } from '@playwright/test'; async function open404PageToInitData(page: Page, version: string) { const snapshotStorage = new SnapshotStorage(version); diff --git a/tests/kit/electron.ts b/tests/kit/electron.ts index 2d83ce2b42..182b215c04 100644 --- a/tests/kit/electron.ts +++ b/tests/kit/electron.ts @@ -95,6 +95,8 @@ export const test = base.extend<{ env.DEV_SERVER_URL = process.env.DEV_SERVER_URL; } + env.SKIP_ONBOARDING = '1'; + const electronApp = await electron.launch({ args: [clonedDist], env, diff --git a/tests/kit/playwright.ts b/tests/kit/playwright.ts index 552dcb38db..dfac426a74 100644 --- a/tests/kit/playwright.ts +++ b/tests/kit/playwright.ts @@ -5,7 +5,7 @@ import path, { resolve } from 'node:path'; import process from 'node:process'; import type { Workspace } from '@blocksuite/store'; -import { test as baseTest } from '@playwright/test'; +import { type BrowserContext, test as baseTest } from '@playwright/test'; export const rootDir = resolve(__dirname, '..', '..'); // assert that the rootDir is the root of the project @@ -32,6 +32,12 @@ type CurrentWorkspace = { blockSuiteWorkspace: Workspace; }; +export const skipOnboarding = async (context: BrowserContext) => { + await context.addInitScript(() => { + window.localStorage.setItem('app_config', '{"onBoarding":false}'); + }); +}; + export const test = baseTest.extend<{ workspace: { current: () => Promise; @@ -59,6 +65,9 @@ export const test = baseTest.extend<{ }); }, context: async ({ context }, use) => { + // workaround for skipping onboarding redirect on the web + await skipOnboarding(context); + if (enableCoverage) { await context.addInitScript(() => window.addEventListener('beforeunload', () => diff --git a/tools/@types/env/__all.d.ts b/tools/@types/env/__all.d.ts index 129df60963..7e75bf0808 100644 --- a/tools/@types/env/__all.d.ts +++ b/tools/@types/env/__all.d.ts @@ -1,5 +1,6 @@ import type { Environment, RuntimeConfig } from '@affine/env/global'; import type { + ConfigStorageHandlerManager, DBHandlerManager, DebugHandlerManager, DialogHandlerManager, @@ -24,6 +25,7 @@ declare global { ui: UnwrapManagerHandlerToClientSide; updater: UnwrapManagerHandlerToClientSide; workspace: UnwrapManagerHandlerToClientSide; + configStorage: UnwrapManagerHandlerToClientSide; }; events: EventMap; affine: {