Merge pull request #18188 from overleaf/jel-react-group-invite

[web] Migrate team invite to React

GitOrigin-RevId: 32e968c3b512020aef9a396808c73a7b4859e6d1
This commit is contained in:
Jessica Lawshe 2024-05-08 08:43:25 -05:00 committed by Copybot
parent 3ae88d80bd
commit c6b88085d5
18 changed files with 800 additions and 24 deletions

View file

@ -14,6 +14,7 @@ const EmailHandler = require('../Email/EmailHandler')
const { RateLimiter } = require('../../infrastructure/RateLimiter')
const Modules = require('../../infrastructure/Modules')
const UserAuditLogHandler = require('../User/UserAuditLogHandler')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const rateLimiters = {
resendGroupInvite: new RateLimiter('resend-group-invite', {
@ -72,6 +73,12 @@ async function viewInvite(req, res, next) {
return ErrorController.notFound(req, res)
}
const { variant } = await SplitTestHandler.promises.getAssignment(
req,
res,
'team-invite-react'
)
let validationStatus = new Map()
if (userId) {
const personalSubscription =
@ -145,17 +152,30 @@ async function viewInvite(req, res, next) {
logger.error({ err }, 'error getting subscription admin email')
}
return res.render('subscriptions/team/invite', {
inviterName: invite.inviterName,
inviteToken: invite.token,
hasIndividualRecurlySubscription,
appName: settings.appName,
expired: req.query.expired,
userRestrictions: Array.from(req.userRestrictions || []),
currentManagedUserAdminEmail,
groupSSOActive,
subscriptionId: subscription._id.toString(),
})
if (variant === 'enabled') {
return res.render('subscriptions/team/invite-react', {
inviterName: invite.inviterName,
inviteToken: invite.token,
hasIndividualRecurlySubscription,
expired: req.query.expired,
userRestrictions: Array.from(req.userRestrictions || []),
currentManagedUserAdminEmail,
groupSSOActive,
subscriptionId: subscription._id.toString(),
})
} else {
return res.render('subscriptions/team/invite', {
inviterName: invite.inviterName,
inviteToken: invite.token,
hasIndividualRecurlySubscription,
appName: settings.appName,
expired: req.query.expired,
userRestrictions: Array.from(req.userRestrictions || []),
currentManagedUserAdminEmail,
groupSSOActive,
subscriptionId: subscription._id.toString(),
})
}
}
} else {
const userByEmail = await UserGetter.promises.getUserByMainEmail(

View file

@ -0,0 +1,17 @@
extends ../../layout-marketing
block entrypointVar
- entrypoint = 'pages/user/subscription/invite'
block append meta
meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription)
meta(name="ol-inviterName" date-type="string" content=inviterName)
meta(name="ol-inviteToken" data-type="string" content=inviteToken)
meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail)
meta(name="ol-expired" data-type="boolean" content=expired)
meta(name="ol-groupSSOActive" data-type="boolean" content=groupSSOActive)
meta(name="ol-subscriptionId" data-type="string" content=subscriptionId)
block content
main.content.content-alt#invite-root

View file

@ -119,6 +119,7 @@
"cancel_anytime": "",
"cancel_my_account": "",
"cancel_my_subscription": "",
"cancel_personal_subscription_first": "",
"cancel_your_subscription": "",
"cannot_invite_non_user": "",
"cannot_invite_self": "",
@ -630,6 +631,7 @@
"join_now": "",
"join_project": "",
"join_team_explanation": "",
"joined_team": "",
"joining": "",
"justify": "",
"keep_current_plan": "",
@ -1178,6 +1180,7 @@
"skip": "",
"something_not_right": "",
"something_went_wrong": "",
"something_went_wrong_canceling_your_subscription": "",
"something_went_wrong_loading_pdf_viewer": "",
"something_went_wrong_processing_the_request": "",
"something_went_wrong_rendering_pdf": "",
@ -1559,6 +1562,7 @@
"you_can_now_enable_sso": "",
"you_can_now_log_in_sso": "",
"you_cant_add_or_change_password_due_to_sso": "",
"you_cant_join_this_group_subscription": "",
"you_dont_have_any_repositories": "",
"you_have_added_x_of_group_size_y": "",
"you_have_been_invited_to_transfer_management_of_your_account": "",
@ -1569,6 +1573,7 @@
"youll_get_best_results_in_visual_but_can_be_used_in_source": "",
"youll_need_to_ask_the_github_repository_owner": "",
"youll_no_longer_need_to_remember_credentials": "",
"your_account_is_managed_by_admin_cant_join_additional_group": "",
"your_affiliation_is_confirmed": "",
"your_browser_does_not_support_this_feature": "",
"your_compile_timed_out": "",

View file

@ -101,13 +101,7 @@ export function useGroupInvitationNotification(
const cancelPersonalSubscription = useCallback(() => {
setGroupInvitationStatus(GroupInvitationStatus.AskToJoin)
runAsync(
postJSON('/user/subscription/cancel', {
body: {
_csrf: getMeta('ol-csrfToken'),
},
})
).catch(debugConsole.error)
runAsync(postJSON('/user/subscription/cancel')).catch(debugConsole.error)
}, [runAsync])
const dismissGroupInviteNotification = useCallback(() => {

View file

@ -0,0 +1,24 @@
import getMeta from '@/utils/meta'
import { useTranslation } from 'react-i18next'
export default function AcceptedInvite() {
const { t } = useTranslation()
const inviterName = getMeta('ol-inviterName') as string
const groupSSOActive = getMeta('ol-groupSSOActive') as boolean
const subscriptionId = getMeta('ol-subscriptionId') as string
const doneLink = groupSSOActive
? `/subscription/${subscriptionId}/sso_enrollment`
: '/project'
return (
<div className="text-center">
<p>{t('joined_team', { inviterName })}</p>
<p>
<a href={doneLink} className="btn btn-primary">
{t('done')}
</a>
</p>
</div>
)
}

View file

@ -0,0 +1,97 @@
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import getMeta from '@/utils/meta'
import HasIndividualRecurlySubscription from './has-individual-recurly-subscription'
import { useEffect, useState } from 'react'
import { useTranslation, Trans } from 'react-i18next'
import ManagedUserCannotJoin from './managed-user-cannot-join'
import Notification from '@/shared/components/notification'
import JoinGroup from './join-group'
import AcceptedInvite from './accepted-invite'
export type InviteViewTypes =
| 'invite'
| 'invite-accepted'
| 'cancel-personal-subscription'
| 'managed-user-cannot-join'
| undefined
function GroupInviteViews() {
const hasIndividualRecurlySubscription = getMeta(
'ol-hasIndividualRecurlySubscription'
) as boolean
const cannotJoinSubscription = getMeta(
'ol-cannot-join-subscription'
) as boolean
useEffect(() => {
if (cannotJoinSubscription) {
setView('managed-user-cannot-join')
} else if (hasIndividualRecurlySubscription) {
setView('cancel-personal-subscription')
} else {
setView('invite')
}
}, [cannotJoinSubscription, hasIndividualRecurlySubscription])
const [view, setView] = useState<InviteViewTypes>(undefined)
if (!view) {
return null
}
if (view === 'managed-user-cannot-join') {
return <ManagedUserCannotJoin />
} else if (view === 'cancel-personal-subscription') {
return <HasIndividualRecurlySubscription setView={setView} />
} else if (view === 'invite') {
return <JoinGroup setView={setView} />
} else if (view === 'invite-accepted') {
return <AcceptedInvite />
}
return null
}
export default function GroupInvite() {
const inviterName = getMeta('ol-inviterName') as string
const expired = getMeta('ol-expired') as boolean
const { isReady } = useWaitForI18n()
const { t } = useTranslation()
if (!isReady) {
return null
}
return (
<div className="container" id="main-content">
{expired && (
<div className="row">
<div className="col-md-8 col-md-offset-2">
<Notification type="error" content={t('email_link_expired')} />
</div>
</div>
)}
<div className="row row-spaced">
<div className="col-md-8 col-md-offset-2">
<div className="card">
<div className="page-header">
<h1 className="text-center">
<Trans
i18nKey="invited_to_group"
values={{ inviterName }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={
/* eslint-disable-next-line react/jsx-key */
[<span className="team-invite-name" />]
}
/>
</h1>
</div>
<GroupInviteViews />
</div>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,70 @@
import { FetchError, postJSON } from '@/infrastructure/fetch-json'
import Notification from '@/shared/components/notification'
import useAsync from '@/shared/hooks/use-async'
import { debugConsole } from '@/utils/debugging'
import getMeta from '@/utils/meta'
import { Dispatch, SetStateAction, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { InviteViewTypes } from './group-invite'
export default function HasIndividualRecurlySubscription({
setView,
}: {
setView: Dispatch<SetStateAction<InviteViewTypes>>
}) {
const { t } = useTranslation()
const {
runAsync,
isLoading: isCancelling,
isError,
} = useAsync<never, FetchError>()
const cancelPersonalSubscription = useCallback(() => {
runAsync(
postJSON('/user/subscription/cancel', {
body: {
_csrf: getMeta('ol-csrfToken'),
},
})
)
.then(() => {
setView('invite')
})
.catch(debugConsole.error)
}, [runAsync, setView])
return (
<>
{isError && (
<Notification
type="error"
content={t('something_went_wrong_canceling_your_subscription')}
className="my-3"
/>
)}
<div className="text-center">
<p>{t('cancel_personal_subscription_first')}</p>
<p>
<button
className="btn btn-secondary"
disabled={isCancelling}
onClick={() => setView('invite')}
>
{t('not_now')}
</button>
&nbsp;&nbsp;
<button
className="btn btn-primary"
disabled={isCancelling}
onClick={() => {
cancelPersonalSubscription()
}}
>
{t('cancel_your_subscription')}
</button>
</p>
</div>
</>
)
}

View file

@ -0,0 +1,73 @@
import { Dispatch, SetStateAction, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { InviteViewTypes } from './group-invite'
import getMeta from '@/utils/meta'
import { FetchError, putJSON } from '@/infrastructure/fetch-json'
import useAsync from '@/shared/hooks/use-async'
import classNames from 'classnames'
import { debugConsole } from '@/utils/debugging'
import Notification from '@/shared/components/notification'
export default function JoinGroup({
setView,
}: {
setView: Dispatch<SetStateAction<InviteViewTypes>>
}) {
const { t } = useTranslation()
const expired = getMeta('ol-expired') as boolean
const inviteToken = getMeta('ol-inviteToken') as string
const {
runAsync,
isLoading: isJoining,
isError,
} = useAsync<never, FetchError>()
const notNowBtnClasses = classNames(
'btn',
'btn-secondary',
isJoining ? 'disabled' : ''
)
const joinTeam = useCallback(() => {
runAsync(putJSON(`/subscription/invites/${inviteToken}`))
.then(() => {
setView('invite-accepted')
})
.catch(debugConsole.error)
}, [inviteToken, runAsync, setView])
if (!inviteToken) {
return null
}
return (
<>
{isError && (
<Notification
type="error"
content={t('generic_something_went_wrong')}
className="my-3"
/>
)}
<div className="text-center">
<p>{t('join_team_explanation')}</p>
{!expired && (
<p>
<a className={notNowBtnClasses} href="/project">
{t('not_now')}
</a>
&nbsp;&nbsp;
<button
className="btn btn-primary"
onClick={() => joinTeam()}
disabled={isJoining}
>
{t('accept_invitation')}
</button>
</p>
)}
</div>
</>
)
}

View file

@ -0,0 +1,31 @@
import { Trans, useTranslation } from 'react-i18next'
import Notification from '@/shared/components/notification'
import getMeta from '@/utils/meta'
export default function ManagedUserCannotJoin() {
const { t } = useTranslation()
const currentManagedUserAdminEmail = getMeta(
'ol-currentManagedUserAdminEmail'
) as string
return (
<Notification
type="info"
title={t('you_cant_join_this_group_subscription')}
content={
<p>
<Trans
i18nKey="your_account_is_managed_by_admin_cant_join_additional_group"
values={{ admin: currentManagedUserAdminEmail }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
<a href="/learn/how-to/Understanding_Managed_Overleaf_Accounts" />,
]}
/>
</p>
}
/>
)
}

View file

@ -0,0 +1,11 @@
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import GroupInvite from './group-invite/group-invite'
export default function InviteRoot() {
const { isReady } = useWaitForI18n()
if (!isReady) {
return null
}
return <GroupInvite />
}

View file

@ -0,0 +1,9 @@
import './base'
import ReactDOM from 'react-dom'
import InviteRoot from '@/features/subscription/components/invite-root'
const element = document.getElementById('invite-root')
if (element) {
ReactDOM.render(<InviteRoot />, element)
}

View file

@ -77,10 +77,8 @@
margin: 3em 0;
}
.team-invite {
.team-invite-name {
word-break: break-word;
}
.team-invite-name {
word-break: break-word;
}
.capitalised {

View file

@ -0,0 +1,39 @@
import { render, screen } from '@testing-library/react'
import AcceptedInvite from '../../../../../../frontend/js/features/subscription/components/group-invite/accepted-invite'
import { expect } from 'chai'
describe('accepted group invite', function () {
afterEach(function () {
window.metaAttributesCache = new Map()
})
it('renders', async function () {
window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
render(<AcceptedInvite />)
await screen.findByText(
'You have joined the group subscription managed by example@overleaf.com'
)
})
it('links to SSO enrollment page for SSO groups', async function () {
window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
window.metaAttributesCache.set('ol-groupSSOActive', true)
window.metaAttributesCache.set('ol-subscriptionId', 'group123')
render(<AcceptedInvite />)
const linkBtn = (await screen.findByRole('link', {
name: 'Done',
})) as HTMLLinkElement
expect(linkBtn.href).to.equal(
'https://www.test-overleaf.com/subscription/group123/sso_enrollment'
)
})
it('links to dash for non-SSO groups', async function () {
window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
render(<AcceptedInvite />)
const linkBtn = (await screen.findByRole('link', {
name: 'Done',
})) as HTMLLinkElement
expect(linkBtn.href).to.equal('https://www.test-overleaf.com/project')
})
})

View file

@ -0,0 +1,125 @@
import { render, screen } from '@testing-library/react'
import { expect } from 'chai'
import GroupInvite from '../../../../../../frontend/js/features/subscription/components/group-invite/group-invite'
describe('group invite', function () {
const inviterName = 'example@overleaf.com'
beforeEach(function () {
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-inviterName', inviterName)
})
afterEach(function () {
window.metaAttributesCache = new Map()
})
it('renders header', async function () {
render(<GroupInvite />)
await screen.findByText(inviterName)
screen.getByText(`has invited you to join a group subscription on Overleaf`)
expect(screen.queryByText('Email link expired, please request a new one.'))
.to.be.null
})
describe('when user has personal subscription', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-hasIndividualRecurlySubscription',
true
)
})
it('renders cancel personal subscription view', async function () {
render(<GroupInvite />)
await screen.findByText(
'You already have an individual subscription, would you like us to cancel this first before joining the group licence?'
)
})
describe('and in a managed group', function () {
// note: this should not be possible but managed user view takes priority over all
beforeEach(function () {
window.metaAttributesCache.set(
'ol-currentManagedUserAdminEmail',
'example@overleaf.com'
)
window.metaAttributesCache.set('ol-cannot-join-subscription', true)
})
it('renders managed user cannot join view', async function () {
render(<GroupInvite />)
await screen.findByText('You cant join this group subscription')
screen.getByText(
'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you cant join additional group subscriptions',
{ exact: false }
)
screen.getByRole('link', { name: 'Read more about Managed Users.' })
})
})
})
describe('when user does not have a personal subscription', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-hasIndividualRecurlySubscription',
false
)
window.metaAttributesCache.set('ol-inviteToken', 'token123')
})
it('does not render cancel personal subscription view', async function () {
render(<GroupInvite />)
await screen.findByText(
'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
)
})
})
describe('when the user is already a managed user in another group', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-currentManagedUserAdminEmail',
'example@overleaf.com'
)
window.metaAttributesCache.set('ol-cannot-join-subscription', true)
})
it('renders managed user cannot join view', async function () {
render(<GroupInvite />)
await screen.findByText(inviterName)
screen.getByText(
`has invited you to join a group subscription on Overleaf`
)
screen.getByText('You cant join this group subscription')
screen.getByText(
'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you cant join additional group subscriptions',
{ exact: false }
)
screen.getByRole('link', { name: 'Read more about Managed Users.' })
})
})
describe('expired', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-expired', true)
})
it('shows error notification when expired', async function () {
render(<GroupInvite />)
await screen.findByText('Email link expired, please request a new one.')
})
})
describe('join view', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-inviteToken', 'token123')
})
it('shows view to join group', async function () {
render(<GroupInvite />)
await screen.findByText(
'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
)
})
})
})

