diff --git a/packages/common/graphql/src/graphql/get-invite-info.gql b/packages/common/graphql/src/graphql/get-invite-info.gql
index faee37e8d..21f347766 100644
--- a/packages/common/graphql/src/graphql/get-invite-info.gql
+++ b/packages/common/graphql/src/graphql/get-invite-info.gql
@@ -11,5 +11,11 @@ query getInviteInfo($inviteId: String!) {
avatarUrl
}
status
+ invitee {
+ id
+ name
+ email
+ avatarUrl
+ }
}
}
diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts
index b439f096b..cdca835f0 100644
--- a/packages/common/graphql/src/graphql/index.ts
+++ b/packages/common/graphql/src/graphql/index.ts
@@ -776,6 +776,12 @@ export const getInviteInfoQuery = {
avatarUrl
}
status
+ invitee {
+ id
+ name
+ email
+ avatarUrl
+ }
}
}`,
};
diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts
index 89354ca07..8972af76a 100644
--- a/packages/common/graphql/src/schema.ts
+++ b/packages/common/graphql/src/schema.ts
@@ -3242,6 +3242,13 @@ export type GetInviteInfoQuery = {
name: string;
avatarUrl: string | null;
};
+ invitee: {
+ __typename?: 'WorkspaceUserType';
+ id: string;
+ name: string;
+ email: string;
+ avatarUrl: string | null;
+ };
};
};
diff --git a/packages/frontend/component/src/components/member-components/failed-to-send-page.tsx b/packages/frontend/component/src/components/member-components/failed-to-send-page.tsx
new file mode 100644
index 000000000..525816e56
--- /dev/null
+++ b/packages/frontend/component/src/components/member-components/failed-to-send-page.tsx
@@ -0,0 +1,48 @@
+import {
+ AuthPageContainer,
+ type User,
+} from '@affine/component/auth-components';
+import type { GetInviteInfoQuery } from '@affine/graphql';
+import { Trans, useI18n } from '@affine/i18n';
+
+import { Avatar } from '../../ui/avatar';
+import * as styles from './styles.css';
+export const FailedToSendPage = ({
+ user,
+ inviteInfo,
+}: {
+ user: User | null;
+ inviteInfo: GetInviteInfoQuery['getInviteInfo'];
+}) => {
+ const t = useI18n();
+ return (
+
+
+
+
+ ),
+ 2: ,
+ 3: ,
+ }}
+ values={{
+ workspaceName: inviteInfo.workspace.name,
+ userEmail: user?.email,
+ }}
+ />
+
+ }
+ >
+ );
+};
diff --git a/packages/frontend/component/src/components/member-components/index.tsx b/packages/frontend/component/src/components/member-components/index.tsx
index 23d2f8531..034ed6b9c 100644
--- a/packages/frontend/component/src/components/member-components/index.tsx
+++ b/packages/frontend/component/src/components/member-components/index.tsx
@@ -1,6 +1,9 @@
export * from './accept-invite-page';
export * from './expired';
+export * from './failed-to-send-page';
export * from './invite-modal';
export * from './invite-team-modal';
export * from './join-failed-page';
export * from './member-limit-modal';
+export * from './request-to-join-page';
+export * from './sent-request-page';
diff --git a/packages/frontend/component/src/components/member-components/invite-team-modal/link-invite.tsx b/packages/frontend/component/src/components/member-components/invite-team-modal/link-invite.tsx
index de33f10e6..759fd6b6f 100644
--- a/packages/frontend/component/src/components/member-components/invite-team-modal/link-invite.tsx
+++ b/packages/frontend/component/src/components/member-components/invite-team-modal/link-invite.tsx
@@ -113,53 +113,62 @@ export const LinkInvite = ({
return (
<>
-
- {t['com.affine.payment.member.team.invite.link-expiration']()}
-
-
-
- {t['com.affine.payment.member.team.invite.invitation-link']()}
-
-
-
+
+ {t['com.affine.payment.member.team.invite.link-expiration']()}
+
+
- {invitationLink ? (
- <>
-
+
+
+ {t['com.affine.payment.member.team.invite.invitation-link']()}
+
+
+
+ {invitationLink ? (
+ <>
+
+ } onClick={onReset} />
+ >
+ ) : (
+
- } onClick={onReset} />
- >
- ) : (
-
- )}
+ )}
+
+
+ {t[
+ 'com.affine.payment.member.team.invite.invitation-link.description'
+ ]()}
+
>
);
diff --git a/packages/frontend/component/src/components/member-components/invite-team-modal/modal-content.tsx b/packages/frontend/component/src/components/member-components/invite-team-modal/modal-content.tsx
index 8670b9c1c..d6f6f93ad 100644
--- a/packages/frontend/component/src/components/member-components/invite-team-modal/modal-content.tsx
+++ b/packages/frontend/component/src/components/member-components/invite-team-modal/modal-content.tsx
@@ -15,7 +15,7 @@ export const ModalContent = ({
inviteEmail,
setInviteEmail,
inviteMethod,
- // onInviteMethodChange,
+ onInviteMethodChange,
handleConfirm,
isMutating,
isValidEmail,
@@ -48,7 +48,7 @@ export const ModalContent = ({
}
- label={`${t['com.affine.payment.member.team.invite.invite-link']()}(Coming soon)`}
+ label={t['com.affine.payment.member.team.invite.invite-link']()}
/>
),
value: 'link',
- style: {
- cursor: 'not-allowed',
- },
},
]}
/>
diff --git a/packages/frontend/component/src/components/member-components/invite-team-modal/styles.css.ts b/packages/frontend/component/src/components/member-components/invite-team-modal/styles.css.ts
index e3ddb5a76..4e8e76723 100644
--- a/packages/frontend/component/src/components/member-components/invite-team-modal/styles.css.ts
+++ b/packages/frontend/component/src/components/member-components/invite-team-modal/styles.css.ts
@@ -81,6 +81,12 @@ export const modalSubTitle = style({
fontWeight: '500',
});
+export const invitationLinkContainer = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+});
+
export const radioItem = style({
display: 'flex',
alignItems: 'center',
@@ -107,3 +113,8 @@ export const invitationLinkContent = style({
display: 'flex',
gap: '8px',
});
+
+export const invitationLinkDescription = style({
+ color: cssVarV2('text/secondary'),
+ fontSize: cssVar('fontSm'),
+});
diff --git a/packages/frontend/component/src/components/member-components/join-failed-page.tsx b/packages/frontend/component/src/components/member-components/join-failed-page.tsx
index 3f44eb493..71e86ab4d 100644
--- a/packages/frontend/component/src/components/member-components/join-failed-page.tsx
+++ b/packages/frontend/component/src/components/member-components/join-failed-page.tsx
@@ -20,7 +20,7 @@ export const JoinFailedPage = ({
title={t['com.affine.fail-to-join-workspace.title']()}
subtitle={
userFriendlyError.name === ErrorNames.MEMBER_QUOTA_EXCEEDED ? (
-
+
void;
+ onSignOut: () => void;
+}) => {
+ const t = useI18n();
+
+ return (
+
+
+
+
{inviteInfo.user.name}
+
+ {t['invited you to join']()}
+
+
+
+ {inviteInfo.workspace.name}
+
+
+
+ }
+ >
+
+ {user ? (
+
+ ) : null}
+
+ );
+};
diff --git a/packages/frontend/component/src/components/member-components/sent-request-page.tsx b/packages/frontend/component/src/components/member-components/sent-request-page.tsx
new file mode 100644
index 000000000..f88a16b63
--- /dev/null
+++ b/packages/frontend/component/src/components/member-components/sent-request-page.tsx
@@ -0,0 +1,48 @@
+import {
+ AuthPageContainer,
+ type User,
+} from '@affine/component/auth-components';
+import type { GetInviteInfoQuery } from '@affine/graphql';
+import { Trans, useI18n } from '@affine/i18n';
+
+import { Avatar } from '../../ui/avatar';
+import * as styles from './styles.css';
+export const SentRequestPage = ({
+ user,
+ inviteInfo,
+}: {
+ user: User | null;
+ inviteInfo: GetInviteInfoQuery['getInviteInfo'];
+}) => {
+ const t = useI18n();
+ return (
+
+
+
+
+ ),
+ 2: ,
+ 3: ,
+ }}
+ values={{
+ workspaceName: inviteInfo.workspace.name,
+ userEmail: user?.email,
+ }}
+ />
+
+ }
+ >
+ );
+};
diff --git a/packages/frontend/component/src/components/member-components/styles.css.ts b/packages/frontend/component/src/components/member-components/styles.css.ts
index 6b377e6e9..3d4e597da 100644
--- a/packages/frontend/component/src/components/member-components/styles.css.ts
+++ b/packages/frontend/component/src/components/member-components/styles.css.ts
@@ -15,11 +15,27 @@ export const inviteModalContent = style({
export const inviteModalButtonContainer = style({
display: 'flex',
justifyContent: 'flex-end',
- // marginTop: 10,
});
export const inviteName = style({
color: cssVarV2('text/primary'),
+ fontWeight: '600',
+});
+
+export const avatarWrapper = style({
+ verticalAlign: 'sub',
+ display: 'inline-block',
+});
+
+export const userInfoWrapper = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: '12px',
+ marginTop: '28px',
+});
+
+export const lineHeight = style({
+ lineHeight: '1.5',
});
export const content = style({
@@ -30,7 +46,7 @@ export const content = style({
});
export const userWrapper = style({
- display: 'flex',
+ display: 'inline-flex',
alignItems: 'center',
gap: '4px',
});
diff --git a/packages/frontend/core/src/components/notification/list.tsx b/packages/frontend/core/src/components/notification/list.tsx
index 5b2157d78..1e7f83efb 100644
--- a/packages/frontend/core/src/components/notification/list.tsx
+++ b/packages/frontend/core/src/components/notification/list.tsx
@@ -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,
diff --git a/packages/frontend/core/src/desktop/pages/invite/index.tsx b/packages/frontend/core/src/desktop/pages/invite/index.tsx
index aa0aaba13..3242d588e 100644
--- a/packages/frontend/core/src/desktop/pages/invite/index.tsx
+++ b/packages/frontend/core/src/desktop/pages/invite/index.tsx
@@ -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 ;
}
@@ -67,17 +89,35 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
return ;
}
- if (accepted) {
+ // for email invite
+ if (accepted && inviteInfo.status === WorkspaceMemberStatus.Accepted) {
return (
);
- } else {
- // invite is expired
- return ;
}
+
+ if (inviteInfo.status === WorkspaceMemberStatus.UnderReview) {
+ return ;
+ }
+
+ if (
+ inviteInfo.status === WorkspaceMemberStatus.NeedMoreSeatAndReview ||
+ inviteInfo.status === WorkspaceMemberStatus.NeedMoreSeat
+ ) {
+ return ;
+ }
+
+ return (
+
+ );
};
/**
@@ -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();
diff --git a/packages/frontend/core/src/modules/cloud/index.ts b/packages/frontend/core/src/modules/cloud/index.ts
index fa11f6781..1541b50ee 100644
--- a/packages/frontend/core/src/modules/cloud/index.ts
+++ b/packages/frontend/core/src/modules/cloud/index.ts
@@ -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])
diff --git a/packages/frontend/core/src/modules/cloud/services/accept-invite.ts b/packages/frontend/core/src/modules/cloud/services/invitation.ts
similarity index 65%
rename from packages/frontend/core/src/modules/cloud/services/accept-invite.ts
rename to packages/frontend/core/src/modules/cloud/services/invitation.ts
index 30e414b38..072c29589 100644
--- a/packages/frontend/core/src/modules/cloud/services/accept-invite.ts
+++ b/packages/frontend/core/src/modules/cloud/services/invitation.ts
@@ -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(undefined);
inviteInfo$ = new LiveData(undefined);
- accepted$ = new LiveData(false);
loading$ = new LiveData(false);
error$ = new LiveData(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();
}
}
diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json
index 9d9eaeeee..b6b479bcd 100644
--- a/packages/frontend/i18n/src/i18n-completenesses.json
+++ b/packages/frontend/i18n/src/i18n-completenesses.json
@@ -6,7 +6,7 @@
"el-GR": 94,
"en": 100,
"es-AR": 94,
- "es-CL": 96,
+ "es-CL": 95,
"es": 94,
"fa": 94,
"fr": 94,
diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts
index 262946d89..0467031b8 100644
--- a/packages/frontend/i18n/src/i18n.gen.ts
+++ b/packages/frontend/i18n/src/i18n.gen.ts
@@ -7203,6 +7203,18 @@ export function useAFFiNEI18N(): {
* `Please contact your workspace owner to add more seats.`
*/
["com.affine.fail-to-join-workspace.description-2"](): string;
+ /**
+ * `Request to join`
+ */
+ ["com.affine.request-to-join-workspace.button"](): string;
+ /**
+ * `Request Sent successfully`
+ */
+ ["com.affine.sent-request-to-join-workspace.title"](): string;
+ /**
+ * `Request failed to send`
+ */
+ ["com.affine.failed-to-send-request.title"](): string;
/**
* `Readwise`
*/
@@ -8445,6 +8457,28 @@ export const TypedTrans: {
["1"]: JSX.Element;
["2"]: JSX.Element;
}>>;
+ /**
+ * `You requested to join <1/> <2>{{workspaceName}}2> with <3>{{userEmail}}3>, the workspace owner and team admins will review your request.`
+ */
+ ["com.affine.sent-request-to-join-workspace.description"]: ComponentType, {
+ ["1"]: JSX.Element;
+ ["2"]: JSX.Element;
+ ["3"]: JSX.Element;
+ }>>;
+ /**
+ * `Unable to process your request to join <1/> <2>{{workspaceName}}2> with <3>{{userEmail}}3>, the workspace has reached its member limit. Please contact the workspace owner for available seats.`
+ */
+ ["com.affine.failed-to-send-request.description"]: ComponentType, {
+ ["1"]: JSX.Element;
+ ["2"]: JSX.Element;
+ ["3"]: JSX.Element;
+ }>>;
/**
* `Import your Readwise highlights to AFFiNE. Please visit Readwise, click "Get Access Token", and paste the token below.`
*/
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 572836f18..8d2d0b819 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -1792,6 +1792,11 @@
"com.affine.fail-to-join-workspace.title": "Join Failed",
"com.affine.fail-to-join-workspace.description-1": "Unable to join <1/> <2>{{workspaceName}}2> due to insufficient seats available.",
"com.affine.fail-to-join-workspace.description-2": "Please contact your workspace owner to add more seats.",
+ "com.affine.request-to-join-workspace.button": "Request to join",
+ "com.affine.sent-request-to-join-workspace.title": "Request Sent successfully",
+ "com.affine.sent-request-to-join-workspace.description": "You requested to join <1/> <2>{{workspaceName}}2> with <3>{{userEmail}}3>, the workspace owner and team admins will review your request.",
+ "com.affine.failed-to-send-request.title": "Request failed to send",
+ "com.affine.failed-to-send-request.description": "Unable to process your request to join <1/> <2>{{workspaceName}}2> with <3>{{userEmail}}3>, the workspace has reached its member limit. Please contact the workspace owner for available seats.",
"com.affine.integration.name.readwise": "Readwise",
"com.affine.integration.integrations": "Integrations",
"com.affine.integration.setting.description": "Elevate your AFFiNE experience with diverse add-ons and seamless integrations.",