mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
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:
parent
fc84bdf68b
commit
6345ec3b04
11 changed files with 219 additions and 0 deletions
|
@ -8,6 +8,8 @@ import SessionManager from '../Authentication/SessionManager.js'
|
||||||
import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
|
import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
|
||||||
import { expressify } from '@overleaf/promise-utils'
|
import { expressify } from '@overleaf/promise-utils'
|
||||||
import Modules from '../../infrastructure/Modules.js'
|
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"
|
* @import { Subscription } from "../../../../types/subscription/dashboard/subscription"
|
||||||
|
@ -111,7 +113,33 @@ async function _removeUserFromGroup(
|
||||||
res.sendStatus(200)
|
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 {
|
export default {
|
||||||
removeUserFromGroup: expressify(removeUserFromGroup),
|
removeUserFromGroup: expressify(removeUserFromGroup),
|
||||||
removeSelfFromGroup: expressify(removeSelfFromGroup),
|
removeSelfFromGroup: expressify(removeSelfFromGroup),
|
||||||
|
requestConfirmation: expressify(requestConfirmation),
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,13 @@ export default {
|
||||||
SubscriptionGroupController.removeSelfFromGroup
|
SubscriptionGroupController.removeSelfFromGroup
|
||||||
)
|
)
|
||||||
|
|
||||||
|
webRouter.get(
|
||||||
|
'/user/subscription/group/request-confirmation',
|
||||||
|
AuthenticationController.requireLogin(),
|
||||||
|
RateLimiterMiddleware.rateLimit(subscriptionRateLimiter),
|
||||||
|
SubscriptionGroupController.requestConfirmation
|
||||||
|
)
|
||||||
|
|
||||||
// Team invites
|
// Team invites
|
||||||
webRouter.get(
|
webRouter.get(
|
||||||
'/subscription/invites/:token/',
|
'/subscription/invites/:token/',
|
||||||
|
|
|
@ -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
|
|
@ -603,6 +603,7 @@
|
||||||
"go_to_overleaf": "",
|
"go_to_overleaf": "",
|
||||||
"go_to_pdf_location_in_code": "",
|
"go_to_pdf_location_in_code": "",
|
||||||
"go_to_settings": "",
|
"go_to_settings": "",
|
||||||
|
"go_to_subscriptions": "",
|
||||||
"group_admin": "",
|
"group_admin": "",
|
||||||
"group_invitations": "",
|
"group_invitations": "",
|
||||||
"group_invite_has_been_sent_to_email": "",
|
"group_invite_has_been_sent_to_email": "",
|
||||||
|
@ -1019,6 +1020,7 @@
|
||||||
"other": "",
|
"other": "",
|
||||||
"other_logs_and_files": "",
|
"other_logs_and_files": "",
|
||||||
"other_output_files": "",
|
"other_output_files": "",
|
||||||
|
"our_team_will_get_back_to_you_shortly": "",
|
||||||
"our_values": "",
|
"our_values": "",
|
||||||
"out_of_sync": "",
|
"out_of_sync": "",
|
||||||
"out_of_sync_detail": "",
|
"out_of_sync_detail": "",
|
||||||
|
@ -1793,6 +1795,7 @@
|
||||||
"we_are_unable_to_opt_you_into_this_experiment": "",
|
"we_are_unable_to_opt_you_into_this_experiment": "",
|
||||||
"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": "",
|
"we_do_not_share_personal_information": "",
|
||||||
|
"we_got_your_request": "",
|
||||||
"we_logged_you_in": "",
|
"we_logged_you_in": "",
|
||||||
"we_sent_new_code": "",
|
"we_sent_new_code": "",
|
||||||
"webinars": "",
|
"webinars": "",
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
|
@ -183,3 +183,21 @@
|
||||||
padding: var(--spacing-10) var(--spacing-09);
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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_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_previous_page": "Go to previous page",
|
||||||
"go_to_settings": "Go to settings",
|
"go_to_settings": "Go to settings",
|
||||||
|
"go_to_subscriptions": "Go to Subscriptions",
|
||||||
"great_for_getting_started": "Great for getting started",
|
"great_for_getting_started": "Great for getting started",
|
||||||
"great_for_small_teams_and_departments": "Great for small teams and departments",
|
"great_for_small_teams_and_departments": "Great for small teams and departments",
|
||||||
"group": "Group",
|
"group": "Group",
|
||||||
|
@ -1455,6 +1456,7 @@
|
||||||
"other_output_files": "Download other output files",
|
"other_output_files": "Download other output files",
|
||||||
"other_sessions": "Other Sessions",
|
"other_sessions": "Other Sessions",
|
||||||
"other_ways_to_log_in": "Other ways to log in",
|
"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",
|
"our_values": "Our values",
|
||||||
"out_of_sync": "Out of sync",
|
"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>",
|
"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 can’t confirm this email",
|
"we_cant_confirm_this_email": "We can’t confirm this email",
|
||||||
"we_cant_find_any_sections_or_subsections_in_this_file": "We can’t find any sections or subsections in this file",
|
"we_cant_find_any_sections_or_subsections_in_this_file": "We can’t 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_do_not_share_personal_information": "See our <0>Privacy Notice</0> for details of how we treat your personal data",
|
||||||
|
"we_got_your_request": "We’ve got your request",
|
||||||
"we_logged_you_in": "We have logged you in.",
|
"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_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": "We’ve sent a new code. If it doesn’t arrive, make sure to check your spam and any promotions folders.",
|
"we_sent_new_code": "We’ve sent a new code. If it doesn’t arrive, make sure to check your spam and any promotions folders.",
|
||||||
|
|
|
@ -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: /we’ve 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'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,5 +1,6 @@
|
||||||
import esmock from 'esmock'
|
import esmock from 'esmock'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
|
import { expect } from 'chai'
|
||||||
const modulePath =
|
const modulePath =
|
||||||
'../../../../app/src/Features/Subscription/SubscriptionGroupController'
|
'../../../../app/src/Features/Subscription/SubscriptionGroupController'
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ describe('SubscriptionGroupController', function () {
|
||||||
|
|
||||||
this.subscription = {
|
this.subscription = {
|
||||||
_id: this.subscriptionId,
|
_id: this.subscriptionId,
|
||||||
|
teamName: 'Cool group',
|
||||||
}
|
}
|
||||||
|
|
||||||
this.SubscriptionGroupHandler = {
|
this.SubscriptionGroupHandler = {
|
||||||
|
@ -35,6 +37,7 @@ describe('SubscriptionGroupController', function () {
|
||||||
this.SubscriptionLocator = {
|
this.SubscriptionLocator = {
|
||||||
promises: {
|
promises: {
|
||||||
getSubscription: sinon.stub().resolves(this.subscription),
|
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, {
|
this.Controller = await esmock.strict(modulePath, {
|
||||||
'../../../../app/src/Features/Subscription/SubscriptionGroupHandler':
|
'../../../../app/src/Features/Subscription/SubscriptionGroupHandler':
|
||||||
this.SubscriptionGroupHandler,
|
this.SubscriptionGroupHandler,
|
||||||
|
@ -71,6 +81,12 @@ describe('SubscriptionGroupController', function () {
|
||||||
'../../../../app/src/Features/User/UserAuditLogHandler':
|
'../../../../app/src/Features/User/UserAuditLogHandler':
|
||||||
this.UserAuditLogHandler,
|
this.UserAuditLogHandler,
|
||||||
'../../../../app/src/infrastructure/Modules': this.Modules,
|
'../../../../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)
|
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
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue