[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
This commit is contained in:
Antoine Clausse 2024-09-30 11:49:18 +02:00 committed by Copybot
parent d4bf47932e
commit 9997c4874f
36 changed files with 763 additions and 492 deletions

View file

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

View file

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

View file

@ -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"> <OLNotification
className={className}
aria-live="polite"
type="error"
content={
<>
{t('generic_something_went_wrong')}. {t('try_again')}.{' '} {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
{t('generic_if_problem_continues_contact_us')}. {t('generic_if_problem_continues_contact_us')}.
</div> </>
}
/>
) )
} }

View file

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

View file

@ -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"> <OLNotification
type="warning"
content={
<p> <p>
Sorry, something went wrong. Subscription information related to Sorry, something went wrong. Subscription information related to
institutional affiliations may not be displayed. Please try again institutional affiliations may not be displayed. Please try again
later. later.
</p> </p>
</div> }
/>
) )
} }

View file

@ -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> <OLModalHeader>
<Modal.Title>{t('leave_group')}</Modal.Title> <OLModalTitle>{t('leave_group')}</OLModalTitle>
</Modal.Header> </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> <OLModalFooter>
<Button <OLButton
bsStyle={null} variant="secondary"
className="btn-secondary"
onClick={handleCloseModal} onClick={handleCloseModal}
disabled={inflight} disabled={inflight}
> >
{t('cancel')} {t('cancel')}
</Button> </OLButton>
<Button <OLButton
bsStyle="danger" 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')} {t('processing_uppercase')}
</Button> </OLButton>
</Modal.Footer> </OLModalFooter>
</AccessibleModal> </OLModal>
) )
} }

View file

@ -100,6 +100,7 @@ export default function ManagedGroupSubscriptions() {
<p> <p>
<ManagedGroupAdministrator subscription={subscription} /> <ManagedGroupAdministrator subscription={subscription} />
</p> </p>
<ul className="list-group p-0">
<RowLink <RowLink
href={`/manage/groups/${subscription._id}/members`} href={`/manage/groups/${subscription._id}/members`}
heading={t('manage_members')} heading={t('manage_members')}
@ -121,6 +122,7 @@ export default function ManagedGroupSubscriptions() {
subtext={t('view_metrics_group_subtext')} subtext={t('view_metrics_group_subtext')}
icon="insights" icon="insights"
/> />
</ul>
<hr /> <hr />
</div> </div>
) )

View file

@ -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,6 +54,7 @@ export default function ManagedInstitution({
tOptions={{ interpolation: { escapeValue: true } }} tOptions={{ interpolation: { escapeValue: true } }}
/> />
</p> </p>
<ul className="list-group p-0">
<RowLink <RowLink
href={`/metrics/institutions/${institution.v1Id}`} href={`/metrics/institutions/${institution.v1Id}`}
heading={t('view_metrics')} heading={t('view_metrics')}
@ -71,15 +73,16 @@ export default function ManagedInstitution({
subtext={t('manage_managers_subtext')} subtext={t('manage_managers_subtext')}
icon="manage_accounts" 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>

View file

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

View file

@ -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 <OLButton
className="btn btn-primary" variant="primary"
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? t('updating') + '…' : t('update'),
}}
> >
{isLoading ? <>{t('updating')}</> : t('update')} {t('update')}
</button> </OLButton>
</div> </div>
</> </>
)} )}
</FormGroup> </OLFormGroup>
</form> </form>
<hr /> <hr />
</> </>

View file

