mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(core): impl invitation link (#11181)
feat(core): add invitee to getInviteInfoQuery feat(core): enable invitation link refactor(core): replace AcceptInviteService to InvitationService
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
Scrollable,
|
||||
Skeleton,
|
||||
} from '@affine/component';
|
||||
import { AcceptInviteService } from '@affine/core/modules/cloud';
|
||||
import { InvitationService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type Notification,
|
||||
NotificationListService,
|
||||
@@ -314,7 +314,7 @@ const InvitationNotificationItem = ({
|
||||
const memberInactived = !body.createdByUser;
|
||||
const workspaceInactived = !body.workspace;
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const acceptInviteService = useService(AcceptInviteService);
|
||||
const invitationService = useService(InvitationService);
|
||||
const notificationListService = useService(NotificationListService);
|
||||
const inviteId = body.inviteId;
|
||||
const [isAccepting, setIsAccepting] = useState(false);
|
||||
@@ -347,8 +347,8 @@ const InvitationNotificationItem = ({
|
||||
|
||||
const handleAcceptInvite = useCallback(() => {
|
||||
setIsAccepting(true);
|
||||
acceptInviteService
|
||||
.waitForAcceptInvite(inviteId)
|
||||
invitationService
|
||||
.acceptInvite(inviteId)
|
||||
.catch(err => {
|
||||
const userFriendlyError = UserFriendlyError.fromAny(err);
|
||||
if (userFriendlyError.is('ALREADY_IN_SPACE')) {
|
||||
@@ -384,7 +384,7 @@ const InvitationNotificationItem = ({
|
||||
setIsAccepting(false);
|
||||
});
|
||||
}, [
|
||||
acceptInviteService,
|
||||
invitationService,
|
||||
handleReadAndOpenWorkspace,
|
||||
inviteId,
|
||||
notification,
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
import {
|
||||
AcceptInvitePage,
|
||||
ExpiredPage,
|
||||
FailedToSendPage,
|
||||
JoinFailedPage,
|
||||
RequestToJoinPage,
|
||||
SentRequestPage,
|
||||
} from '@affine/component/member-components';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { WorkspaceMemberStatus } from '@affine/graphql';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../../../components/hooks/use-navigate-helper';
|
||||
import { AcceptInviteService, AuthService } from '../../../modules/cloud';
|
||||
import { AuthService, InvitationService } from '../../../modules/cloud';
|
||||
|
||||
const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const acceptInviteService = useService(AcceptInviteService);
|
||||
|
||||
const invitationService = useService(InvitationService);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const error = useLiveData(acceptInviteService.error$);
|
||||
const inviteId = useLiveData(acceptInviteService.inviteId$);
|
||||
const inviteInfo = useLiveData(acceptInviteService.inviteInfo$);
|
||||
const accepted = useLiveData(acceptInviteService.accepted$);
|
||||
const loading = useLiveData(acceptInviteService.loading$);
|
||||
const authService = useService(AuthService);
|
||||
const user = useLiveData(authService.session.account$);
|
||||
const error = useLiveData(invitationService.error$);
|
||||
const inviteId = useLiveData(invitationService.inviteId$);
|
||||
const inviteInfo = useLiveData(invitationService.inviteInfo$);
|
||||
const loading = useLiveData(invitationService.loading$);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
const navigateHelper = useNavigateHelper();
|
||||
const [accepted, setAccepted] = useState(false);
|
||||
|
||||
const openWorkspace = useAsyncCallback(async () => {
|
||||
if (!inviteInfo?.workspace.id) {
|
||||
@@ -40,26 +48,40 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
||||
}, [navigateHelper]);
|
||||
|
||||
useEffect(() => {
|
||||
acceptInviteService.acceptInvite({
|
||||
inviteId: targetInviteId,
|
||||
});
|
||||
}, [acceptInviteService, targetInviteId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error && inviteId === targetInviteId) {
|
||||
const err = UserFriendlyError.fromAny(error);
|
||||
if (err.is('ALREADY_IN_SPACE')) {
|
||||
return openWorkspace();
|
||||
}
|
||||
// if workspace already exists, open it
|
||||
if (
|
||||
!accepted &&
|
||||
inviteInfo?.workspace.id &&
|
||||
workspaces.some(w => w.id === inviteInfo.workspace.id)
|
||||
) {
|
||||
return openWorkspace();
|
||||
}
|
||||
}, [error, inviteId, navigateHelper, openWorkspace, targetInviteId]);
|
||||
}, [accepted, inviteInfo?.workspace.id, openWorkspace, workspaces]);
|
||||
|
||||
const requestToJoin = useAsyncCallback(async () => {
|
||||
await invitationService
|
||||
.acceptInvite(targetInviteId)
|
||||
.then(() => {
|
||||
invitationService.getInviteInfo({ inviteId: targetInviteId });
|
||||
setAccepted(true);
|
||||
})
|
||||
.catch(error => {
|
||||
const err = UserFriendlyError.fromAny(error);
|
||||
if (err.is('ALREADY_IN_SPACE')) {
|
||||
return openWorkspace();
|
||||
}
|
||||
});
|
||||
}, [invitationService, openWorkspace, targetInviteId]);
|
||||
|
||||
const onSignOut = useAsyncCallback(async () => {
|
||||
await authService.signOut();
|
||||
}, [authService]);
|
||||
|
||||
if (loading || inviteId !== targetInviteId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!inviteInfo) {
|
||||
// if invite is expired
|
||||
return <ExpiredPage onOpenAffine={onOpenAffine} />;
|
||||
}
|
||||
|
||||
@@ -67,17 +89,35 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
||||
return <JoinFailedPage inviteInfo={inviteInfo} error={error} />;
|
||||
}
|
||||
|
||||
if (accepted) {
|
||||
// for email invite
|
||||
if (accepted && inviteInfo.status === WorkspaceMemberStatus.Accepted) {
|
||||
return (
|
||||
<AcceptInvitePage
|
||||
inviteInfo={inviteInfo}
|
||||
onOpenWorkspace={openWorkspace}
|
||||
inviteInfo={inviteInfo}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// invite is expired
|
||||
return <ExpiredPage onOpenAffine={onOpenAffine} />;
|
||||
}
|
||||
|
||||
if (inviteInfo.status === WorkspaceMemberStatus.UnderReview) {
|
||||
return <SentRequestPage user={user} inviteInfo={inviteInfo} />;
|
||||
}
|
||||
|
||||
if (
|
||||
inviteInfo.status === WorkspaceMemberStatus.NeedMoreSeatAndReview ||
|
||||
inviteInfo.status === WorkspaceMemberStatus.NeedMoreSeat
|
||||
) {
|
||||
return <FailedToSendPage user={user} inviteInfo={inviteInfo} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RequestToJoinPage
|
||||
user={user}
|
||||
inviteInfo={inviteInfo}
|
||||
requestToJoin={requestToJoin}
|
||||
onSignOut={onSignOut}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -87,13 +127,17 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
||||
*/
|
||||
export const Component = () => {
|
||||
const authService = useService(AuthService);
|
||||
const invitationService = useService(InvitationService);
|
||||
const isRevalidating = useLiveData(authService.session.isRevalidating$);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
const params = useParams<{ inviteId: string }>();
|
||||
|
||||
useEffect(() => {
|
||||
authService.session.revalidate();
|
||||
}, [authService]);
|
||||
if (params.inviteId) {
|
||||
invitationService.getInviteInfo({ inviteId: params.inviteId });
|
||||
}
|
||||
}, [authService, invitationService, params.inviteId]);
|
||||
|
||||
const { jumpToSignIn } = useNavigateHelper();
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ export { AccountLoggedOut } from './events/account-logged-out';
|
||||
export { AuthProvider } from './provider/auth';
|
||||
export { ValidatorProvider } from './provider/validator';
|
||||
export { ServerScope } from './scopes/server';
|
||||
export { AcceptInviteService } from './services/accept-invite';
|
||||
export { AuthService } from './services/auth';
|
||||
export { CaptchaService } from './services/captcha';
|
||||
export { DefaultServerService } from './services/default-server';
|
||||
export { EventSourceService } from './services/eventsource';
|
||||
export { FetchService } from './services/fetch';
|
||||
export { GraphQLService } from './services/graphql';
|
||||
export { InvitationService } from './services/invitation';
|
||||
export { InvoicesService } from './services/invoices';
|
||||
export { PublicUserService } from './services/public-user';
|
||||
export { SelfhostGenerateLicenseService } from './services/selfhost-generate-license';
|
||||
@@ -33,6 +33,7 @@ export { WorkspaceServerService } from './services/workspace-server';
|
||||
export { WorkspaceSubscriptionService } from './services/workspace-subscription';
|
||||
export type { ServerConfig } from './types';
|
||||
|
||||
// eslint-disable-next-line simple-import-sort/imports
|
||||
import { type Framework } from '@toeverything/infra';
|
||||
|
||||
import { DocScope } from '../doc/scopes/doc';
|
||||
@@ -56,7 +57,7 @@ import { configureDefaultAuthProvider } from './impl/auth';
|
||||
import { AuthProvider } from './provider/auth';
|
||||
import { ValidatorProvider } from './provider/validator';
|
||||
import { ServerScope } from './scopes/server';
|
||||
import { AcceptInviteService } from './services/accept-invite';
|
||||
import { InvitationService } from './services/invitation';
|
||||
import { AuthService } from './services/auth';
|
||||
import { BlocksuiteWriterInfoService } from './services/blocksuite-writer-info';
|
||||
import { CaptchaService } from './services/captcha';
|
||||
@@ -153,7 +154,7 @@ export function configureCloudModule(framework: Framework) {
|
||||
.service(SelfhostGenerateLicenseService, [SelfhostGenerateLicenseStore])
|
||||
.store(SelfhostGenerateLicenseStore, [GraphQLService])
|
||||
.store(InviteInfoStore, [GraphQLService])
|
||||
.service(AcceptInviteService, [AcceptInviteStore, InviteInfoStore])
|
||||
.service(InvitationService, [AcceptInviteStore, InviteInfoStore])
|
||||
.store(AcceptInviteStore, [GraphQLService])
|
||||
.service(PublicUserService, [PublicUserStore])
|
||||
.store(PublicUserStore, [GraphQLService])
|
||||
|
||||
@@ -16,20 +16,19 @@ import type { InviteInfoStore } from '../stores/invite-info';
|
||||
|
||||
export type InviteInfo = GetInviteInfoQuery['getInviteInfo'];
|
||||
|
||||
export class AcceptInviteService extends Service {
|
||||
export class InvitationService extends Service {
|
||||
constructor(
|
||||
private readonly store: AcceptInviteStore,
|
||||
private readonly acceptInviteStore: AcceptInviteStore,
|
||||
private readonly inviteInfoStore: InviteInfoStore
|
||||
) {
|
||||
super();
|
||||
}
|
||||
inviteId$ = new LiveData<string | undefined>(undefined);
|
||||
inviteInfo$ = new LiveData<InviteInfo | undefined>(undefined);
|
||||
accepted$ = new LiveData<boolean>(false);
|
||||
loading$ = new LiveData(false);
|
||||
error$ = new LiveData<any>(null);
|
||||
|
||||
readonly acceptInvite = effect(
|
||||
readonly getInviteInfo = effect(
|
||||
switchMap(({ inviteId }: { inviteId: string }) => {
|
||||
if (!inviteId) {
|
||||
return EMPTY;
|
||||
@@ -39,16 +38,6 @@ export class AcceptInviteService extends Service {
|
||||
}).pipe(
|
||||
mergeMap(res => {
|
||||
this.inviteInfo$.setValue(res);
|
||||
return fromPromise(async () => {
|
||||
return await this.store.acceptInvite(
|
||||
res.workspace.id,
|
||||
inviteId,
|
||||
true
|
||||
);
|
||||
});
|
||||
}),
|
||||
mergeMap(res => {
|
||||
this.accepted$.next(res);
|
||||
return EMPTY;
|
||||
}),
|
||||
smartRetry({
|
||||
@@ -59,7 +48,6 @@ export class AcceptInviteService extends Service {
|
||||
this.inviteId$.setValue(inviteId);
|
||||
this.loading$.setValue(true);
|
||||
this.inviteInfo$.setValue(undefined);
|
||||
this.accepted$.setValue(false);
|
||||
}),
|
||||
onComplete(() => {
|
||||
this.loading$.setValue(false);
|
||||
@@ -68,21 +56,20 @@ export class AcceptInviteService extends Service {
|
||||
})
|
||||
);
|
||||
|
||||
async waitForAcceptInvite(inviteId: string) {
|
||||
this.acceptInvite({ inviteId });
|
||||
async acceptInvite(inviteId: string) {
|
||||
this.getInviteInfo({ inviteId });
|
||||
await this.loading$.waitFor(f => !f);
|
||||
if (this.accepted$.value) {
|
||||
return true; // invite is accepted
|
||||
if (!this.inviteInfo$.value) {
|
||||
throw new Error('Invalid invite id');
|
||||
}
|
||||
|
||||
if (this.error$.value) {
|
||||
throw this.error$.value;
|
||||
}
|
||||
|
||||
return false; // invite is expired
|
||||
return await this.acceptInviteStore.acceptInvite(
|
||||
this.inviteInfo$.value.workspace.id,
|
||||
inviteId,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.acceptInvite.unsubscribe();
|
||||
this.getInviteInfo.unsubscribe();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user