mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[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:
parent
d4bf47932e
commit
9997c4874f
36 changed files with 763 additions and 492 deletions
|
@ -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)
|
||||||
|
|
|
@ -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') + '…'}</>
|
|
||||||
}
|
|
|
@ -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>
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -150,3 +150,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-gray {
|
||||||
|
background-color: var(--neutral-10);
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -48,7 +48,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item-with-margin-bottom {
|
|
||||||
margin-bottom: @line-height-computed;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue