Przeglądaj źródła

[web] Migrate `/user/subscription` to BS5 (#20513)

* [web] Initialize BS5 in subscription page

* [web] Update subscription-dashboard.tsx for BS5

* [web] Update row-link.tsx for BS5

* [web] Update modals

* [web] Add `btn` to `btn-inline-link` classes

* [web] Update circle change-to-group circle price element

* [web] Replace `list-item-with-margin-bottom` with `mb-3`

* [web] Update form elements to BS5

* [web] Use `useContactUsModal`

* [web] Adjust tables margin/padding, and more

* [web] Update change-to-group-modal.tsx

* [web] Add gap to subscription buttons

* [web] Remove subscription page colspan for md and above

* [web] Use Notification component

* [web] Update "leave group" buttons

* [web] Fix tests: add `ol-user` meta tag

* [web] Nest .hover-highlight in #subscription-dashboard-root

* [web] Update to OLRow/OLCol

* [web] Update to OLButtons

* [web] Update to OLFormGroup

* [web] Naming: use BSversion prefix

* [web] Set CancelSubscriptionButton as ghost directly in component

* [web] Set "Plan" font size

* [web] Simplify cancel-subscription buttons

* [web] Remove `--neutral-10` ModalFooter background

* [web] Simplify circle styles

* [web] Center discount badge

* [web] Update fieldset label

* [web] Add `<ul>` around RowLink

* [web] Define SCSS for row-link component

* [web] Remove some use of utility classes

* [web] Revert and update `fieldset` changes (fixes tests)

* [web] Fixup some more OLButtons

* [web] Fixup use of OLRow/OLCol

* [web] Reduce spacing below "legend-as-label"

* [web] Use h5 instead of small in OLModalTitle

* [web] Revert OLCol removal on lg screens

I had removed them by mistake because I wasn't using the proper breakpoints

* [web] Add backdrop to nested modal ContactUsModal

* [web] Don't prefill project URL in ContactUsModal

* [web] Fix lint

* [web] Share `className` prop in BS5 and BS3 modals

* [web] Set sub-title font sans serif (BS3)

* [web] Update remaining Alerts to OLNotification

GitOrigin-RevId: 7fd975ae3e992cebfaf71d4e182f8e13ec886d09
Antoine Clausse 2 lat temu
rodzic
commit
9997c4874f
36 zmienionych plików z 760 dodań i 489 usunięć
  1. 1 0
      services/web/app/views/subscriptions/dashboard-react.pug
  2. 0 12
      services/web/frontend/js/features/subscription/components/dashboard/action-button-text.tsx
  3. 12 6
      services/web/frontend/js/features/subscription/components/dashboard/generic-error-alert.tsx
  4. 3 3
      services/web/frontend/js/features/subscription/components/dashboard/group-subscription-membership.tsx
  5. 11 7
      services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx
  6. 29 19
      services/web/frontend/js/features/subscription/components/dashboard/leave-group-modal.tsx
  7. 23 21
      services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx
  8. 24 21
      services/web/frontend/js/features/subscription/components/dashboard/managed-institution.tsx
  9. 14 12
      services/web/frontend/js/features/subscription/components/dashboard/managed-publisher.tsx
  10. 17 8
      services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx
  11. 20 15
      services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
  12. 5 4
      services/web/frontend/js/features/subscription/components/dashboard/reactivate-subscription.tsx
  13. 21 1
      services/web/frontend/js/features/subscription/components/dashboard/row-link.tsx
  14. 9 4
      services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx
  15. 29 44
      services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx
  16. 14 17
      services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx
  17. 14 17
      services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx
  18. 4 5
      services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx
  19. 4 3
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-to-group-plan.tsx
  20. 10 9
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/individual-plans-table.tsx
  21. 13 10
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-plan-modal.tsx
  22. 193 181
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx
  23. 40 22
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx
  24. 40 24
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx
  25. 27 18
      services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx
  26. 2 0
      services/web/frontend/js/features/ui/components/ol/ol-modal.tsx
  27. 7 2
      services/web/frontend/js/shared/hooks/use-contact-us-modal.tsx
  28. 9 0
      services/web/frontend/stylesheets/app/contact-us.less
  29. 8 0
      services/web/frontend/stylesheets/app/group-subscription-modal.less
  30. 4 0
      services/web/frontend/stylesheets/bootstrap-5/components/card.scss
  31. 9 0
      services/web/frontend/stylesheets/bootstrap-5/modals/contact-us-modal.scss
  32. 1 0
      services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
  33. 140 0
      services/web/frontend/stylesheets/bootstrap-5/pages/subscription.scss
  34. 2 0
      services/web/frontend/stylesheets/components/forms.less
  35. 0 4
      services/web/frontend/stylesheets/components/lists.less
  36. 1 0
      services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx

+ 1 - 0
services/web/app/views/subscriptions/dashboard-react.pug

@@ -23,6 +23,7 @@ block append meta
 	meta(name="ol-fromPlansPage" data-type="boolean" content=fromPlansPage)
 	meta(name="ol-fromPlansPage" data-type="boolean" content=fromPlansPage)
 	meta(name="ol-plans", data-type="json" content=plans)
 	meta(name="ol-plans", data-type="json" content=plans)
 	meta(name="ol-groupSettingsEnabledFor", data-type="json" content=groupSettingsEnabledFor)
 	meta(name="ol-groupSettingsEnabledFor", data-type="json" content=groupSettingsEnabledFor)
+	meta(name="ol-user" data-type="json" content=user)
 	if (personalSubscription && personalSubscription.recurly)
 	if (personalSubscription && personalSubscription.recurly)
 		meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
 		meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
 		meta(name="ol-recommendedCurrency" content=personalSubscription.recurly.currency)
 		meta(name="ol-recommendedCurrency" content=personalSubscription.recurly.currency)

+ 0 - 12
services/web/frontend/js/features/subscription/components/dashboard/action-button-text.tsx

@@ -1,12 +0,0 @@
-import { useTranslation } from 'react-i18next'
-
-export default function ActionButtonText({
-  inflight,
-  buttonText,
-}: {
-  inflight: boolean
-  buttonText: string
-}) {
-  const { t } = useTranslation()
-  return <>{!inflight ? buttonText : t('processing_uppercase') + '…'}</>
-}

+ 12 - 6
services/web/frontend/js/features/subscription/components/dashboard/generic-error-alert.tsx

@@ -1,5 +1,5 @@
-import classNames from 'classnames'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 
 export default function GenericErrorAlert({
 export default function GenericErrorAlert({
   className,
   className,
@@ -7,12 +7,18 @@ export default function GenericErrorAlert({
   className?: string
   className?: string
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
-  const alertClassName = classNames('alert', 'alert-danger', className)
 
 
   return (
   return (
-    <div className={alertClassName} aria-live="polite">
-      {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
-      {t('generic_if_problem_continues_contact_us')}.
-    </div>
+    <OLNotification
+      className={className}
+      aria-live="polite"
+      type="error"
+      content={
+        <>
+          {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
+          {t('generic_if_problem_continues_contact_us')}.
+        </>
+      }
+    />
   )
   )
 }
 }

+ 3 - 3
services/web/frontend/js/features/subscription/components/dashboard/group-subscription-membership.tsx

@@ -1,9 +1,9 @@
-import { Button } from 'react-bootstrap'
 import { Trans, useTranslation } from 'react-i18next'
 import { Trans, useTranslation } from 'react-i18next'
 import { MemberGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
 import { MemberGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import { LEAVE_GROUP_MODAL_ID } from './leave-group-modal'
 import { LEAVE_GROUP_MODAL_ID } from './leave-group-modal'
 import getMeta from '../../../../utils/meta'
 import getMeta from '../../../../utils/meta'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 type GroupSubscriptionMembershipProps = {
 type GroupSubscriptionMembershipProps = {
   subscription: MemberGroupSubscription
   subscription: MemberGroupSubscription
@@ -52,9 +52,9 @@ export default function GroupSubscriptionMembership({
         </span>
         </span>
       ) : (
       ) : (
         <span>
         <span>
-          <Button bsStyle="danger" onClick={leaveGroup}>
+          <OLButton variant="danger" onClick={leaveGroup}>
             {t('leave_group')}
             {t('leave_group')}
-          </Button>
+          </OLButton>
         </span>
         </span>
       )}
       )}
       <hr />
       <hr />

+ 11 - 7
services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx

@@ -1,6 +1,7 @@
 import { Trans } from 'react-i18next'
 import { Trans } from 'react-i18next'
 import { Institution } from '../../../../../../types/institution'
 import { Institution } from '../../../../../../types/institution'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 
 function InstitutionMemberships() {
 function InstitutionMemberships() {
   const { institutionMemberships } = useSubscriptionDashboardContext()
   const { institutionMemberships } = useSubscriptionDashboardContext()
@@ -9,13 +10,16 @@ function InstitutionMemberships() {
 
 
   if (!institutionMemberships) {
   if (!institutionMemberships) {
     return (
     return (
-      <div className="alert alert-warning">
-        <p>
-          Sorry, something went wrong. Subscription information related to
-          institutional affiliations may not be displayed. Please try again
-          later.
-        </p>
-      </div>
+      <OLNotification
+        type="warning"
+        content={
+          <p>
+            Sorry, something went wrong. Subscription information related to
+            institutional affiliations may not be displayed. Please try again
+            later.
+          </p>
+        }
+      />
     )
     )
   }
   }
 
 

+ 29 - 19
services/web/frontend/js/features/subscription/components/dashboard/leave-group-modal.tsx

@@ -1,11 +1,16 @@
 import { useCallback, useState } from 'react'
 import { useCallback, useState } from 'react'
-import { Button, Modal } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import { deleteJSON } from '../../../../infrastructure/fetch-json'
 import { deleteJSON } from '../../../../infrastructure/fetch-json'
-import AccessibleModal from '../../../../shared/components/accessible-modal'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import { useLocation } from '../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../shared/hooks/use-location'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 export const LEAVE_GROUP_MODAL_ID = 'leave-group'
 export const LEAVE_GROUP_MODAL_ID = 'leave-group'
 
 
@@ -37,38 +42,43 @@ export default function LeaveGroupModal() {
   }
   }
 
 
   return (
   return (
-    <AccessibleModal
+    <OLModal
       id={LEAVE_GROUP_MODAL_ID}
       id={LEAVE_GROUP_MODAL_ID}
       show
       show
       animation
       animation
       onHide={handleCloseModal}
       onHide={handleCloseModal}
       backdrop="static"
       backdrop="static"
     >
     >
-      <Modal.Header>
-        <Modal.Title>{t('leave_group')}</Modal.Title>
-      </Modal.Header>
+      <OLModalHeader>
+        <OLModalTitle>{t('leave_group')}</OLModalTitle>
+      </OLModalHeader>
 
 
-      <Modal.Body>
+      <OLModalBody>
         <p>{t('sure_you_want_to_leave_group')}</p>
         <p>{t('sure_you_want_to_leave_group')}</p>
-      </Modal.Body>
+      </OLModalBody>
 
 
-      <Modal.Footer>
-        <Button
-          bsStyle={null}
-          className="btn-secondary"
+      <OLModalFooter>
+        <OLButton
+          variant="secondary"
           onClick={handleCloseModal}
           onClick={handleCloseModal}
           disabled={inflight}
           disabled={inflight}
         >
         >
           {t('cancel')}
           {t('cancel')}
-        </Button>
-        <Button
-          bsStyle="danger"
+        </OLButton>
+        <OLButton
+          variant="danger"
           onClick={handleConfirmLeaveGroup}
           onClick={handleConfirmLeaveGroup}
           disabled={inflight}
           disabled={inflight}
+          isLoading={inflight}
+          bs3Props={{
+            loading: inflight
+              ? t('processing_uppercase') + '…'
+              : t('leave_now'),
+          }}
         >
         >
-          {inflight ? t('processing_uppercase') + '…' : t('leave_now')}
-        </Button>
-      </Modal.Footer>
-    </AccessibleModal>
+          {t('processing_uppercase')}
+        </OLButton>
+      </OLModalFooter>
+    </OLModal>
   )
   )
 }
 }

+ 23 - 21
services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx

@@ -100,27 +100,29 @@ export default function ManagedGroupSubscriptions() {
             <p>
             <p>
               <ManagedGroupAdministrator subscription={subscription} />
               <ManagedGroupAdministrator subscription={subscription} />
             </p>
             </p>
-            <RowLink
-              href={`/manage/groups/${subscription._id}/members`}
-              heading={t('manage_members')}
-              subtext={t('manage_group_members_subtext')}
-              icon="groups"
-            />
-            <RowLink
-              href={`/manage/groups/${subscription._id}/managers`}
-              heading={t('manage_group_managers')}
-              subtext={t('manage_managers_subtext')}
-              icon="manage_accounts"
-            />
-            {groupSettingsEnabledFor?.includes(subscription._id) && (
-              <GroupSettingsButton subscription={subscription} />
-            )}
-            <RowLink
-              href={`/metrics/groups/${subscription._id}`}
-              heading={t('view_metrics')}
-              subtext={t('view_metrics_group_subtext')}
-              icon="insights"
-            />
+            <ul className="list-group p-0">
+              <RowLink
+                href={`/manage/groups/${subscription._id}/members`}
+                heading={t('manage_members')}
+                subtext={t('manage_group_members_subtext')}
+                icon="groups"
+              />
+              <RowLink
+                href={`/manage/groups/${subscription._id}/managers`}
+                heading={t('manage_group_managers')}
+                subtext={t('manage_managers_subtext')}
+                icon="manage_accounts"
+              />
+              {groupSettingsEnabledFor?.includes(subscription._id) && (
+                <GroupSettingsButton subscription={subscription} />
+              )}
+              <RowLink
+                href={`/metrics/groups/${subscription._id}`}
+                heading={t('view_metrics')}
+                subtext={t('view_metrics_group_subtext')}
+                icon="insights"
+              />
+            </ul>
             <hr />
             <hr />
           </div>
           </div>
         )
         )

+ 24 - 21
services/web/frontend/js/features/subscription/components/dashboard/managed-institution.tsx

@@ -6,6 +6,7 @@ import { ManagedInstitution as Institution } from '../../../../../../types/subsc
 import { RowLink } from './row-link'
 import { RowLink } from './row-link'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 type ManagedInstitutionProps = {
 type ManagedInstitutionProps = {
   institution: Institution
   institution: Institution
@@ -53,33 +54,35 @@ export default function ManagedInstitution({
           tOptions={{ interpolation: { escapeValue: true } }}
           tOptions={{ interpolation: { escapeValue: true } }}
         />
         />
       </p>
       </p>
-      <RowLink
-        href={`/metrics/institutions/${institution.v1Id}`}
-        heading={t('view_metrics')}
-        subtext={t('view_metrics_commons_subtext')}
-        icon="insights"
-      />
-      <RowLink
-        href={`/institutions/${institution.v1Id}/hub`}
-        heading={t('view_hub')}
-        subtext={t('view_hub_subtext')}
-        icon="account_circle"
-      />
-      <RowLink
-        href={`/manage/institutions/${institution.v1Id}/managers`}
-        heading={t('manage_institution_managers')}
-        subtext={t('manage_managers_subtext')}
-        icon="manage_accounts"
-      />
+      <ul className="list-group p-0">
+        <RowLink
+          href={`/metrics/institutions/${institution.v1Id}`}
+          heading={t('view_metrics')}
+          subtext={t('view_metrics_commons_subtext')}
+          icon="insights"
+        />
+        <RowLink
+          href={`/institutions/${institution.v1Id}/hub`}
+          heading={t('view_hub')}
+          subtext={t('view_hub_subtext')}
+          icon="account_circle"
+        />
+        <RowLink
+          href={`/manage/institutions/${institution.v1Id}/managers`}
+          heading={t('manage_institution_managers')}
+          subtext={t('manage_managers_subtext')}
+          icon="manage_accounts"
+        />
+      </ul>
       <div>
       <div>
         <p>
         <p>
           <span>Monthly metrics emails: </span>
           <span>Monthly metrics emails: </span>
           {subscriptionChanging ? (
           {subscriptionChanging ? (
             <i className="fa fa-spin fa-refresh" />
             <i className="fa fa-spin fa-refresh" />
           ) : (
           ) : (
-            <button
+            <OLButton
+              variant="link"
               className="btn-inline-link"
               className="btn-inline-link"
-              style={{ border: 0 }}
               onClick={e =>
               onClick={e =>
                 changeInstitutionalEmailSubscription(e, institution.v1Id)
                 changeInstitutionalEmailSubscription(e, institution.v1Id)
               }
               }
@@ -89,7 +92,7 @@ export default function ManagedInstitution({
               )
               )
                 ? t('subscribe')
                 ? t('subscribe')
                 : t('unsubscribe')}
                 : t('unsubscribe')}
-            </button>
+            </OLButton>
           )}
           )}
         </p>
         </p>
       </div>
       </div>

+ 14 - 12
services/web/frontend/js/features/subscription/components/dashboard/managed-publisher.tsx

@@ -22,18 +22,20 @@ export default function ManagedPublisher({ publisher }: ManagedPublisherProps) {
           tOptions={{ interpolation: { escapeValue: true } }}
           tOptions={{ interpolation: { escapeValue: true } }}
         />
         />
       </p>
       </p>
-      <RowLink
-        href={`/publishers/${publisher.slug}/hub`}
-        heading={t('view_hub')}
-        subtext={t('view_hub_subtext')}
-        icon="account_circle"
-      />
-      <RowLink
-        href={`/manage/publishers/${publisher.slug}/managers`}
-        heading={t('manage_publisher_managers')}
-        subtext={t('manage_managers_subtext')}
-        icon="manage_accounts"
-      />
+      <ul className="list-group p-0">
+        <RowLink
+          href={`/publishers/${publisher.slug}/hub`}
+          heading={t('view_hub')}
+          subtext={t('view_hub_subtext')}
+          icon="account_circle"
+        />
+        <RowLink
+          href={`/manage/publishers/${publisher.slug}/managers`}
+          heading={t('manage_publisher_managers')}
+          subtext={t('manage_managers_subtext')}
+          icon="manage_accounts"
+        />
+      </ul>
       <hr />
       <hr />
     </div>
     </div>
   )
   )

+ 17 - 8
services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx

@@ -1,9 +1,11 @@
 import { useTranslation, Trans } from 'react-i18next'
 import { useTranslation, Trans } from 'react-i18next'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
-import { FormGroup, Alert } from 'react-bootstrap'
 import getMeta from '../../../../utils/meta'
 import getMeta from '../../../../utils/meta'
 import useAsync from '../../../../shared/hooks/use-async'
 import useAsync from '../../../../shared/hooks/use-async'
 import { postJSON } from '../../../../infrastructure/fetch-json'
 import { postJSON } from '../../../../infrastructure/fetch-json'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
 
 
 function PersonalSubscriptionRecurlySyncEmail() {
 function PersonalSubscriptionRecurlySyncEmail() {
   const { t } = useTranslation()
   const { t } = useTranslation()
@@ -25,9 +27,12 @@ function PersonalSubscriptionRecurlySyncEmail() {
   return (
   return (
     <>
     <>
       <form onSubmit={handleSubmit}>
       <form onSubmit={handleSubmit}>
-        <FormGroup>
+        <OLFormGroup>
           {isSuccess ? (
           {isSuccess ? (
-            <Alert bsStyle="success">{t('recurly_email_updated')}</Alert>
+            <OLNotification
+              type="success"
+              content={t('recurly_email_updated')}
+            />
           ) : (
           ) : (
             <>
             <>
               <p>
               <p>
@@ -40,17 +45,21 @@ function PersonalSubscriptionRecurlySyncEmail() {
                 />
                 />
               </p>
               </p>
               <div>
               <div>
-                <button
-                  className="btn btn-primary"
+                <OLButton
+                  variant="primary"
                   type="submit"
                   type="submit"
                   disabled={isLoading}
                   disabled={isLoading}
+                  isLoading={isLoading}
+                  bs3Props={{
+                    loading: isLoading ? t('updating') + '…' : t('update'),
+                  }}
                 >
                 >
-                  {isLoading ? <>{t('updating')}…</> : t('update')}
-                </button>
+                  {t('update')}
+                </OLButton>
               </div>
               </div>
             </>
             </>
           )}
           )}
-        </FormGroup>
+        </OLFormGroup>
       </form>
       </form>
       <hr />
       <hr />
     </>
     </>

+ 20 - 15
services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx

@@ -5,6 +5,7 @@ import { CanceledSubscription } from './states/canceled'
 import { ExpiredSubscription } from './states/expired'
 import { ExpiredSubscription } from './states/expired'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import PersonalSubscriptionRecurlySyncEmail from './personal-subscription-recurly-sync-email'
 import PersonalSubscriptionRecurlySyncEmail from './personal-subscription-recurly-sync-email'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 
 function PastDueSubscriptionAlert({
 function PastDueSubscriptionAlert({
   subscription,
   subscription,
@@ -13,18 +14,21 @@ function PastDueSubscriptionAlert({
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
   return (
   return (
-    <>
-      <div className="alert alert-danger" role="alert">
-        {t('account_has_past_due_invoice_change_plan_warning')}{' '}
-        <a
-          href={subscription.recurly.accountManagementLink}
-          target="_blank"
-          rel="noreferrer noopener"
-        >
-          {t('view_your_invoices')}
-        </a>
-      </div>
-    </>
+    <OLNotification
+      type="error"
+      content={
+        <>
+          {t('account_has_past_due_invoice_change_plan_warning')}{' '}
+          <a
+            href={subscription.recurly.accountManagementLink}
+            target="_blank"
+            rel="noreferrer noopener"
+          >
+            {t('view_your_invoices')}
+          </a>
+        </>
+      }
+    />
   )
   )
 }
 }
 
 
@@ -75,9 +79,10 @@ function PersonalSubscription() {
         subscription={personalSubscription as RecurlySubscription}
         subscription={personalSubscription as RecurlySubscription}
       />
       />
       {recurlyLoadError && (
       {recurlyLoadError && (
-        <div className="alert alert-warning" role="alert">
-          <strong>{t('payment_provider_unreachable_error')}</strong>
-        </div>
+        <OLNotification
+          type="warning"
+          content={<strong>{t('payment_provider_unreachable_error')}</strong>}
+        />
       )}
       )}
       <hr />
       <hr />
       <PersonalSubscriptionRecurlySyncEmail />
       <PersonalSubscriptionRecurlySyncEmail />

+ 5 - 4
services/web/frontend/js/features/subscription/components/dashboard/reactivate-subscription.tsx

@@ -5,6 +5,7 @@ import useAsync from '../../../../shared/hooks/use-async'
 import { useLocation } from '../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../shared/hooks/use-location'
 import getMeta from '../../../../utils/meta'
 import getMeta from '../../../../utils/meta'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 function ReactivateSubscription() {
 function ReactivateSubscription() {
   const { t } = useTranslation()
   const { t } = useTranslation()
@@ -25,14 +26,14 @@ function ReactivateSubscription() {
   }
   }
 
 
   return (
   return (
-    <button
-      type="button"
-      className="btn btn-primary"
+    <OLButton
+      variant="primary"
       disabled={isLoading || isSuccess}
       disabled={isLoading || isSuccess}
       onClick={handleReactivate}
       onClick={handleReactivate}
+      isLoading={isLoading}
     >
     >
       {t('reactivate_subscription')}
       {t('reactivate_subscription')}
-    </button>
+    </OLButton>
   )
   )
 }
 }
 
 

+ 21 - 1
services/web/frontend/js/features/subscription/components/dashboard/row-link.tsx

@@ -1,4 +1,5 @@
 import MaterialIcon from '../../../../shared/components/material-icon'
 import MaterialIcon from '../../../../shared/components/material-icon'
+import { isBootstrap5 } from '@/features/utils/bootstrap-5'
 
 
 type RowLinkProps = {
 type RowLinkProps = {
   href: string
   href: string
@@ -7,7 +8,11 @@ type RowLinkProps = {
   icon: string
   icon: string
 }
 }
 
 
-export function RowLink({ href, heading, subtext, icon }: RowLinkProps) {
+export function RowLink(props: RowLinkProps) {
+  return isBootstrap5() ? <BS5RowLink {...props} /> : <BS3RowLink {...props} />
+}
+
+function BS3RowLink({ href, heading, subtext, icon }: RowLinkProps) {
   return (
   return (
     <a href={href} className="row-link">
     <a href={href} className="row-link">
       <div className="icon">
       <div className="icon">
@@ -23,3 +28,18 @@ export function RowLink({ href, heading, subtext, icon }: RowLinkProps) {
     </a>
     </a>
   )
   )
 }
 }
+
+function BS5RowLink({ href, heading, subtext, icon }: RowLinkProps) {
+  return (
+    <li className="list-group-item row-link">
+      <a href={href}>
+        <MaterialIcon type={icon} className="p-2 p-md-3" />
+        <div className="flex-grow-1">
+          <strong>{heading}</strong>
+          <div>{subtext}</div>
+        </div>
+        <MaterialIcon type="keyboard_arrow_right" className="p-2 p-md-3" />
+      </a>
+    </li>
+  )
+}

+ 9 - 4
services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx

@@ -14,6 +14,7 @@ import { ChangePlanModal } from './change-plan/modals/change-plan-modal'
 import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
 import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
 import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
 import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
 import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
 import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 export function ActiveSubscription({
 export function ActiveSubscription({
   subscription,
   subscription,
@@ -62,12 +63,13 @@ export function ActiveSubscription({
           subscription.recurly.account.has_past_due_invoice._ !== 'true' && (
           subscription.recurly.account.has_past_due_invoice._ !== 'true' && (
             <>
             <>
               {' '}
               {' '}
-              <button
+              <OLButton
+                variant="link"
                 className="btn-inline-link"
                 className="btn-inline-link"
                 onClick={() => setModalIdShown('change-plan')}
                 onClick={() => setModalIdShown('change-plan')}
               >
               >
                 {t('change_plan')}
                 {t('change_plan')}
-              </button>
+              </OLButton>
             </>
             </>
           )}
           )}
       </p>
       </p>
@@ -103,7 +105,7 @@ export function ActiveSubscription({
         />
         />
       </p>
       </p>
       <PriceExceptions subscription={subscription} />
       <PriceExceptions subscription={subscription} />
-      <p>
+      <p className="d-inline-flex flex-wrap gap-1">
         <a
         <a
           href={subscription.recurly.billingDetailsLink}
           href={subscription.recurly.billingDetailsLink}
           target="_blank"
           target="_blank"
@@ -121,7 +123,10 @@ export function ActiveSubscription({
           {t('view_your_invoices')}
           {t('view_your_invoices')}
         </a>
         </a>
         {!recurlyLoadError && (
         {!recurlyLoadError && (
-          <CancelSubscriptionButton className="btn btn-danger-ghost ms-1" />
+          <>
+            {' '}
+            <CancelSubscriptionButton />
+          </>
         )}
         )}
       </p>
       </p>
 
 

+ 29 - 44
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx

@@ -9,41 +9,40 @@ import {
   redirectAfterCancelSubscriptionUrl,
   redirectAfterCancelSubscriptionUrl,
 } from '../../../../../data/subscription-url'
 } from '../../../../../data/subscription-url'
 import showDowngradeOption from '../../../../../util/show-downgrade-option'
 import showDowngradeOption from '../../../../../util/show-downgrade-option'
-import ActionButtonText from '../../../action-button-text'
 import GenericErrorAlert from '../../../generic-error-alert'
 import GenericErrorAlert from '../../../generic-error-alert'
 import DowngradePlanButton from './downgrade-plan-button'
 import DowngradePlanButton from './downgrade-plan-button'
 import ExtendTrialButton from './extend-trial-button'
 import ExtendTrialButton from './extend-trial-button'
 import { useLocation } from '../../../../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../../../../shared/hooks/use-location'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 const planCodeToDowngradeTo = 'paid-personal'
 const planCodeToDowngradeTo = 'paid-personal'
 
 
 function ConfirmCancelSubscriptionButton({
 function ConfirmCancelSubscriptionButton({
-  buttonClass,
-  buttonText,
-  handleCancelSubscription,
-  isLoadingCancel,
-  isSuccessCancel,
-  isButtonDisabled,
+  showNoThanks,
+  onClick,
+  disabled,
+  isLoading,
 }: {
 }: {
-  buttonClass: string
-  buttonText: string
-  handleCancelSubscription: () => void
-  isLoadingCancel: boolean
-  isSuccessCancel: boolean
-  isButtonDisabled: boolean
+  showNoThanks: boolean
+  onClick: () => void
+  disabled: boolean
+  isLoading: boolean
 }) {
 }) {
+  const { t } = useTranslation()
+  const text = showNoThanks ? t('no_thanks_cancel_now') : t('cancel_my_account')
   return (
   return (
-    <button
-      className={`btn ${buttonClass}`}
-      onClick={handleCancelSubscription}
-      disabled={isButtonDisabled}
+    <OLButton
+      isLoading={isLoading}
+      disabled={disabled}
+      onClick={onClick}
+      className={showNoThanks ? 'btn-inline-link' : undefined}
+      bs3Props={{
+        loading: isLoading ? t('processing_uppercase') + '…' : text,
+      }}
     >
     >
-      <ActionButtonText
-        inflight={isSuccessCancel || isLoadingCancel}
-        buttonText={buttonText}
-      />
-    </button>
+      {text}
+    </OLButton>
   )
   )
 }
 }
 
 
@@ -85,8 +84,7 @@ function NotCancelOption({
         <p>
         <p>
           <ExtendTrialButton
           <ExtendTrialButton
             isButtonDisabled={isButtonDisabled}
             isButtonDisabled={isButtonDisabled}
-            isLoadingSecondaryAction={isLoadingSecondaryAction}
-            isSuccessSecondaryAction={isSuccessSecondaryAction}
+            isLoading={isLoadingSecondaryAction || isSuccessSecondaryAction}
             runAsyncSecondaryAction={runAsyncSecondaryAction}
             runAsyncSecondaryAction={runAsyncSecondaryAction}
           />
           />
         </p>
         </p>
@@ -114,8 +112,7 @@ function NotCancelOption({
         <p>
         <p>
           <DowngradePlanButton
           <DowngradePlanButton
             isButtonDisabled={isButtonDisabled}
             isButtonDisabled={isButtonDisabled}
-            isLoadingSecondaryAction={isLoadingSecondaryAction}
-            isSuccessSecondaryAction={isSuccessSecondaryAction}
+            isLoading={isLoadingSecondaryAction || isSuccessSecondaryAction}
             planToDowngradeTo={planToDowngradeTo}
             planToDowngradeTo={planToDowngradeTo}
             runAsyncSecondaryAction={runAsyncSecondaryAction}
             runAsyncSecondaryAction={runAsyncSecondaryAction}
           />
           />
@@ -130,12 +127,9 @@ function NotCancelOption({
 
 
   return (
   return (
     <p>
     <p>
-      <button
-        className="btn btn-secondary-info btn-secondary"
-        onClick={handleKeepPlan}
-      >
+      <OLButton variant="secondary" onClick={handleKeepPlan}>
         {t('i_want_to_stay')}
         {t('i_want_to_stay')}
-      </button>
+      </OLButton>
     </p>
     </p>
   )
   )
 }
 }
@@ -188,13 +182,6 @@ export function CancelSubscription() {
 
 
   const showExtendFreeTrial = userCanExtendTrial
   const showExtendFreeTrial = userCanExtendTrial
 
 
-  let confirmCancelButtonText = t('cancel_my_account')
-  let confirmCancelButtonClass = 'btn-primary'
-  if (showExtendFreeTrial || showDowngrade) {
-    confirmCancelButtonText = t('no_thanks_cancel_now')
-    confirmCancelButtonClass = 'btn-inline-link'
-  }
-
   return (
   return (
     <div className="text-center">
     <div className="text-center">
       <p>
       <p>
@@ -214,12 +201,10 @@ export function CancelSubscription() {
       />
       />
 
 
       <ConfirmCancelSubscriptionButton
       <ConfirmCancelSubscriptionButton
-        buttonClass={confirmCancelButtonClass}
-        buttonText={confirmCancelButtonText}
-        isButtonDisabled={isButtonDisabled}
-        handleCancelSubscription={handleCancelSubscription}
-        isSuccessCancel={isSuccessCancel}
-        isLoadingCancel={isLoadingCancel}
+        showNoThanks={showExtendFreeTrial || showDowngrade}
+        onClick={handleCancelSubscription}
+        disabled={isButtonDisabled}
+        isLoading={isSuccessCancel || isLoadingCancel}
       />
       />
     </div>
     </div>
   )
   )

+ 14 - 17
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx

@@ -2,20 +2,18 @@ import { useTranslation } from 'react-i18next'
 import { Plan } from '../../../../../../../../../types/subscription/plan'
 import { Plan } from '../../../../../../../../../types/subscription/plan'
 import { postJSON } from '../../../../../../../infrastructure/fetch-json'
 import { postJSON } from '../../../../../../../infrastructure/fetch-json'
 import { subscriptionUpdateUrl } from '../../../../../data/subscription-url'
 import { subscriptionUpdateUrl } from '../../../../../data/subscription-url'
-import ActionButtonText from '../../../action-button-text'
 import { useLocation } from '../../../../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../../../../shared/hooks/use-location'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 export default function DowngradePlanButton({
 export default function DowngradePlanButton({
   isButtonDisabled,
   isButtonDisabled,
-  isLoadingSecondaryAction,
-  isSuccessSecondaryAction,
+  isLoading,
   planToDowngradeTo,
   planToDowngradeTo,
   runAsyncSecondaryAction,
   runAsyncSecondaryAction,
 }: {
 }: {
   isButtonDisabled: boolean
   isButtonDisabled: boolean
-  isLoadingSecondaryAction: boolean
-  isSuccessSecondaryAction: boolean
+  isLoading: boolean
   planToDowngradeTo: Plan
   planToDowngradeTo: Plan
   runAsyncSecondaryAction: (promise: Promise<unknown>) => Promise<unknown>
   runAsyncSecondaryAction: (promise: Promise<unknown>) => Promise<unknown>
 }) {
 }) {
@@ -37,17 +35,16 @@ export default function DowngradePlanButton({
   }
   }
 
 
   return (
   return (
-    <>
-      <button
-        className="btn btn-primary"
-        onClick={handleDowngradePlan}
-        disabled={isButtonDisabled}
-      >
-        <ActionButtonText
-          inflight={isLoadingSecondaryAction || isSuccessSecondaryAction}
-          buttonText={buttonText}
-        />
-      </button>
-    </>
+    <OLButton
+      variant="primary"
+      onClick={handleDowngradePlan}
+      disabled={isButtonDisabled}
+      isLoading={isLoading}
+      bs3Props={{
+        loading: isLoading ? t('processing_uppercase') + '…' : buttonText,
+      }}
+    >
+      {buttonText}
+    </OLButton>
   )
   )
 }
 }

+ 14 - 17
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx

@@ -1,19 +1,17 @@
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import { putJSON } from '../../../../../../../infrastructure/fetch-json'
 import { putJSON } from '../../../../../../../infrastructure/fetch-json'
 import { extendTrialUrl } from '../../../../../data/subscription-url'
 import { extendTrialUrl } from '../../../../../data/subscription-url'
-import ActionButtonText from '../../../action-button-text'
 import { useLocation } from '../../../../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../../../../shared/hooks/use-location'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 export default function ExtendTrialButton({
 export default function ExtendTrialButton({
   isButtonDisabled,
   isButtonDisabled,
-  isLoadingSecondaryAction,
-  isSuccessSecondaryAction,
+  isLoading,
   runAsyncSecondaryAction,
   runAsyncSecondaryAction,
 }: {
 }: {
   isButtonDisabled: boolean
   isButtonDisabled: boolean
-  isLoadingSecondaryAction: boolean
-  isSuccessSecondaryAction: boolean
+  isLoading: boolean
   runAsyncSecondaryAction: (promise: Promise<unknown>) => Promise<unknown>
   runAsyncSecondaryAction: (promise: Promise<unknown>) => Promise<unknown>
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
@@ -30,17 +28,16 @@ export default function ExtendTrialButton({
   }
   }
 
 
   return (
   return (
-    <>
-      <button
-        className="btn btn-primary"
-        onClick={handleExtendTrial}
-        disabled={isButtonDisabled}
-      >
-        <ActionButtonText
-          inflight={isLoadingSecondaryAction || isSuccessSecondaryAction}
-          buttonText={buttonText}
-        />
-      </button>
-    </>
+    <OLButton
+      variant="primary"
+      onClick={handleExtendTrial}
+      disabled={isButtonDisabled}
+      isLoading={isLoading}
+      bs3Props={{
+        loading: isLoading ? t('processing_uppercase') + '…' : buttonText,
+      }}
+    >
+      {buttonText}
+    </OLButton>
   )
   )
 }
 }

+ 4 - 5
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx

@@ -1,10 +1,9 @@
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import * as eventTracking from '../../../../../../infrastructure/event-tracking'
 import * as eventTracking from '../../../../../../infrastructure/event-tracking'
 import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
-export function CancelSubscriptionButton(
-  props: React.ComponentProps<'button'>
-) {
+export function CancelSubscriptionButton() {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { recurlyLoadError, setShowCancellation } =
   const { recurlyLoadError, setShowCancellation } =
     useSubscriptionDashboardContext()
     useSubscriptionDashboardContext()
@@ -17,8 +16,8 @@ export function CancelSubscriptionButton(
   if (recurlyLoadError) return null
   if (recurlyLoadError) return null
 
 
   return (
   return (
-    <button onClick={handleCancelSubscriptionClick} {...props}>
+    <OLButton variant="danger-ghost" onClick={handleCancelSubscriptionClick}>
       {t('cancel_your_subscription')}
       {t('cancel_your_subscription')}
-    </button>
+    </OLButton>
   )
   )
 }
 }

+ 4 - 3
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-to-group-plan.tsx

@@ -1,5 +1,6 @@
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 export function ChangeToGroupPlan() {
 export function ChangeToGroupPlan() {
   const { t } = useTranslation()
   const { t } = useTranslation()
@@ -10,13 +11,13 @@ export function ChangeToGroupPlan() {
   }
   }
 
 
   return (
   return (
-    <div className="card-gray text-center mt-3">
+    <div className="card-gray text-center mt-3 p-3">
       <h2 style={{ marginTop: 0 }}>{t('looking_multiple_licenses')}</h2>
       <h2 style={{ marginTop: 0 }}>{t('looking_multiple_licenses')}</h2>
       <p style={{ margin: 0 }}>{t('reduce_costs_group_licenses')}</p>
       <p style={{ margin: 0 }}>{t('reduce_costs_group_licenses')}</p>
       <br />
       <br />
-      <button className="btn btn-primary" onClick={handleClick}>
+      <OLButton variant="primary" onClick={handleClick}>
         {t('change_to_group_plan')}
         {t('change_to_group_plan')}
-      </button>
+      </OLButton>
     </div>
     </div>
   )
   )
 }
 }

+ 10 - 9
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/individual-plans-table.tsx

@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'
 import { Plan } from '../../../../../../../../../types/subscription/plan'
 import { Plan } from '../../../../../../../../../types/subscription/plan'
 import Icon from '../../../../../../../shared/components/icon'
 import Icon from '../../../../../../../shared/components/icon'
 import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
+import OLButton from '@/features/ui/components/ol/ol-button'
 
 
 function ChangeToPlanButton({ planCode }: { planCode: string }) {
 function ChangeToPlanButton({ planCode }: { planCode: string }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
@@ -12,9 +13,9 @@ function ChangeToPlanButton({ planCode }: { planCode: string }) {
   }
   }
 
 
   return (
   return (
-    <button className="btn btn-primary" onClick={handleClick}>
+    <OLButton variant="primary" onClick={handleClick}>
       {t('change_to_this_plan')}
       {t('change_to_this_plan')}
-    </button>
+    </OLButton>
   )
   )
 }
 }
 
 
@@ -27,9 +28,9 @@ function KeepCurrentPlanButton({ plan }: { plan: Plan }) {
   }
   }
 
 
   return (
   return (
-    <button className="btn btn-primary" onClick={handleClick}>
+    <OLButton variant="primary" onClick={handleClick}>
       {t('keep_current_plan')}
       {t('keep_current_plan')}
-    </button>
+    </OLButton>
   )
   )
 }
 }
 
 
@@ -66,13 +67,13 @@ function PlansRow({ plan }: { plan: Plan }) {
 
 
   return (
   return (
     <tr>
     <tr>
-      <td>
+      <td className="align-middle">
         <strong>{plan.name}</strong>
         <strong>{plan.name}</strong>
       </td>
       </td>
-      <td>
+      <td className="align-middle">
         {plan.displayPrice} / {plan.annual ? t('year') : t('month')}
         {plan.displayPrice} / {plan.annual ? t('year') : t('month')}
       </td>
       </td>
-      <td>
+      <td className="align-middle text-center">
         <ChangePlanButton plan={plan} />
         <ChangePlanButton plan={plan} />
       </td>
       </td>
     </tr>
     </tr>
@@ -97,9 +98,9 @@ export function IndividualPlansTable({ plans }: { plans: Array<Plan> }) {
   if (!plans || recurlyLoadError) return null
   if (!plans || recurlyLoadError) return null
 
 
   return (
   return (
-    <table className="table table-vertically-centered-cells">
+    <table className="table align-middle table-vertically-centered-cells m-0">
       <thead>
       <thead>
-        <tr>
+        <tr className="d-none d-md-table-row">
           <th>{t('name')}</th>
           <th>{t('name')}</th>
           <th>{t('price')}</th>
           <th>{t('price')}</th>
           <th />
           <th />

+ 13 - 10
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-plan-modal.tsx

@@ -1,11 +1,14 @@
-import { Modal } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
 import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
-import AccessibleModal from '../../../../../../../../shared/components/accessible-modal'
 import LoadingSpinner from '../../../../../../../../shared/components/loading-spinner'
 import LoadingSpinner from '../../../../../../../../shared/components/loading-spinner'
 import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
 import { ChangeToGroupPlan } from '../change-to-group-plan'
 import { ChangeToGroupPlan } from '../change-to-group-plan'
 import { IndividualPlansTable } from '../individual-plans-table'
 import { IndividualPlansTable } from '../individual-plans-table'
+import OLModal, {
+  OLModalBody,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
 
 
 function ChangePlanOptions() {
 function ChangePlanOptions() {
   const { plans, queryingIndividualPlansData, recurlyLoadError } =
   const { plans, queryingIndividualPlansData, recurlyLoadError } =
@@ -22,7 +25,7 @@ function ChangePlanOptions() {
   } else {
   } else {
     return (
     return (
       <>
       <>
-        <div className="table-outlined-container">
+        <div className="border rounded px-2 pt-1 table-outlined-container">
           <IndividualPlansTable plans={plans} />
           <IndividualPlansTable plans={plans} />
         </div>
         </div>
         <ChangeToGroupPlan />
         <ChangeToGroupPlan />
@@ -39,14 +42,14 @@ export function ChangePlanModal() {
   if (modalIdShown !== modalId) return null
   if (modalIdShown !== modalId) return null
 
 
   return (
   return (
-    <AccessibleModal id={modalId} show animation onHide={handleCloseModal}>
-      <Modal.Header closeButton>
-        <Modal.Title>{t('change_plan')}</Modal.Title>
-      </Modal.Header>
+    <OLModal id={modalId} show animation onHide={handleCloseModal} size="lg">
+      <OLModalHeader closeButton>
+        <OLModalTitle>{t('change_plan')}</OLModalTitle>
+      </OLModalHeader>
 
 
-      <Modal.Body>
+      <OLModalBody>
         <ChangePlanOptions />
         <ChangePlanOptions />
-      </Modal.Body>
-    </AccessibleModal>
+      </OLModalBody>
+    </OLModal>
   )
   )
 }
 }

+ 193 - 181
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx

@@ -1,16 +1,29 @@
 import { useEffect, useState } from 'react'
 import { useEffect, useState } from 'react'
-import { Modal } from 'react-bootstrap'
 import { useTranslation, Trans } from 'react-i18next'
 import { useTranslation, Trans } from 'react-i18next'
 import { Subscription } from '../../../../../../../../../../types/subscription/dashboard/subscription'
 import { Subscription } from '../../../../../../../../../../types/subscription/dashboard/subscription'
 import { PriceForDisplayData } from '../../../../../../../../../../types/subscription/plan'
 import { PriceForDisplayData } from '../../../../../../../../../../types/subscription/plan'
 import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
 import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
-import AccessibleModal from '../../../../../../../../shared/components/accessible-modal'
 import getMeta from '../../../../../../../../utils/meta'
 import getMeta from '../../../../../../../../utils/meta'
 import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
 import GenericErrorAlert from '../../../../generic-error-alert'
 import GenericErrorAlert from '../../../../generic-error-alert'
 import { subscriptionUpdateUrl } from '../../../../../../data/subscription-url'
 import { subscriptionUpdateUrl } from '../../../../../../data/subscription-url'
 import { getRecurlyGroupPlanCode } from '../../../../../../util/recurly-group-plan-code'
 import { getRecurlyGroupPlanCode } from '../../../../../../util/recurly-group-plan-code'
 import { useLocation } from '../../../../../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../../../../../shared/hooks/use-location'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
+import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
+import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
+import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
+import { useContactUsModal } from '@/shared/hooks/use-contact-us-modal'
+import { UserProvider } from '@/shared/context/user-context'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 
 const educationalPercentDiscount = 40
 const educationalPercentDiscount = 40
 const groupSizeForEducationalDiscount = 10
 const groupSizeForEducationalDiscount = 10
@@ -88,7 +101,7 @@ function GroupPrice({
             })}
             })}
       </span>
       </span>
 
 
-      <br />
+      <BootstrapVersionSwitcher bs3={<br />} />
 
 
       <span className="circle-subtext">
       <span className="circle-subtext">
         <span aria-hidden>
         <span aria-hidden>
@@ -124,6 +137,8 @@ export function ChangeToGroupModal() {
     setGroupPlanToChangeToSize,
     setGroupPlanToChangeToSize,
     setGroupPlanToChangeToUsage,
     setGroupPlanToChangeToUsage,
   } = useSubscriptionDashboardContext()
   } = useSubscriptionDashboardContext()
+  const { modal: contactModal, showModal: showContactModal } =
+    useContactUsModal({ autofillProjectUrl: false })
   const groupPlans = getMeta('ol-groupPlans')
   const groupPlans = getMeta('ol-groupPlans')
   const personalSubscription = getMeta('ol-subscription') as Subscription
   const personalSubscription = getMeta('ol-subscription') as Subscription
   const [error, setError] = useState(false)
   const [error, setError] = useState(false)
@@ -157,11 +172,6 @@ export function ChangeToGroupModal() {
     }
     }
   }, [personalSubscription, setGroupPlanToChangeToCode])
   }, [personalSubscription, setGroupPlanToChangeToCode])
 
 
-  function handleGetInTouchButton() {
-    handleCloseModal()
-    $('[data-ol-contact-form-modal="contact-us"]').modal()
-  }
-
   if (
   if (
     modalIdShown !== modalId ||
     modalIdShown !== modalId ||
     !groupPlans ||
     !groupPlans ||
@@ -172,197 +182,199 @@ export function ChangeToGroupModal() {
     return null
     return null
 
 
   return (
   return (
-    <AccessibleModal
-      id={modalId}
-      show
-      animation
-      onHide={handleCloseModal}
-      backdrop="static"
-    >
-      <Modal.Header>
-        <button className="close" onClick={handleCloseModal}>
-          <span aria-hidden="true">×</span>
-          <span className="sr-only">{t('close')}</span>
-        </button>
-        <div className="modal-title">
-          <h2>{t('customize_your_group_subscription')}</h2>
-          <h3>
-            {t('save_x_percent_or_more', {
-              percent: '30',
-            })}
-          </h3>
-        </div>
-      </Modal.Header>
+    <>
+      <UserProvider>{contactModal}</UserProvider>
+      <OLModal
+        id={modalId}
+        show
+        animation
+        onHide={handleCloseModal}
+        backdrop="static"
+      >
+        <OLModalHeader closeButton>
+          <OLModalTitle className="lh-sm">
+            {t('customize_your_group_subscription')}
+            <br />
+            <span className="h5">
+              {t('save_x_percent_or_more', {
+                percent: '30',
+              })}
+            </span>
+          </OLModalTitle>
+        </OLModalHeader>
 
 
-      <Modal.Body>
-        <div className="container-fluid plans group-subscription-modal">
-          {groupPlanToChangeToPriceError && <GenericErrorAlert />}
-          <div className="row">
-            <div className="col-md-6 text-center">
-              <div className="circle circle-lg">
-                <GroupPrice
-                  groupPlanToChangeToPrice={groupPlanToChangeToPrice}
-                  queryingGroupPlanToChangeToPrice={
-                    queryingGroupPlanToChangeToPrice
-                  }
-                />
+        <OLModalBody>
+          <div className="container-fluid plans group-subscription-modal">
+            {groupPlanToChangeToPriceError && <GenericErrorAlert />}
+            <div className="row">
+              <div className="col-md-6 text-center">
+                <div className="circle circle-lg mb-4 mx-auto">
+                  <GroupPrice
+                    groupPlanToChangeToPrice={groupPlanToChangeToPrice}
+                    queryingGroupPlanToChangeToPrice={
+                      queryingGroupPlanToChangeToPrice
+                    }
+                  />
+                </div>
+                <p>{t('each_user_will_have_access_to')}:</p>
+                <ul className="list-unstyled">
+                  <li className="mb-3">
+                    <strong>
+                      <GroupPlanCollaboratorCount
+                        planCode={groupPlanToChangeToCode}
+                      />
+                    </strong>
+                  </li>
+                  <li>
+                    <strong>{t('all_premium_features')}</strong>
+                  </li>
+                  <li>{t('sync_dropbox_github')}</li>
+                  <li>{t('full_doc_history')}</li>
+                  <li>{t('track_changes')}</li>
+                  <li>
+                    <span aria-hidden>+ {t('more').toLowerCase()}</span>
+                    <span className="sr-only">{t('plus_more')}</span>
+                  </li>
+                </ul>
               </div>
               </div>
-              <p>{t('each_user_will_have_access_to')}:</p>
-              <ul className="list-unstyled">
-                <li className="list-item-with-margin-bottom">
-                  <strong>
-                    <GroupPlanCollaboratorCount
-                      planCode={groupPlanToChangeToCode}
-                    />
-                  </strong>
-                </li>
-                <li>
-                  <strong>{t('all_premium_features')}</strong>
-                </li>
-                <li>{t('sync_dropbox_github')}</li>
-                <li>{t('full_doc_history')}</li>
-                <li>{t('track_changes')}</li>
-                <li>
-                  <span aria-hidden>+ {t('more').toLowerCase()}</span>
-                  <span className="sr-only">{t('plus_more')}</span>
-                </li>
-              </ul>
-            </div>
 
 
-            <div className="col-md-6">
-              <form className="form">
-                <fieldset className="form-group">
-                  <legend className="legend-as-label">{t('plan')}</legend>
-                  {groupPlans.plans.map(option => (
-                    <label
-                      htmlFor={`plan-option-${option.code}`}
-                      key={option.code}
-                      className="group-plan-option"
-                    >
-                      <input
+              <div className="col-md-6">
+                <form className="form">
+                  <fieldset className="form-group">
+                    <legend className="legend-as-label">{t('plan')}</legend>
+                    {groupPlans.plans.map(option => (
+                      <OLFormCheckbox
+                        key={option.code}
                         type="radio"
                         type="radio"
                         name="plan-code"
                         name="plan-code"
                         value={option.code}
                         value={option.code}
                         id={`plan-option-${option.code}`}
                         id={`plan-option-${option.code}`}
-                        onChange={e =>
-                          setGroupPlanToChangeToCode(e.target.value)
-                        }
+                        onChange={() => setGroupPlanToChangeToCode(option.code)}
                         checked={option.code === groupPlanToChangeToCode}
                         checked={option.code === groupPlanToChangeToCode}
+                        label={option.display}
                       />
                       />
-                      <span>{option.display}</span>
-                    </label>
-                  ))}
-                </fieldset>
-
-                <div className="form-group">
-                  <label htmlFor="size">{t('number_of_users')}</label>
-                  <select
-                    name="size"
-                    id="size"
-                    className="form-control"
-                    value={groupPlanToChangeToSize}
-                    onChange={e => setGroupPlanToChangeToSize(e.target.value)}
-                  >
-                    {groupPlans.sizes.map(size => (
-                      <option key={`size-option-${size}`}>{size}</option>
                     ))}
                     ))}
-                  </select>
-                </div>
+                  </fieldset>
 
 
-                <div className="form-group">
-                  <strong>
-                    {t('percent_discount_for_groups', {
-                      percent: educationalPercentDiscount,
-                      size: groupSizeForEducationalDiscount,
-                    })}
-                  </strong>
-                </div>
+                  <OLFormGroup controlId="size">
+                    <OLFormLabel>{t('number_of_users')}</OLFormLabel>
+                    <OLFormSelect
+                      name="size"
+                      value={groupPlanToChangeToSize}
+                      onChange={e => setGroupPlanToChangeToSize(e.target.value)}
+                    >
+                      {groupPlans.sizes.map(size => (
+                        <option key={`size-option-${size}`}>{size}</option>
+                      ))}
+                    </OLFormSelect>
+                  </OLFormGroup>
 
 
-                <div className="form-group group-plan-option">
-                  <label htmlFor="usage">
-                    <input
-                      id="usage"
-                      type="checkbox"
-                      autoComplete="off"
-                      checked={groupPlanToChangeToUsage === 'educational'}
-                      onChange={e => {
-                        if (e.target.checked) {
-                          setGroupPlanToChangeToUsage('educational')
-                        } else {
-                          setGroupPlanToChangeToUsage('enterprise')
-                        }
-                      }}
-                    />
-                    <span>{t('license_for_educational_purposes')}</span>
-                  </label>
-                </div>
-              </form>
-            </div>
-          </div>
-          <div className="row">
-            <div className="col-md-12 text-center">
-              <div className="educational-discount-badge">
-                {groupPlanToChangeToUsage === 'educational' && (
-                  <EducationDiscountAppliedOrNot
-                    groupSize={groupPlanToChangeToSize}
+                  <OLFormGroup>
+                    <strong>
+                      {t('percent_discount_for_groups', {
+                        percent: educationalPercentDiscount,
+                        size: groupSizeForEducationalDiscount,
+                      })}
+                    </strong>
+                  </OLFormGroup>
+
+                  <OLFormCheckbox
+                    id="usage"
+                    type="checkbox"
+                    autoComplete="off"
+                    checked={groupPlanToChangeToUsage === 'educational'}
+                    onChange={e => {
+                      if (e.target.checked) {
+                        setGroupPlanToChangeToUsage('educational')
+                      } else {
+                        setGroupPlanToChangeToUsage('enterprise')
+                      }
+                    }}
+                    label={t('license_for_educational_purposes')}
                   />
                   />
-                )}
+                </form>
               </div>
               </div>
             </div>
             </div>
+            <div className="educational-discount-badge pt-4 text-center">
+              {groupPlanToChangeToUsage === 'educational' && (
+                <EducationDiscountAppliedOrNot
+                  groupSize={groupPlanToChangeToSize}
+                />
+              )}
+            </div>
           </div>
           </div>
-        </div>
-      </Modal.Body>
+        </OLModalBody>
 
 
-      <Modal.Footer>
-        <div className="text-center">
-          {groupPlanToChangeToPrice?.includesTax && (
+        <OLModalFooter>
+          <div className="text-center">
+            {groupPlanToChangeToPrice?.includesTax && (
+              <p>
+                <Trans
+                  i18nKey="total_with_subtotal_and_tax"
+                  values={{
+                    total: groupPlanToChangeToPrice.totalForDisplay,
+                    subtotal: groupPlanToChangeToPrice.subtotal,
+                    tax: groupPlanToChangeToPrice.tax,
+                  }}
+                  shouldUnescape
+                  tOptions={{ interpolation: { escapeValue: true } }}
+                  components={[
+                    /* eslint-disable-next-line react/jsx-key */
+                    <strong />,
+                  ]}
+                />
+              </p>
+            )}
             <p>
             <p>
-              <Trans
-                i18nKey="total_with_subtotal_and_tax"
-                values={{
-                  total: groupPlanToChangeToPrice.totalForDisplay,
-                  subtotal: groupPlanToChangeToPrice.subtotal,
-                  tax: groupPlanToChangeToPrice.tax,
-                }}
-                shouldUnescape
-                tOptions={{ interpolation: { escapeValue: true } }}
-                components={[
-                  /* eslint-disable-next-line react/jsx-key */
-                  <strong />,
-                ]}
-              />
+              <strong>
+                {t('new_subscription_will_be_billed_immediately')}
+              </strong>
             </p>
             </p>
-          )}
-          <p>
-            <strong>{t('new_subscription_will_be_billed_immediately')}</strong>
-          </p>
-          <hr className="thin" />
-          {error && (
-            <div className="alert alert-danger" aria-live="polite">
-              {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
-              {t('generic_if_problem_continues_contact_us')}.
-            </div>
-          )}
-          <button
-            className="btn btn-primary btn-lg"
-            disabled={
-              queryingGroupPlanToChangeToPrice ||
-              !groupPlanToChangeToPrice ||
-              inflight
-            }
-            onClick={upgrade}
-          >
-            {!inflight ? t('upgrade_now') : t('processing_uppercase') + '…'}
-          </button>
-          <hr className="thin" />
-          <button className="btn-inline-link" onClick={handleGetInTouchButton}>
-            {t('need_more_than_x_licenses', {
-              x: 50,
-            })}{' '}
-            {t('please_get_in_touch')}
-          </button>
-        </div>
-      </Modal.Footer>
-    </AccessibleModal>
+            <hr className="thin my-3" />
+            {error && (
+              <OLNotification
+                type="error"
+                aria-live="polite"
+                content={
+                  <>
+                    {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
+                    {t('generic_if_problem_continues_contact_us')}.
+                  </>
+                }
+              />
+            )}
+            <OLButton
+              variant="primary"
+              size="large"
+              disabled={
+                queryingGroupPlanToChangeToPrice ||
+                !groupPlanToChangeToPrice ||
+                inflight
+              }
+              onClick={upgrade}
+              isLoading={inflight}
+              bs3Props={{
+                loading: inflight
+                  ? t('processing_uppercase') + '…'
+                  : t('upgrade_now'),
+              }}
+            >
+              {t('upgrade_now')}
+            </OLButton>
+            <hr className="thin my-3" />
+            <OLButton
+              variant="link"
+              className="btn-inline-link"
+              onClick={showContactModal}
+            >
+              {t('need_more_than_x_licenses', {
+                x: 50,
+              })}{' '}
+              {t('please_get_in_touch')}
+            </OLButton>
+          </div>
+        </OLModalFooter>
+      </OLModal>
+    </>
   )
   )
 }
 }

+ 40 - 22
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx

@@ -1,13 +1,19 @@
 import { useState } from 'react'
 import { useState } from 'react'
-import { Modal } from 'react-bootstrap'
 import { useTranslation, Trans } from 'react-i18next'
 import { useTranslation, Trans } from 'react-i18next'
 import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
 import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
 import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
 import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
-import AccessibleModal from '../../../../../../../../shared/components/accessible-modal'
 import getMeta from '../../../../../../../../utils/meta'
 import getMeta from '../../../../../../../../utils/meta'
 import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
 import { subscriptionUpdateUrl } from '../../../../../../data/subscription-url'
 import { subscriptionUpdateUrl } from '../../../../../../data/subscription-url'
 import { useLocation } from '../../../../../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../../../../../shared/hooks/use-location'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 
 export function ConfirmChangePlanModal() {
 export function ConfirmChangePlanModal() {
   const modalId: SubscriptionDashModalIds = 'change-to-plan'
   const modalId: SubscriptionDashModalIds = 'change-to-plan'
@@ -46,23 +52,29 @@ export function ConfirmChangePlanModal() {
     planCodesChangingAtTermEnd.indexOf(planCodeToChangeTo) > -1
     planCodesChangingAtTermEnd.indexOf(planCodeToChangeTo) > -1
 
 
   return (
   return (
-    <AccessibleModal
+    <OLModal
       id={modalId}
       id={modalId}
       show
       show
       animation
       animation
       onHide={handleCloseModal}
       onHide={handleCloseModal}
       backdrop="static"
       backdrop="static"
     >
     >
-      <Modal.Header>
-        <Modal.Title>{t('change_plan')}</Modal.Title>
-      </Modal.Header>
+      <OLModalHeader>
+        <OLModalTitle>{t('change_plan')}</OLModalTitle>
+      </OLModalHeader>
 
 
-      <Modal.Body>
+      <OLModalBody>
         {error && (
         {error && (
-          <div className="alert alert-danger" aria-live="polite">
-            {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
-            {t('generic_if_problem_continues_contact_us')}.
-          </div>
+          <OLNotification
+            type="error"
+            aria-live="polite"
+            content={
+              <>
+                {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
+                {t('generic_if_problem_continues_contact_us')}.
+              </>
+            }
+          />
         )}
         )}
         <p>
         <p>
           <Trans
           <Trans
@@ -84,24 +96,30 @@ export function ConfirmChangePlanModal() {
             <p>{t('want_change_to_apply_before_plan_end')}</p>
             <p>{t('want_change_to_apply_before_plan_end')}</p>
           </>
           </>
         )}
         )}
-      </Modal.Body>
+      </OLModalBody>
 
 
-      <Modal.Footer>
-        <button
+      <OLModalFooter>
+        <OLButton
+          variant="secondary"
           disabled={inflight}
           disabled={inflight}
-          className="btn btn-secondary"
           onClick={handleCloseModal}
           onClick={handleCloseModal}
         >
         >
           {t('cancel')}
           {t('cancel')}
-        </button>
-        <button
+        </OLButton>
+        <OLButton
+          variant="primary"
           disabled={inflight}
           disabled={inflight}
-          className="btn btn-primary"
+          isLoading={inflight}
           onClick={handleConfirmChange}
           onClick={handleConfirmChange}
+          bs3Props={{
+            loading: inflight
+              ? t('processing_uppercase') + '…'
+              : t('change_plan'),
+          }}
         >
         >
-          {!inflight ? t('change_plan') : t('processing_uppercase') + '…'}
-        </button>
-      </Modal.Footer>
-    </AccessibleModal>
+          {t('change_plan')}
+        </OLButton>
+      </OLModalFooter>
+    </OLModal>
   )
   )
 }
 }

+ 40 - 24
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx

@@ -1,12 +1,18 @@
 import { useState } from 'react'
 import { useState } from 'react'
-import { Modal } from 'react-bootstrap'
 import { useTranslation, Trans } from 'react-i18next'
 import { useTranslation, Trans } from 'react-i18next'
 import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
 import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
 import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
 import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
-import AccessibleModal from '../../../../../../../../shared/components/accessible-modal'
 import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
 import { cancelPendingSubscriptionChangeUrl } from '../../../../../../data/subscription-url'
 import { cancelPendingSubscriptionChangeUrl } from '../../../../../../data/subscription-url'
 import { useLocation } from '../../../../../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../../../../../shared/hooks/use-location'
+import OLModal, {
+  OLModalBody,
+  OLModalFooter,
+  OLModalHeader,
+  OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 
 export function KeepCurrentPlanModal() {
 export function KeepCurrentPlanModal() {
   const modalId: SubscriptionDashModalIds = 'keep-current-plan'
   const modalId: SubscriptionDashModalIds = 'keep-current-plan'
@@ -33,23 +39,29 @@ export function KeepCurrentPlanModal() {
   if (modalIdShown !== modalId || !personalSubscription) return null
   if (modalIdShown !== modalId || !personalSubscription) return null
 
 
   return (
   return (
-    <AccessibleModal
+    <OLModal
       id={modalId}
       id={modalId}
       show
       show
       animation
       animation
       onHide={handleCloseModal}
       onHide={handleCloseModal}
       backdrop="static"
       backdrop="static"
     >
     >
-      <Modal.Header>
-        <Modal.Title>{t('change_plan')}</Modal.Title>
-      </Modal.Header>
+      <OLModalHeader>
+        <OLModalTitle>{t('change_plan')}</OLModalTitle>
+      </OLModalHeader>
 
 
-      <Modal.Body>
+      <OLModalBody>
         {error && (
         {error && (
-          <div className="alert alert-danger" aria-live="polite">
-            {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
-            {t('generic_if_problem_continues_contact_us')}.
-          </div>
+          <OLNotification
+            type="error"
+            aria-live="polite"
+            content={
+              <>
+                {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
+                {t('generic_if_problem_continues_contact_us')}.
+              </>
+            }
+          />
         )}
         )}
         <p>
         <p>
           <Trans
           <Trans
@@ -65,26 +77,30 @@ export function KeepCurrentPlanModal() {
             ]}
             ]}
           />
           />
         </p>
         </p>
-      </Modal.Body>
+      </OLModalBody>
 
 
-      <Modal.Footer>
-        <button
+      <OLModalFooter>
+        <OLButton
+          variant="secondary"
           disabled={inflight}
           disabled={inflight}
-          className="btn btn-secondary"
           onClick={handleCloseModal}
           onClick={handleCloseModal}
         >
         >
           {t('cancel')}
           {t('cancel')}
-        </button>
-        <button
+        </OLButton>
+        <OLButton
+          variant="primary"
           disabled={inflight}
           disabled={inflight}
-          className="btn btn-primary"
+          isLoading={inflight}
           onClick={confirmCancelPendingPlanChange}
           onClick={confirmCancelPendingPlanChange}
+          bs3Props={{
+            loading: inflight
+              ? t('processing_uppercase') + '…'
+              : t('revert_pending_plan_change'),
+          }}
         >
         >
-          {!inflight
-            ? t('revert_pending_plan_change')
-            : t('processing_uppercase') + '…'}
-        </button>
-      </Modal.Footer>
-    </AccessibleModal>
+          {t('revert_pending_plan_change')}
+        </OLButton>
+      </OLModalFooter>
+    </OLModal>
   )
   )
 }
 }

+ 27 - 18
services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx

@@ -10,6 +10,10 @@ import ManagedInstitutions from './managed-institutions'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import getMeta from '../../../../utils/meta'
 import getMeta from '../../../../utils/meta'
 import PremiumFeaturesLink from './premium-features-link'
 import PremiumFeaturesLink from './premium-features-link'
+import OLCard from '@/features/ui/components/ol/ol-card'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 
 function SubscriptionDashboard() {
 function SubscriptionDashboard() {
   const { t } = useTranslation()
   const { t } = useTranslation()
@@ -23,30 +27,35 @@ function SubscriptionDashboard() {
 
 
   return (
   return (
     <div className="container">
     <div className="container">
-      <div className="row">
-        <div className="col-md-8 col-md-offset-2">
+      <OLRow>
+        <OLCol lg={{ span: 8, offset: 2 }}>
           {fromPlansPage && (
           {fromPlansPage && (
-            <div className="alert alert-warning" aria-live="polite">
-              {t('you_already_have_a_subscription')}
-            </div>
+            <OLNotification
+              className="mb-4"
+              aria-live="polite"
+              content={t('you_already_have_a_subscription')}
+              type="warning"
+            />
           )}
           )}
-          <div className="card">
+          <OLCard>
             <div className="page-header">
             <div className="page-header">
               <h1>{t('your_subscription')}</h1>
               <h1>{t('your_subscription')}</h1>
             </div>
             </div>
 
 
-            <PersonalSubscription />
-            <ManagedGroupSubscriptions />
-            <ManagedInstitutions />
-            <ManagedPublishers />
-            <GroupSubscriptionMemberships />
-            <InstitutionMemberships />
-            {hasValidActiveSubscription && <PremiumFeaturesLink />}
-            {!hasDisplayedSubscription &&
-              (hasSubscription ? <ContactSupport /> : <FreePlan />)}
-          </div>
-        </div>
-      </div>
+            <div>
+              <PersonalSubscription />
+              <ManagedGroupSubscriptions />
+              <ManagedInstitutions />
+              <ManagedPublishers />
+              <GroupSubscriptionMemberships />
+              <InstitutionMemberships />
+              {hasValidActiveSubscription && <PremiumFeaturesLink />}
+              {!hasDisplayedSubscription &&
+                (hasSubscription ? <ContactSupport /> : <FreePlan />)}
+            </div>
+          </OLCard>
+        </OLCol>
+      </OLRow>
     </div>
     </div>
   )
   )
 }
 }

+ 2 - 0
services/web/frontend/js/features/ui/components/ol/ol-modal.tsx

@@ -50,6 +50,8 @@ export default function OLModal({ children, ...props }: OLModalProps) {
     backdrop: bs5Props.backdrop,
     backdrop: bs5Props.backdrop,
     animation: bs5Props.animation,
     animation: bs5Props.animation,
     id: bs5Props.id,
     id: bs5Props.id,
+    className: bs5Props.className,
+    backdropClassName: bs5Props.backdropClassName,
     ...bs3Props,
     ...bs3Props,
   }
   }
 
 

+ 7 - 2
services/web/frontend/js/shared/hooks/use-contact-us-modal.tsx

@@ -1,5 +1,10 @@
 import importOverleafModules from '../../../macros/import-overleaf-module.macro'
 import importOverleafModules from '../../../macros/import-overleaf-module.macro'
-import { JSXElementConstructor, useCallback, useState } from 'react'
+import {
+  JSXElementConstructor,
+  useCallback,
+  useState,
+  type UIEvent,
+} from 'react'
 
 
 const [contactUsModalModules] = importOverleafModules('contactUsModal')
 const [contactUsModalModules] = importOverleafModules('contactUsModal')
 const ContactUsModal: JSXElementConstructor<{
 const ContactUsModal: JSXElementConstructor<{
@@ -16,7 +21,7 @@ export const useContactUsModal = (options = { autofillProjectUrl: true }) => {
     setShow(false)
     setShow(false)
   }, [])
   }, [])
 
 
-  const showModal = useCallback((event?: Event) => {
+  const showModal = useCallback((event?: Event | UIEvent) => {
     event?.preventDefault()
     event?.preventDefault()
     setShow(true)
     setShow(true)
   }, [])
   }, [])

+ 9 - 0
services/web/frontend/stylesheets/app/contact-us.less

@@ -1,3 +1,12 @@
+// Hack to make the contact modal appear above other modals
+.contact-modal {
+  z-index: 1065;
+}
+
+.contact-backdrop {
+  z-index: 1060;
+}
+
 .contact-us-modal {
 .contact-us-modal {
   textarea {
   textarea {
     height: 120px;
     height: 120px;

+ 8 - 0
services/web/frontend/stylesheets/app/group-subscription-modal.less

@@ -1,3 +1,11 @@
+#change-to-group .modal-header h4 {
+  line-height: 1.33rem;
+
+  .h5 {
+    font-family: @font-family-sans-serif;
+  }
+}
+
 .group-subscription-modal {
 .group-subscription-modal {
   .modal-header {
   .modal-header {
     text-align: center;
     text-align: center;

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

@@ -150,3 +150,7 @@
     }
     }
   }
   }
 }
 }
+
+.card-gray {
+  background-color: var(--neutral-10);
+}

+ 9 - 0
services/web/frontend/stylesheets/bootstrap-5/modals/contact-us-modal.scss

@@ -1,3 +1,12 @@
+// Hack to make the contact modal appear above other modals
+.contact-modal {
+  z-index: 1065;
+}
+
+.contact-backdrop {
+  z-index: 1060;
+}
+
 .contact-us-modal-textarea {
 .contact-us-modal-textarea {
   height: 120px;
   height: 120px;
 }
 }

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/all.scss

@@ -11,5 +11,6 @@
 @import 'editor/outline';
 @import 'editor/outline';
 @import 'editor/file-tree';
 @import 'editor/file-tree';
 @import 'editor/figure-modal';
 @import 'editor/figure-modal';
+@import 'subscription';
 @import 'website-redesign';
 @import 'website-redesign';
 @import 'group-settings';
 @import 'group-settings';

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

@@ -0,0 +1,140 @@
+/**
+ *  MAIN CONTENT
+ */
+
+#subscription-dashboard-root {
+  .hover-highlight {
+    &:hover,
+    &:focus {
+      background-color: var(--neutral-10);
+    }
+  }
+
+  li.row-link {
+    display: flex;
+    border: 0;
+    padding: 0;
+
+    a {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: var(--spacing-02) 0;
+      text-decoration: none;
+      color: var(--neutral-90);
+      width: 100%;
+
+      &:hover {
+        background-color: var(--neutral-10);
+      }
+    }
+  }
+}
+
+/**
+ *  MODALS
+ */
+
+.group-subscription-modal {
+  .circle {
+    font-size: var(--font-size-06);
+    border-radius: 50%;
+    background-color: var(--green-70);
+    color: white;
+    white-space: nowrap;
+    height: 180px;
+    width: 180px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: var(--spacing-02);
+    font-weight: bold;
+
+    .small {
+      opacity: 0.85;
+    }
+
+    .circle-subtext {
+      font-size: var(--font-size-03);
+    }
+  }
+
+  .legend-as-label {
+    font-size: var(--font-size-02);
+    font-weight: 600;
+    color: var(--content-secondary);
+    margin-bottom: 0;
+  }
+
+  .educational-discount-badge {
+    height: 50px;
+
+    p {
+      display: inline-block;
+      font-weight: bold;
+      padding-left: var(--spacing-02);
+      padding-right: var(--spacing-02);
+    }
+
+    .applied {
+      background-color: rgba($green-70, 0.1);
+      color: var(--green-70);
+    }
+
+    .ineligible {
+      background-color: var(--neutral-10);
+    }
+  }
+}
+
+#change-plan {
+  table {
+    @include media-breakpoint-up(lg) {
+      th:last-child,
+      td:last-child {
+        width: 1%; // will expand to fit the content
+      }
+    }
+
+    @include media-breakpoint-down(lg) {
+      display: block;
+
+      thead {
+        display: none;
+      }
+
+      tbody,
+      td,
+      tr {
+        display: inline-block;
+        padding-top: 0;
+        padding-bottom: 0;
+        text-align: center;
+        width: 100%;
+      }
+
+      td:first-child {
+        padding-top: var(--spacing-07);
+      }
+
+      td:last-child {
+        padding-top: var(--spacing-03);
+        padding-bottom: var(--spacing-07);
+      }
+
+      tr {
+        border-bottom: 1px solid var(--bs-border-color);
+
+        td,
+        th {
+          border: 0 !important;
+        }
+      }
+
+      tr:last-child {
+        border-bottom: 0;
+      }
+    }
+  }
+}

+ 2 - 0
services/web/frontend/stylesheets/components/forms.less

@@ -42,6 +42,8 @@ label {
   font-size: @font-size-base;
   font-size: @font-size-base;
   color: @text-color;
   color: @text-color;
   border: 0;
   border: 0;
+  margin-bottom: 0;
+  line-height: @line-height-01;
 }
 }
 
 
 // Normalize form controls
 // Normalize form controls

+ 0 - 4
services/web/frontend/stylesheets/components/lists.less

@@ -48,7 +48,3 @@
     }
     }
   }
   }
 }
 }
-
-.list-item-with-margin-bottom {
-  margin-bottom: @line-height-computed;
-}

+ 1 - 0
services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx

@@ -30,6 +30,7 @@ export function renderWithSubscriptionDashContext(
   options?.metaTags?.forEach(tag =>
   options?.metaTags?.forEach(tag =>
     window.metaAttributesCache.set(tag.name, tag.value)
     window.metaAttributesCache.set(tag.name, tag.value)
   )
   )
+  window.metaAttributesCache.set('ol-user', {})
 
 
   if (!options?.recurlyNotLoaded) {
   if (!options?.recurlyNotLoaded) {
     // @ts-ignore
     // @ts-ignore