View file

@ -0,0 +1,48 @@
import { expect } from 'chai'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import HasIndividualRecurlySubscription from '../../../../../../frontend/js/features/subscription/components/group-invite/has-individual-recurly-subscription'
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
describe('group invite', function () {
describe('user has a personal subscription', function () {
afterEach(function () {
fetchMock.reset()
})
it('shows option to cancel subscription', async function () {
render(<HasIndividualRecurlySubscription setView={() => {}} />)
await screen.findByText(
'You already have an individual subscription, would you like us to cancel this first before joining the group licence?'
)
screen.getByRole('button', { name: 'Not now' })
screen.getByRole('button', { name: 'Cancel Your Subscription' })
})
it('handles subscription cancellation and calls to change invite view', async function () {
fetchMock.post('/user/subscription/cancel', 200)
const setView = sinon.stub()
render(<HasIndividualRecurlySubscription setView={setView} />)
const button = await screen.findByRole('button', {
name: 'Cancel Your Subscription',
})
fireEvent.click(button)
await waitFor(() => {
expect(setView).to.have.been.calledOnce
})
})
it('shows error message when cancelling subscription fails', async function () {
render(<HasIndividualRecurlySubscription setView={() => {}} />)
const button = await screen.findByRole('button', {
name: 'Cancel Your Subscription',
})
fireEvent.click(button)
await waitFor(() => {
screen.getByText(
'Something went wrong canceling your subscription. Please contact support.'
)
})
})
})
})

