diff --git a/packages/frontend/core/src/components/affine/auth/index.ts b/packages/frontend/core/src/components/affine/auth/index.ts new file mode 100644 index 0000000000000..38414ac204343 --- /dev/null +++ b/packages/frontend/core/src/components/affine/auth/index.ts @@ -0,0 +1 @@ +export { OAuthLaunchComponent } from './oauth-launch-component'; diff --git a/packages/frontend/core/src/components/affine/auth/oauth-launch-component.tsx b/packages/frontend/core/src/components/affine/auth/oauth-launch-component.tsx new file mode 100644 index 0000000000000..208060562b934 --- /dev/null +++ b/packages/frontend/core/src/components/affine/auth/oauth-launch-component.tsx @@ -0,0 +1,88 @@ +import { notify } from '@affine/component/ui/notification'; +import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; +import { AuthService, ServerService } from '@affine/core/modules/cloud'; +import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session'; +import { UrlService } from '@affine/core/modules/url'; +import { UserFriendlyError } from '@affine/error'; +import { OAuthProviderType } from '@affine/graphql'; +import { useI18n } from '@affine/i18n'; +import track from '@affine/track'; +import { useLiveData, useService } from '@toeverything/infra'; +import { useEffect } from 'react'; + +export function OAuthLaunchComponent({ + onAuthenticated, + redirectUrl, +}: { + onAuthenticated?: (status: AuthSessionStatus) => void; + redirectUrl?: string; +}) { + const serverService = useService(ServerService); + const urlService = useService(UrlService); + const auth = useService(AuthService); + const loginStatus = useLiveData(auth.session.status$); + const t = useI18n(); + + const effectiveRedirectUrl = + redirectUrl ?? serverService.server.baseUrl + '/oauth/callback'; + + const onContinue = useAsyncCallback( + async (provider: OAuthProviderType) => { + track.$.$.auth.signIn({ method: 'oauth', provider }); + + const open: () => Promise | void = BUILD_CONFIG.isNative + ? async () => { + try { + const scheme = urlService.getClientScheme(); + const options = await auth.oauthPreflight( + provider, + scheme ?? 'web' + ); + urlService.openPopupWindow(options.url); + } catch (e) { + notify.error(UserFriendlyError.fromAny(e)); + } + } + : () => { + const params = new URLSearchParams(); + + params.set('provider', provider); + + if (effectiveRedirectUrl) { + params.set('redirect_uri', effectiveRedirectUrl); + } + + const oauthUrl = + serverService.server.baseUrl + + `/oauth/login?${params.toString()}`; + + urlService.openPopupWindow(oauthUrl); + }; + + const ret = open(); + + if (ret instanceof Promise) { + await ret; + } + }, + [urlService, effectiveRedirectUrl, serverService, auth] + ); + + const provider = OAuthProviderType.OIDC; + + useEffect(() => { + if (loginStatus === 'authenticated') { + notify.success({ + title: t['com.affine.auth.toast.title.signed-in'](), + message: t['com.affine.auth.toast.message.signed-in'](), + }); + } + onAuthenticated?.(loginStatus); + }, [loginStatus, onAuthenticated, t]); + + useEffect(() => { + onContinue(provider); + }, [onContinue, provider]); + + return

Logging in with OIDC

; +} diff --git a/packages/frontend/core/src/desktop/pages/auth/oauth-launch.tsx b/packages/frontend/core/src/desktop/pages/auth/oauth-launch.tsx new file mode 100644 index 0000000000000..7813cc4f3c944 --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/auth/oauth-launch.tsx @@ -0,0 +1,77 @@ +import { notify } from '@affine/component'; +import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout'; +import { SignInPageContainer } from '@affine/component/auth-components'; +import { OAuthLaunchComponent } from '@affine/core/components/affine/auth'; +import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session'; +import { useI18n } from '@affine/i18n'; +import { useCallback, useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + +import { + RouteLogic, + useNavigateHelper, +} from '../../../components/hooks/use-navigate-helper'; + +export const OAuthLaunch = ({ + redirectUrl: redirectUrlFromProps, +}: { + redirectUrl?: string; +}) => { + const t = useI18n(); + const navigate = useNavigate(); + const { jumpToIndex } = useNavigateHelper(); + const [searchParams] = useSearchParams(); + const redirectUrl = redirectUrlFromProps ?? searchParams.get('redirect_uri'); + + const error = searchParams.get('error'); + + useEffect(() => { + if (error) { + notify.error({ + title: t['com.affine.auth.toast.title.failed'](), + message: error, + }); + } + }, [error, t]); + + const handleClose = useCallback(() => { + jumpToIndex(RouteLogic.REPLACE, { + search: searchParams.toString(), + }); + }, [jumpToIndex, searchParams]); + + const handleAuthenticated = useCallback( + (status: AuthSessionStatus) => { + if (status === 'authenticated') { + if (redirectUrl) { + if (redirectUrl.toUpperCase() === 'CLOSE_POPUP') { + window.close(); + return; + } + navigate(redirectUrl, { + replace: true, + }); + } else { + handleClose(); + } + } + }, + [handleClose, navigate, redirectUrl] + ); + + return ( + +
+ +
+
+ ); +}; + +export const Component = () => { + return ( + + + + ); +}; diff --git a/packages/frontend/core/src/desktop/router.tsx b/packages/frontend/core/src/desktop/router.tsx index c89bd4eb90429..88a5228313e63 100644 --- a/packages/frontend/core/src/desktop/router.tsx +++ b/packages/frontend/core/src/desktop/router.tsx @@ -150,6 +150,11 @@ export const topLevelRoutes = [ lazy: () => import(/* webpackChunkName: "auth" */ './pages/auth/oauth-login'), }, + { + path: '/oauth/launch', + lazy: () => + import(/* webpackChunkName: "auth" */ './pages/auth/oauth-launch'), + }, { path: '/oauth/callback', lazy: () =>