@ -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,8 +14,10 @@ function PastDueSubscriptionAlert({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<OLNotification
type="error"
content={
<> <>
<div className="alert alert-danger" role="alert">
{t('account_has_past_due_invoice_change_plan_warning')}{' '} {t('account_has_past_due_invoice_change_plan_warning')}{' '}
<a <a
href={subscription.recurly.accountManagementLink} href={subscription.recurly.accountManagementLink}
@ -23,8 +26,9 @@ function PastDueSubscriptionAlert({
> >
{t('view_your_invoices')} {t('view_your_invoices')}
</a> </a>
</div>
</> </>
}
/>
) )
} }
@ -75,9 +79,10 @@ function PersonalSubscription() {
subscription={personalSubscription as RecurlySubscription} subscription={personalSubscription as RecurlySubscription}
/> />
{recurlyLoadError && ( {recurlyLoadError && (
<div className="alert alert-warning" role="alert"> <OLNotification
<strong>{t('payment_provider_unreachable_error')}</strong> type="warning"
</div> content={<strong>{t('payment_provider_unreachable_error')}</strong>}
/>
)} )}
<hr /> <hr />
<PersonalSubscriptionRecurlySyncEmail /> <PersonalSubscriptionRecurlySyncEmail />

View file

@ -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 <OLButton
type="button" variant="primary"
className="btn btn-primary"
disabled={isLoading || isSuccess} disabled={isLoading || isSuccess}
onClick={handleReactivate} onClick={handleReactivate}
isLoading={isLoading}
> >
{t('reactivate_subscription')} {t('reactivate_subscription')}
</button> </OLButton>
) )
} }

View file

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

View file

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

View file

@ -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, showNoThanks,
buttonText, onClick,
handleCancelSubscription, disabled,
isLoadingCancel, isLoading,
isSuccessCancel,
isButtonDisabled,
}: { }: {
buttonClass: string showNoThanks: boolean
buttonText: string onClick: () => void
handleCancelSubscription: () => void disabled: boolean
isLoadingCancel: boolean isLoading: boolean
isSuccessCancel: boolean
isButtonDisabled: boolean
}) { }) {
const { t } = useTranslation()
const text = showNoThanks ? t('no_thanks_cancel_now') : t('cancel_my_account')
return ( return (
<button <OLButton
className={`btn ${buttonClass}`} isLoading={isLoading}
onClick={handleCancelSubscription} disabled={disabled}
disabled={isButtonDisabled} onClick={onClick}
className={showNoThanks ? 'btn-inline-link' : undefined}
bs3Props={{
loading: isLoading ? t('processing_uppercase') + '…' : text,
}}
> >
<ActionButtonText {text}
inflight={isSuccessCancel || isLoadingCancel} </OLButton>
buttonText={buttonText}
/>
</button>
) )
} }
@ -85,8 +84,7 @@ function NotCancelOption({
<p> <p>
<ExtendTrialButton <ExtendTrialButton
isButtonDisabled={isButtonDisabled} isButtonDisabled={isButtonDisabled}
isLoadingSecondaryAction={isLoadingSecondaryAction} isLoading={isLoadingSecondaryAction || isSuccessSecondaryAction}
isSuccessSecondaryAction={isSuccessSecondaryAction}
runAsyncSecondaryAction={runAsyncSecondaryAction} runAsyncSecondaryAction={runAsyncSecondaryAction}
/> />
</p> </p>
@ -114,8 +112,7 @@ function NotCancelOption({
<p> <p>
<DowngradePlanButton <DowngradePlanButton
isButtonDisabled={isButtonDisabled} isButtonDisabled={isButtonDisabled}
isLoadingSecondaryAction={isLoadingSecondaryAction} isLoading={isLoadingSecondaryAction || isSuccessSecondaryAction}
isSuccessSecondaryAction={isSuccessSecondaryAction}
planToDowngradeTo={planToDowngradeTo} planToDowngradeTo={planToDowngradeTo}
runAsyncSecondaryAction={runAsyncSecondaryAction} runAsyncSecondaryAction={runAsyncSecondaryAction}
/> />
@ -130,12 +127,9 @@ function NotCancelOption({
return ( return (
<p> <p>
<button <OLButton variant="secondary" onClick={handleKeepPlan}>
className="btn btn-secondary-info btn-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} showNoThanks={showExtendFreeTrial || showDowngrade}
buttonText={confirmCancelButtonText} onClick={handleCancelSubscription}
isButtonDisabled={isButtonDisabled} disabled={isButtonDisabled}
handleCancelSubscription={handleCancelSubscription} isLoading={isSuccessCancel || isLoadingCancel}
isSuccessCancel={isSuccessCancel}
isLoadingCancel={isLoadingCancel}
/> />
</div> </div>
) )

View file

@ -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, isLoading,
isSuccessSecondaryAction,
planToDowngradeTo, planToDowngradeTo,
runAsyncSecondaryAction, runAsyncSecondaryAction,
}: { }: {
isButtonDisabled: boolean isButtonDisabled: boolean
isLoadingSecondaryAction: boolean isLoading: boolean
isSuccessSecondaryAction: 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 (
<> <OLButton
<button variant="primary"
className="btn btn-primary"
onClick={handleDowngradePlan} onClick={handleDowngradePlan}
disabled={isButtonDisabled} disabled={isButtonDisabled}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? t('processing_uppercase') + '…' : buttonText,
}}
> >
<ActionButtonText {buttonText}
inflight={isLoadingSecondaryAction || isSuccessSecondaryAction} </OLButton>
buttonText={buttonText}
/>
</button>
</>
) )
} }

View file

@ -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, isLoading,
isSuccessSecondaryAction,
runAsyncSecondaryAction, runAsyncSecondaryAction,
}: { }: {
isButtonDisabled: boolean isButtonDisabled: boolean
isLoadingSecondaryAction: boolean isLoading: boolean
isSuccessSecondaryAction: 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 (
<> <OLButton
<button variant="primary"
className="btn btn-primary"
onClick={handleExtendTrial} onClick={handleExtendTrial}
disabled={isButtonDisabled} disabled={isButtonDisabled}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? t('processing_uppercase') + '…' : buttonText,
}}
> >
<ActionButtonText {buttonText}
inflight={isLoadingSecondaryAction || isSuccessSecondaryAction} </OLButton>
buttonText={buttonText}
/>
</button>
</>
) )
} }