View file

@ -0,0 +1,48 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import JoinGroup from '../../../../../../frontend/js/features/subscription/components/group-invite/join-group'
import fetchMock from 'fetch-mock'
import sinon from 'sinon'
describe('join group', function () {
const inviteToken = 'token123'
beforeEach(function () {
window.metaAttributesCache.set('ol-inviteToken', inviteToken)
})
afterEach(function () {
fetchMock.reset()
})
it('shows option to join subscription', async function () {
render(<JoinGroup setView={() => {}} />)
await screen.findByText(
'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
)
screen.getByRole('link', { name: 'Not now' })
screen.getByRole('button', { name: 'Accept invitation' })
})
it('handles success when accepting invite', async function () {
fetchMock.put(`/subscription/invites/${inviteToken}`, 200)
const setView = sinon.stub()
render(<JoinGroup setView={setView} />)
const button = await screen.getByRole('button', {
name: 'Accept invitation',
})
fireEvent.click(button)
await waitFor(() => {
expect(setView).to.have.been.calledOnce
})
})
it('handles errors when accepting invite', async function () {
render(<JoinGroup setView={() => {}} />)
const button = await screen.getByRole('button', {
name: 'Accept invitation',
})
fireEvent.click(button)
await waitFor(() => {
screen.getByText('Sorry, something went wrong')
})
})
})

