Просмотр исходного кода

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

[web] Request confirmation page for flexible licensing

GitOrigin-RevId: 855dcbd46c645da75b8c641f0c49670b2e04df3f
ilkin-overleaf 1 год назад
Родитель
Сommit
6345ec3b04

+ 28 - 0
services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs

@@ -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),
 }

+ 7 - 0
services/web/app/src/Features/Subscription/SubscriptionRouter.mjs

@@ -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/',

+ 13 - 0
services/web/app/views/subscriptions/request-confirmation-react.pug

@@ -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

+ 3 - 0
services/web/frontend/extracted-translations.json

@@ -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": "",

+ 50 - 0
services/web/frontend/js/features/group-management/components/request-confirmation.tsx

@@ -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

+ 8 - 0
services/web/frontend/js/pages/user/subscription/group-management/request-confirmation.tsx

@@ -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)
+}

+ 18 - 0
services/web/frontend/stylesheets/bootstrap-5/components/card.scss

@@ -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);
+}

+ 18 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/subscription.scss

@@ -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;
+  }
+}

+ 3 - 0
services/web/locales/en.json

@@ -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 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_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_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.",

+ 40 - 0
services/web/test/frontend/features/group-management/components/request-confirmation.spec.tsx

@@ -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'
+    )
+  })
+})

+ 31 - 0
services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs

@@ -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
+    })
+  })
 })