View file

@ -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( export function CancelSubscriptionButton() {
props: React.ComponentProps<'button'>
) {
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>
) )
} }

View file

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

View file

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

View file

@ -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}> <OLModal id={modalId} show animation onHide={handleCloseModal} size="lg">
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{t('change_plan')}</Modal.Title> <OLModalTitle>{t('change_plan')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
<ChangePlanOptions /> <ChangePlanOptions />
</Modal.Body> </OLModalBody>
</AccessibleModal> </OLModal>
) )
} }

View file

@ -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,34 +182,33 @@ export function ChangeToGroupModal() {
return null return null
return ( return (
<AccessibleModal <>
<UserProvider>{contactModal}</UserProvider>
<OLModal
id={modalId} id={modalId}
show show
animation animation
onHide={handleCloseModal} onHide={handleCloseModal}
backdrop="static" backdrop="static"
> >
<Modal.Header> <OLModalHeader closeButton>
<button className="close" onClick={handleCloseModal}> <OLModalTitle className="lh-sm">
<span aria-hidden="true">×</span> {t('customize_your_group_subscription')}
<span className="sr-only">{t('close')}</span> <br />
</button> <span className="h5">
<div className="modal-title">
<h2>{t('customize_your_group_subscription')}</h2>
<h3>
{t('save_x_percent_or_more', { {t('save_x_percent_or_more', {
percent: '30', percent: '30',
})} })}
</h3> </span>
</div> </OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
<div className="container-fluid plans group-subscription-modal"> <div className="container-fluid plans group-subscription-modal">
{groupPlanToChangeToPriceError && <GenericErrorAlert />} {groupPlanToChangeToPriceError && <GenericErrorAlert />}
<div className="row"> <div className="row">
<div className="col-md-6 text-center"> <div className="col-md-6 text-center">
<div className="circle circle-lg"> <div className="circle circle-lg mb-4 mx-auto">
<GroupPrice <GroupPrice
groupPlanToChangeToPrice={groupPlanToChangeToPrice} groupPlanToChangeToPrice={groupPlanToChangeToPrice}
queryingGroupPlanToChangeToPrice={ queryingGroupPlanToChangeToPrice={
@ -209,7 +218,7 @@ export function ChangeToGroupModal() {
</div> </div>
<p>{t('each_user_will_have_access_to')}:</p> <p>{t('each_user_will_have_access_to')}:</p>
<ul className="list-unstyled"> <ul className="list-unstyled">
<li className="list-item-with-margin-bottom"> <li className="mb-3">
<strong> <strong>
<GroupPlanCollaboratorCount <GroupPlanCollaboratorCount
planCode={groupPlanToChangeToCode} planCode={groupPlanToChangeToCode}
@ -234,53 +243,42 @@ export function ChangeToGroupModal() {
<fieldset className="form-group"> <fieldset className="form-group">
<legend className="legend-as-label">{t('plan')}</legend> <legend className="legend-as-label">{t('plan')}</legend>
{groupPlans.plans.map(option => ( {groupPlans.plans.map(option => (
<label <OLFormCheckbox
htmlFor={`plan-option-${option.code}`}
key={option.code} key={option.code}
className="group-plan-option"
>
<input
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 => onChange={() => setGroupPlanToChangeToCode(option.code)}
setGroupPlanToChangeToCode(e.target.value)
}
checked={option.code === groupPlanToChangeToCode} checked={option.code === groupPlanToChangeToCode}
label={option.display}
/> />
<span>{option.display}</span>
</label>
))} ))}
</fieldset> </fieldset>
<div className="form-group"> <OLFormGroup controlId="size">
<label htmlFor="size">{t('number_of_users')}</label> <OLFormLabel>{t('number_of_users')}</OLFormLabel>
<select <OLFormSelect
name="size" name="size"
id="size"
className="form-control"
value={groupPlanToChangeToSize} value={groupPlanToChangeToSize}
onChange={e => setGroupPlanToChangeToSize(e.target.value)} onChange={e => setGroupPlanToChangeToSize(e.target.value)}
> >
{groupPlans.sizes.map(size => ( {groupPlans.sizes.map(size => (
<option key={`size-option-${size}`}>{size}</option> <option key={`size-option-${size}`}>{size}</option>
))} ))}
</select> </OLFormSelect>
</div> </OLFormGroup>
<div className="form-group"> <OLFormGroup>
<strong> <strong>
{t('percent_discount_for_groups', { {t('percent_discount_for_groups', {
percent: educationalPercentDiscount, percent: educationalPercentDiscount,
size: groupSizeForEducationalDiscount, size: groupSizeForEducationalDiscount,
})} })}
</strong> </strong>
</div> </OLFormGroup>
<div className="form-group group-plan-option"> <OLFormCheckbox
<label htmlFor="usage">
<input
id="usage" id="usage"
type="checkbox" type="checkbox"
autoComplete="off" autoComplete="off"
@ -292,16 +290,12 @@ export function ChangeToGroupModal() {
setGroupPlanToChangeToUsage('enterprise') setGroupPlanToChangeToUsage('enterprise')
} }
}} }}
label={t('license_for_educational_purposes')}
/> />
<span>{t('license_for_educational_purposes')}</span>
</label>
</div>
</form> </form>
</div> </div>
</div> </div>
<div className="row"> <div className="educational-discount-badge pt-4 text-center">
<div className="col-md-12 text-center">
<div className="educational-discount-badge">
{groupPlanToChangeToUsage === 'educational' && ( {groupPlanToChangeToUsage === 'educational' && (
<EducationDiscountAppliedOrNot <EducationDiscountAppliedOrNot
groupSize={groupPlanToChangeToSize} groupSize={groupPlanToChangeToSize}
@ -309,11 +303,9 @@ export function ChangeToGroupModal() {
)} )}
</div> </div>
</div> </div>
</div> </OLModalBody>
</div>
</Modal.Body>
<Modal.Footer> <OLModalFooter>
<div className="text-center"> <div className="text-center">
{groupPlanToChangeToPrice?.includesTax && ( {groupPlanToChangeToPrice?.includesTax && (
<p> <p>
@ -334,35 +326,55 @@ export function ChangeToGroupModal() {
</p> </p>
)} )}
<p> <p>
<strong>{t('new_subscription_will_be_billed_immediately')}</strong> <strong>
{t('new_subscription_will_be_billed_immediately')}
</strong>
</p> </p>
<hr className="thin" /> <hr className="thin my-3" />
{error && ( {error && (
<div className="alert alert-danger" aria-live="polite"> <OLNotification
type="error"
aria-live="polite"
content={
<>
{t('generic_something_went_wrong')}. {t('try_again')}.{' '} {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
{t('generic_if_problem_continues_contact_us')}. {t('generic_if_problem_continues_contact_us')}.
</div> </>
}
/>
)} )}
<button <OLButton
className="btn btn-primary btn-lg" variant="primary"
size="large"
disabled={ disabled={
queryingGroupPlanToChangeToPrice || queryingGroupPlanToChangeToPrice ||
!groupPlanToChangeToPrice || !groupPlanToChangeToPrice ||
inflight inflight
} }
onClick={upgrade} 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}
> >
{!inflight ? t('upgrade_now') : t('processing_uppercase') + '…'}
</button>
<hr className="thin" />
<button className="btn-inline-link" onClick={handleGetInTouchButton}>
{t('need_more_than_x_licenses', { {t('need_more_than_x_licenses', {
x: 50, x: 50,
})}{' '} })}{' '}
{t('please_get_in_touch')} {t('please_get_in_touch')}
</button> </OLButton>
</div> </div>
</Modal.Footer> </OLModalFooter>
</AccessibleModal> </OLModal>
</>
) )
} }

