diff --git a/services/web/app/src/Features/Subscription/SubscriptionLocator.js b/services/web/app/src/Features/Subscription/SubscriptionLocator.js
index 1ad304acde..c1b8fde703 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionLocator.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionLocator.js
@@ -99,6 +99,14 @@ const SubscriptionLocator = {
Subscription.find({ invited_emails: email }, callback)
},
+ getGroupsWithTeamInvitesEmail(email, callback) {
+ Subscription.find(
+ { teamInvites: { $elemMatch: { email } } },
+ { teamInvites: 1 },
+ callback
+ )
+ },
+
getGroupWithV1Id(v1TeamId, callback) {
Subscription.findOne({ 'overleaf.id': v1TeamId }, callback)
},
@@ -151,6 +159,9 @@ SubscriptionLocator.promises = {
getGroupsWithEmailInvite: promisify(
SubscriptionLocator.getGroupsWithEmailInvite
),
+ getGroupsWithTeamInvitesEmail: promisify(
+ SubscriptionLocator.getGroupsWithTeamInvitesEmail
+ ),
getGroupWithV1Id: promisify(SubscriptionLocator.getGroupWithV1Id),
getUserDeletedSubscriptions: promisify(
SubscriptionLocator.getUserDeletedSubscriptions
diff --git a/services/web/app/src/Features/Subscription/SubscriptionRouter.js b/services/web/app/src/Features/Subscription/SubscriptionRouter.js
index 26a6c0978e..bc1674072d 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionRouter.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionRouter.js
@@ -64,6 +64,12 @@ module.exports = {
PermissionsController.useCapabilities(),
TeamInvitesController.viewInvite
)
+ webRouter.get(
+ '/subscription/invites/',
+ AuthenticationController.requireLogin(),
+ PermissionsController.useCapabilities(),
+ TeamInvitesController.viewInvites
+ )
webRouter.put(
'/subscription/invites/:token/',
AuthenticationController.requireLogin(),
diff --git a/services/web/app/src/Features/Subscription/TeamInvitesController.js b/services/web/app/src/Features/Subscription/TeamInvitesController.js
index 9ed4d279a1..8d8abba4a3 100644
--- a/services/web/app/src/Features/Subscription/TeamInvitesController.js
+++ b/services/web/app/src/Features/Subscription/TeamInvitesController.js
@@ -171,6 +171,21 @@ async function viewInvite(req, res, next) {
}
}
+async function viewInvites(req, res, next) {
+ const userId = SessionManager.getLoggedInUserId(req.session)
+ const userEmail = await UserGetter.promises.getUserEmail(userId)
+ const groupSubscriptions =
+ await SubscriptionLocator.promises.getGroupsWithTeamInvitesEmail(userEmail)
+
+ const teamInvites = groupSubscriptions.map(groupSubscription =>
+ groupSubscription.teamInvites.find(invite => invite.email === userEmail)
+ )
+
+ return res.render('subscriptions/team/group-invites', {
+ teamInvites,
+ })
+}
+
async function acceptInvite(req, res, next) {
const { token } = req.params
const userId = SessionManager.getLoggedInUserId(req.session)
@@ -255,6 +270,7 @@ async function resendInvite(req, res, next) {
module.exports = {
createInvite: expressify(createInvite),
viewInvite: expressify(viewInvite),
+ viewInvites: expressify(viewInvites),
acceptInvite: expressify(acceptInvite),
revokeInvite,
resendInvite: expressify(resendInvite),
diff --git a/services/web/app/views/subscriptions/team/group-invites.pug b/services/web/app/views/subscriptions/team/group-invites.pug
new file mode 100644
index 0000000000..c68bc39e6e
--- /dev/null
+++ b/services/web/app/views/subscriptions/team/group-invites.pug
@@ -0,0 +1,10 @@
+extends ../../layout-marketing
+
+block entrypointVar
+ - entrypoint = 'pages/user/subscription/group-invites'
+
+block append meta
+ meta(name="ol-teamInvites" data-type="json" content=teamInvites)
+
+block content
+ main.content.content-alt.team-invite#group-invites-root
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 6a573305c3..5cc765f69f 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -454,6 +454,7 @@
"go_to_pdf_location_in_code": "",
"go_to_settings": "",
"group_admin": "",
+ "group_invitations": "",
"group_invite_has_been_sent_to_email": "",
"group_libraries": "",
"group_managed_by_group_administrator": "",
@@ -580,6 +581,7 @@
"is_email_affiliated": "",
"join_now": "",
"join_project": "",
+ "join_team_explanation": "",
"joining": "",
"keep_current_plan": "",
"keep_personal_projects_separate": "",
@@ -1372,6 +1374,7 @@
"view_hub": "",
"view_hub_subtext": "",
"view_in_template_gallery": "",
+ "view_invitation": "",
"view_logs": "",
"view_metrics": "",
"view_metrics_commons_subtext": "",
diff --git a/services/web/frontend/js/features/subscription/components/group-invites/group-invites-item-footer.tsx b/services/web/frontend/js/features/subscription/components/group-invites/group-invites-item-footer.tsx
new file mode 100644
index 0000000000..94fa3d339b
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/group-invites/group-invites-item-footer.tsx
@@ -0,0 +1,28 @@
+import { useTranslation } from 'react-i18next'
+import type { TeamInvite } from '../../../../../../types/team-invite'
+
+type GroupInvitesItemFooterProps = {
+ teamInvite: TeamInvite
+}
+
+export default function GroupInvitesItemFooter({
+ teamInvite,
+}: GroupInvitesItemFooterProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {t('join_team_explanation')}
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/group-invites/group-invites-item.tsx b/services/web/frontend/js/features/subscription/components/group-invites/group-invites-item.tsx
new file mode 100644
index 0000000000..c9291589d1
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/group-invites/group-invites-item.tsx
@@ -0,0 +1,31 @@
+import { Col, Row } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+import GroupInvitesItemFooter from './group-invites-item-footer'
+import type { TeamInvite } from '../../../../../../types/team-invite'
+
+type GroupInvitesItemProps = {
+ teamInvite: TeamInvite
+}
+
+export default function GroupInvitesItem({
+ teamInvite,
+}: GroupInvitesItemProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+
+ {t('invited_to_group', {
+ inviterName: teamInvite.inviterName,
+ })}
+
+
+
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/group-invites/group-invites-root.tsx b/services/web/frontend/js/features/subscription/components/group-invites/group-invites-root.tsx
new file mode 100644
index 0000000000..64a733df31
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/group-invites/group-invites-root.tsx
@@ -0,0 +1,14 @@
+import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
+import GroupInvites from './group-invites'
+
+function GroupInvitesRoot() {
+ const { isReady } = useWaitForI18n()
+
+ if (!isReady) {
+ return null
+ }
+
+ return
+}
+
+export default GroupInvitesRoot
diff --git a/services/web/frontend/js/features/subscription/components/group-invites/group-invites.tsx b/services/web/frontend/js/features/subscription/components/group-invites/group-invites.tsx
new file mode 100644
index 0000000000..2ef7ec6996
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/group-invites/group-invites.tsx
@@ -0,0 +1,34 @@
+import { useEffect } from 'react'
+import { Col, Row } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+import getMeta from '@/utils/meta'
+import { useLocation } from '@/shared/hooks/use-location'
+import GroupInvitesItem from './group-invites-item'
+import type { TeamInvite } from '../../../../../../types/team-invite'
+
+function GroupInvites() {
+ const { t } = useTranslation()
+ const teamInvites: TeamInvite[] = getMeta('ol-teamInvites')
+ const location = useLocation()
+
+ useEffect(() => {
+ if (teamInvites.length === 0) {
+ location.assign('/project')
+ }
+ }, [teamInvites, location])
+
+ return (
+
+
+
+ {t('group_invitations')}
+
+
+ {teamInvites.map(teamInvite => (
+
+ ))}
+
+ )
+}
+
+export default GroupInvites
diff --git a/services/web/frontend/js/pages/user/subscription/group-invites.tsx b/services/web/frontend/js/pages/user/subscription/group-invites.tsx
new file mode 100644
index 0000000000..35e559c110
--- /dev/null
+++ b/services/web/frontend/js/pages/user/subscription/group-invites.tsx
@@ -0,0 +1,8 @@
+import './base'
+import ReactDOM from 'react-dom'
+import GroupInvitesRoot from '@/features/subscription/components/group-invites/group-invites-root'
+
+const element = document.getElementById('group-invites-root')
+if (element) {
+ ReactDOM.render(, element)
+}
diff --git a/services/web/frontend/stories/subscription/group-invites/group-invites.stories.tsx b/services/web/frontend/stories/subscription/group-invites/group-invites.stories.tsx
new file mode 100644
index 0000000000..1021f770e9
--- /dev/null
+++ b/services/web/frontend/stories/subscription/group-invites/group-invites.stories.tsx
@@ -0,0 +1,44 @@
+import GroupInvites from '@/features/subscription/components/group-invites/group-invites'
+import type { TeamInvite } from '../../../../types/team-invite'
+import { useMeta } from '../../hooks/use-meta'
+import { ScopeDecorator } from '../../decorators/scope'
+
+export const GroupInvitesDefault = () => {
+ const teamInvites: TeamInvite[] = [
+ {
+ email: 'email1@exammple.com',
+ token: 'token123',
+ inviterName: 'inviter1@example.com',
+ sentAt: new Date(),
+ _id: '123abc',
+ },
+ {
+ email: 'email2@exammple.com',
+ token: 'token456',
+ inviterName: 'inviter2@example.com',
+ sentAt: new Date(),
+ _id: '456bcd',
+ },
+ ]
+
+ useMeta({ 'ol-teamInvites': teamInvites })
+
+ return (
+
+
+
+ )
+}
+
+export default {
+ title: 'Subscription / Group Invites',
+ component: GroupInvites,
+ args: {
+ show: true,
+ },
+ argTypes: {
+ handleHide: { action: 'close modal' },
+ onDisableSSO: { action: 'callback' },
+ },
+ decorators: [ScopeDecorator],
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 731b230c9c..7ccd38d259 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -718,6 +718,7 @@
"group_admins_get_access_to": "Group admins get access to",
"group_admins_get_access_to_info": "Special features available only on group plans.",
"group_full": "This group is already full",
+ "group_invitations": "Group Invitations",
"group_invite_has_been_sent_to_email": "Group invite has been sent to <0>__email__0>",
"group_libraries": "Group Libraries",
"group_managed_by_group_administrator": "User accounts in this group are managed by the group administrator.",
@@ -2021,6 +2022,7 @@
"view_hub": "View Admin Hub",
"view_hub_subtext": "Access and download subscription statistics and a list of users",
"view_in_template_gallery": "View it in the template gallery",
+ "view_invitation": "View Invitation",
"view_logs": "View logs",
"view_metrics": "View metrics",
"view_metrics_commons_subtext": "Monitor and download usage metrics for your Commons subscription",
diff --git a/services/web/types/admin/subscription.ts b/services/web/types/admin/subscription.ts
index 4d39d92c5a..701a238b9b 100644
--- a/services/web/types/admin/subscription.ts
+++ b/services/web/types/admin/subscription.ts
@@ -1,5 +1,5 @@
import { GroupPolicy } from '../subscription/dashboard/subscription'
-import { TeamInvite } from './team-invite'
+import { TeamInvite } from '../team-invite'
export type Subscription = {
_id: string
diff --git a/services/web/types/admin/team-invite.ts b/services/web/types/admin/team-invite.ts
deleted file mode 100644
index 3b6184026f..0000000000
--- a/services/web/types/admin/team-invite.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export type TeamInvite = {
- email: string
-}
diff --git a/services/web/types/team-invite.ts b/services/web/types/team-invite.ts
new file mode 100644
index 0000000000..e1ff5468ab
--- /dev/null
+++ b/services/web/types/team-invite.ts
@@ -0,0 +1,7 @@
+export type TeamInvite = {
+ email: string
+ token: string
+ inviterName: string
+ sentAt: Date
+ _id: string
+}