Merge pull request #21712 from overleaf/ii-flexible-group-licensing-confirmation-page

[web] Request confirmation page for flexible licensing

GitOrigin-RevId: 855dcbd46c645da75b8c641f0c49670b2e04df3f
This commit is contained in:
ilkin-overleaf 2024-11-11 11:50:35 +02:00 committed by Copybot
parent fc84bdf68b
commit 6345ec3b04
11 changed files with 219 additions and 0 deletions

View file

@ -8,6 +8,8 @@ import SessionManager from '../Authentication/SessionManager.js'
import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
import { expressify } from '@overleaf/promise-utils'
import Modules from '../../infrastructure/Modules.js'
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
import ErrorController from '../Errors/ErrorController.js'
/**
* @import { Subscription } from "../../../../types/subscription/dashboard/subscription"
@ -111,7 +113,33 @@ async function _removeUserFromGroup(
res.sendStatus(200)
}
async function requestConfirmation(req, res) {
const { variant } = await SplitTestHandler.promises.getAssignment(
req,
res,
'flexible-group-licensing'
)
if (variant !== 'enabled') {
return ErrorController.notFound(req, res)
}
try {
const userId = SessionManager.getLoggedInUserId(req.session)
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
res.render('subscriptions/request-confirmation-react', {
groupName: subscription.teamName,
})
} catch (error) {
logger.err({ error }, 'error trying to request seats to subscription')
return res.render('/user/subscription')
}
}
export default {
removeUserFromGroup: expressify(removeUserFromGroup),
removeSelfFromGroup: expressify(removeSelfFromGroup),
requestConfirmation: expressify(requestConfirmation),
}

View file

@ -80,6 +80,13 @@ export default {
SubscriptionGroupController.removeSelfFromGroup
)
webRouter.get(
'/user/subscription/group/request-confirmation',
AuthenticationController.requireLogin(),
RateLimiterMiddleware.rateLimit(subscriptionRateLimiter),
SubscriptionGroupController.requestConfirmation
)
// Team invites
webRouter.get(
'/subscription/invites/:token/',

View file

@ -0,0 +1,13 @@
extends ../layout-marketing
block vars
- bootstrap5PageStatus = 'enabled' // Enforce BS5 version
block entrypointVar
- entrypoint = 'pages/user/subscription/group-management/request-confirmation'
block append meta
meta(name="ol-groupName", data-type="string", content=groupName)
block content
main.content.content-alt#subscription-manage-group-root

View file

@ -603,6 +603,7 @@
"go_to_overleaf": "",
"go_to_pdf_location_in_code": "",
"go_to_settings": "",
"go_to_subscriptions": "",
"group_admin": "",
"group_invitations": "",
"group_invite_has_been_sent_to_email": "",
@ -1019,6 +1020,7 @@
"other": "",
"other_logs_and_files": "",
"other_output_files": "",
"our_team_will_get_back_to_you_shortly": "",
"our_values": "",
"out_of_sync": "",
"out_of_sync_detail": "",
@ -1793,6 +1795,7 @@
"we_are_unable_to_opt_you_into_this_experiment": "",
"we_cant_find_any_sections_or_subsections_in_this_file": "",
"we_do_not_share_personal_information": "",
"we_got_your_request": "",
"we_logged_you_in": "",
"we_sent_new_code": "",
"webinars": "",

View file

@ -0,0 +1,50 @@
import { useTranslation } from 'react-i18next'
import { Card, CardBody, Row, Col } from 'react-bootstrap-5'
import Button from '@/features/ui/components/bootstrap-5/button'
import MaterialIcon from '@/shared/components/material-icon'
import getMeta from '@/utils/meta'
import IconButton from '@/features/ui/components/bootstrap-5/icon-button'
function RequestConfirmation() {
const { t } = useTranslation()
const groupName = getMeta('ol-groupName')
return (
<div className="container">
<Row>
<Col xl={{ span: 4, offset: 4 }} md={{ span: 6, offset: 3 }}>
<div className="group-heading" data-testid="group-heading">
<IconButton
variant="ghost"
href="/user/subscription"
size="lg"
icon="arrow_back"
accessibilityLabel={t('back_to_subscription')}
/>
<h2>{groupName || t('group_subscription')}</h2>
</div>
<Card>
<CardBody className="d-grid gap-3">
<div className="card-icon">
<MaterialIcon type="email" />
</div>
<div className="d-grid gap-2 text-center">
<h3 className="mb-0 fw-bold">{t('we_got_your_request')}</h3>
<div className="card-description-secondary">
{t('our_team_will_get_back_to_you_shortly')}
</div>
</div>
<div className="text-center">
<Button variant="secondary" href="/user/subscription">
{t('go_to_subscriptions')}
</Button>
</div>
</CardBody>
</Card>
</Col>
</Row>
</div>
)
}
export default RequestConfirmation

View file

@ -0,0 +1,8 @@
import '../base'
import ReactDOM from 'react-dom'
import RequestConfirmation from '@/features/group-management/components/request-confirmation'
const element = document.getElementById('subscription-manage-group-root')
if (element) {
ReactDOM.render(<RequestConfirmation />, element)
}

View file

@ -183,3 +183,21 @@
padding: var(--spacing-10) var(--spacing-09);
}
}
.card-icon {
display: flex;
width: max-content;
margin: 0 auto;
padding: var(--spacing-08);
border-radius: 50%;
background-color: var(--bg-light-secondary);
color: var(--content-secondary);
.material-symbols {
font-size: 2rem;
}
}
.card-description-secondary {
color: var(--content-secondary);
}

View file

@ -158,3 +158,21 @@
}
}
}
.group-heading {
display: flex;
align-items: center;
gap: var(--spacing-04);
margin-bottom: var(--spacing-06);
h2 {
@include heading-lg;
margin: 0;
word-break: break-all;
}
.btn-ghost {
--bs-btn-bg: transparent;
}
}

View file

@ -860,6 +860,7 @@
"go_to_pdf_location_in_code": "Go to PDF location in code (Tip: double click on the PDF for best results)",
"go_to_previous_page": "Go to previous page",
"go_to_settings": "Go to settings",
"go_to_subscriptions": "Go to Subscriptions",
"great_for_getting_started": "Great for getting started",
"great_for_small_teams_and_departments": "Great for small teams and departments",
"group": "Group",
@ -1455,6 +1456,7 @@
"other_output_files": "Download other output files",
"other_sessions": "Other Sessions",
"other_ways_to_log_in": "Other ways to log in",
"our_team_will_get_back_to_you_shortly": "Our team will get back to you shortly.",
"our_values": "Our values",
"out_of_sync": "Out of sync",
"out_of_sync_detail": "Sorry, this file has gone out of sync and we need to do a full refresh.<0 /><1>Please see this help guide for more information</1>",
@ -2438,6 +2440,7 @@
"we_cant_confirm_this_email": "We cant confirm this email",
"we_cant_find_any_sections_or_subsections_in_this_file": "We cant find any sections or subsections in this file",
"we_do_not_share_personal_information": "See our <0>Privacy Notice</0> for details of how we treat your personal data",
"we_got_your_request": "Weve got your request",
"we_logged_you_in": "We have logged you in.",
"we_may_also_contact_you_from_time_to_time_by_email_with_a_survey": "<0>We may also contact you</0> from time to time by email with a survey, or to see if you would like to participate in other user research initiatives",
"we_sent_new_code": "Weve sent a new code. If it doesnt arrive, make sure to check your spam and any promotions folders.",

View file

@ -0,0 +1,40 @@
import '../../../helpers/bootstrap-5'
import RequestConfirmation from '@/features/group-management/components/request-confirmation'
describe('request confirmation page', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
})
cy.mount(<RequestConfirmation />)
})
it('renders the back button', function () {
cy.findByTestId('group-heading').within(() => {
cy.findByRole('button', { name: /back to subscription/i }).should(
'have.attr',
'href',
'/user/subscription'
)
})
})
it('shows the group name', function () {
cy.findByTestId('group-heading').within(() => {
cy.findByRole('heading', { name: 'My Awesome Team' })
})
})
it('indicates the message was received', function () {
cy.findByRole('heading', { name: /weve got your request/i })
cy.findByText(/our team will get back to you shortly/i)
})
it('renders the link to subscriptions', function () {
cy.findByRole('button', { name: /go to subscriptions/i }).should(
'have.attr',
'href',
'/user/subscription'
)
})
})

View file

@ -1,5 +1,6 @@
import esmock from 'esmock'
import sinon from 'sinon'
import { expect } from 'chai'
const modulePath =
'../../../../app/src/Features/Subscription/SubscriptionGroupController'
@ -24,6 +25,7 @@ describe('SubscriptionGroupController', function () {
this.subscription = {
_id: this.subscriptionId,
teamName: 'Cool group',
}
this.SubscriptionGroupHandler = {
@ -35,6 +37,7 @@ describe('SubscriptionGroupController', function () {
this.SubscriptionLocator = {
promises: {
getSubscription: sinon.stub().resolves(this.subscription),
getUsersSubscription: sinon.stub().resolves(this.subscription),
},
}
@ -61,6 +64,13 @@ describe('SubscriptionGroupController', function () {
},
}
this.SplitTestHandler = {
promises: {
getAssignment: sinon.stub().resolves({ variant: 'default' }),
},
getAssignment: sinon.stub().yields(null, { variant: 'default' }),
}
this.Controller = await esmock.strict(modulePath, {
'../../../../app/src/Features/Subscription/SubscriptionGroupHandler':
this.SubscriptionGroupHandler,
@ -71,6 +81,12 @@ describe('SubscriptionGroupController', function () {
'../../../../app/src/Features/User/UserAuditLogHandler':
this.UserAuditLogHandler,
'../../../../app/src/infrastructure/Modules': this.Modules,
'../../../../app/src/Features/SplitTests/SplitTestHandler':
this.SplitTestHandler,
'../../../../app/src/Features/Errors/ErrorController':
(this.ErrorController = {
notFound: sinon.stub(),
}),
})
})
@ -253,4 +269,19 @@ describe('SubscriptionGroupController', function () {
this.Controller.removeSelfFromGroup(this.req, res, done)
})
})
describe('add seats', function () {
it('render the request confirmation view', async function () {
this.SplitTestHandler.promises.getAssignment.resolves({
variant: 'enabled',
})
await this.Controller.requestConfirmation(this.req, {
render: (viewPath, viewParams) => {
expect(viewPath).to.equal('subscriptions/request-confirmation-react')
expect(viewParams.groupName).to.equal('Cool group')
},
})
expect(this.ErrorController.notFound).to.not.have.been.called
})
})
})