View file

@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react'
import ManagedUserCannotJoin from '../../../../../../frontend/js/features/subscription/components/group-invite/managed-user-cannot-join'
describe('ManagedUserCannotJoin', function () {
beforeEach(function () {
window.metaAttributesCache.set(
'ol-currentManagedUserAdminEmail',
'example@overleaf.com'
)
window.metaAttributesCache.set('ol-cannot-join-subscription', true)
})
it('renders the component', async function () {
render(<ManagedUserCannotJoin />)
await screen.findByText(
'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you cant join additional group subscriptions',
{ exact: false }
)
screen.getByRole('link', { name: 'Read more about Managed Users.' })
})
})

View file

@ -1,5 +1,6 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath =
'../../../../app/src/Features/Subscription/TeamInvitesController'
@ -26,19 +27,30 @@ describe('TeamInvitesController', function () {
}
this.TeamInvitesHandler = {
promises: { acceptInvite: sinon.stub().resolves(this.subscription) },
promises: {
acceptInvite: sinon.stub().resolves(this.subscription),
getInvite: sinon.stub().resolves({
invite: {
email: this.user.email,
token: 'token123',
inviterName: this.user_email,
},
subscription: this.subscription,
}),
},
}
this.SubscriptionLocator = {
promises: {
hasSSOEnabled: sinon.stub().resolves(true),
getUsersSubscription: sinon.stub().resolves(),
},
}
this.ErrorController = { notFound: sinon.stub() }
this.SessionManager = {
getLoggedInUserId(session) {
return session.user._id
return session.user?._id
},
getSessionUser(session) {
return session.user
@ -75,6 +87,18 @@ describe('TeamInvitesController', function () {
'../User/UserGetter': this.UserGetter,
'../Email/EmailHandler': this.EmailHandler,
'../../infrastructure/RateLimiter': this.RateLimiter,
'../../infrastructure/Modules': (this.Modules = {
promises: {
hooks: {
fire: sinon.stub().resolves([]),
},
},
}),
'../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
promises: {
getAssignment: sinon.stub().resolves({}),
},
}),
},
})
})
@ -99,4 +123,126 @@ describe('TeamInvitesController', function () {
this.Controller.acceptInvite(this.req, res)
})
})
describe('viewInvite', function () {
const req = {
params: { token: 'token123' },
query: {},
session: {
user: { _id: 'user123' },
},
}
describe('hasIndividualRecurlySubscription', function () {
it('is true for personal subscription', function (done) {
this.SubscriptionLocator.promises.getUsersSubscription.resolves({
recurlySubscription_id: 'subscription123',
groupPlan: false,
})
const res = {
render: (template, data) => {
expect(data.hasIndividualRecurlySubscription).to.be.true
done()
},
}
this.Controller.viewInvite(req, res)
})
it('is true for group subscriptions', function (done) {
this.SubscriptionLocator.promises.getUsersSubscription.resolves({
recurlySubscription_id: 'subscription123',
groupPlan: true,
})
const res = {
render: (template, data) => {
expect(data.hasIndividualRecurlySubscription).to.be.false
done()
},
}
this.Controller.viewInvite(req, res)
})
it('is false for canceled subscriptions', function (done) {
this.SubscriptionLocator.promises.getUsersSubscription.resolves({
recurlySubscription_id: 'subscription123',
groupPlan: false,
recurlyStatus: {
state: 'canceled',
},
})
const res = {
render: (template, data) => {
expect(data.hasIndividualRecurlySubscription).to.be.false
done()
},
}
this.Controller.viewInvite(req, res)
})
})
describe('when user is logged out', function () {
it('renders logged out invite page', function (done) {
const res = {
render: template => {
expect(template).to.equal('subscriptions/team/invite_logged_out')
done()
},
}
this.Controller.viewInvite(
{ params: { token: 'token123' }, session: {} },
res
)
})
})
describe('Feature rollout of React migration', function () {
describe('feature rollout is not active', function () {
it('renders old Angular template', function (done) {
const res = {
render: template => {
expect(template).to.equal('subscriptions/team/invite')
done()
},
}
this.Controller.viewInvite(req, res)
})
})
describe('user is on default variant', function () {
beforeEach(function () {
this.SplitTestHandler.promises.getAssignment.resolves({
variant: 'default',
})
})
it('renders old Angular template', function (done) {
const res = {
render: template => {
expect(template).to.equal('subscriptions/team/invite')
done()
},
}
this.Controller.viewInvite(req, res)
})
})
describe('user is on enabled variant', function () {
beforeEach(function () {
this.SplitTestHandler.promises.getAssignment.resolves({
variant: 'enabled',
})
})
it('renders React template', function (done) {
const res = {
render: template => {
expect(template).to.equal('subscriptions/team/invite-react')
done()
},
}
this.Controller.viewInvite(req, res)
})
})
})
})
})