View file

@ -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> <OLModalHeader>
<Modal.Title>{t('change_plan')}</Modal.Title> <OLModalTitle>{t('change_plan')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
{error && ( {error && (
<div className="alert alert-danger" aria-live="polite"> <OLNotification
type="error"
aria-live="polite"
content={
<>
{t('generic_something_went_wrong')}. {t('try_again')}.{' '} {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
{t('generic_if_problem_continues_contact_us')}. {t('generic_if_problem_continues_contact_us')}.
</div> </>
}
/>
)} )}
<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> <OLModalFooter>
<button <OLButton
variant="secondary"
disabled={inflight} disabled={inflight}
className="btn btn-secondary"
onClick={handleCloseModal} onClick={handleCloseModal}
> >
{t('cancel')} {t('cancel')}
</button> </OLButton>
<button <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') + '…'} {t('change_plan')}
</button> </OLButton>
</Modal.Footer> </OLModalFooter>
</AccessibleModal> </OLModal>
) )
} }

View file

@ -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> <OLModalHeader>
<Modal.Title>{t('change_plan')}</Modal.Title> <OLModalTitle>{t('change_plan')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
{error && ( {error && (
<div className="alert alert-danger" aria-live="polite"> <OLNotification
type="error"
aria-live="polite"
content={
<>
{t('generic_something_went_wrong')}. {t('try_again')}.{' '} {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
{t('generic_if_problem_continues_contact_us')}. {t('generic_if_problem_continues_contact_us')}.
</div> </>
}
/>
)} )}
<p> <p>
<Trans <Trans
@ -65,26 +77,30 @@ export function KeepCurrentPlanModal() {
]} ]}
/> />
</p> </p>
</Modal.Body> </OLModalBody>
<Modal.Footer> <OLModalFooter>
<button <OLButton
variant="secondary"
disabled={inflight} disabled={inflight}
className="btn btn-secondary"
onClick={handleCloseModal} onClick={handleCloseModal}
> >
{t('cancel')} {t('cancel')}
</button> </OLButton>
<button <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('revert_pending_plan_change') </OLButton>
: t('processing_uppercase') + '…'} </OLModalFooter>
</button> </OLModal>
</Modal.Footer>
</AccessibleModal>
) )
} }

View file

@ -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,18 +27,22 @@ function SubscriptionDashboard() {
return ( return (
<div className="container"> <div className="container">
<div className="row"> <OLRow>
<div className="col-md-8 col-md-offset-2"> <OLCol lg={{ span: 8, offset: 2 }}>
{fromPlansPage && ( {fromPlansPage && (
<div className="alert alert-warning" aria-live="polite"> <OLNotification
{t('you_already_have_a_subscription')} className="mb-4"
</div> 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>
<div>
<PersonalSubscription /> <PersonalSubscription />
<ManagedGroupSubscriptions /> <ManagedGroupSubscriptions />
<ManagedInstitutions /> <ManagedInstitutions />
@ -45,8 +53,9 @@ function SubscriptionDashboard() {
{!hasDisplayedSubscription && {!hasDisplayedSubscription &&
(hasSubscription ? <ContactSupport /> : <FreePlan />)} (hasSubscription ? <ContactSupport /> : <FreePlan />)}
</div> </div>
</div> </OLCard>
</div> </OLCol>
</OLRow>
</div> </div>
) )
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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