first commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
|
||||
import UpgradeSubscription from '@/features/group-management/components/upgrade-subscription/upgrade-subscription'
|
||||
|
||||
function Root() {
|
||||
const { isReady } = useWaitForI18n()
|
||||
|
||||
if (!isReady) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <UpgradeSubscription />
|
||||
}
|
||||
|
||||
export default Root
|
@@ -0,0 +1,75 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, Row, Col } from 'react-bootstrap-5'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { formatCurrency } from '@/shared/utils/currency'
|
||||
|
||||
export const LICENSE_ADD_ON = 'additional-license'
|
||||
|
||||
function UpgradeSubscriptionPlanDetails() {
|
||||
const { t } = useTranslation()
|
||||
const preview = getMeta('ol-subscriptionChangePreview')
|
||||
const totalLicenses = getMeta('ol-totalLicenses')
|
||||
|
||||
const licenseUnitPrice = useMemo(() => {
|
||||
const additionalLicenseAddOn = preview.nextInvoice.addOns.filter(
|
||||
addOn => addOn.code === LICENSE_ADD_ON
|
||||
)
|
||||
// Legacy plans might not have additional-license add-on.
|
||||
// Hence we need to compute unit price
|
||||
return additionalLicenseAddOn.length > 0
|
||||
? additionalLicenseAddOn[0].unitAmount
|
||||
: preview.nextInvoice.plan.amount / totalLicenses
|
||||
}, [
|
||||
preview.nextInvoice.addOns,
|
||||
preview.nextInvoice.plan.amount,
|
||||
totalLicenses,
|
||||
])
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="group-subscription-upgrade-features card-description-secondary border-1"
|
||||
border="light"
|
||||
>
|
||||
<Card.Body className="d-grid gap-3 p-3">
|
||||
<b>{preview.nextInvoice.plan.name}</b>
|
||||
<Row xs="auto" className="gx-2">
|
||||
<Col>
|
||||
<span className="per-user-price" data-testid="per-user-price">
|
||||
<b>
|
||||
{formatCurrency(
|
||||
licenseUnitPrice,
|
||||
preview.currency,
|
||||
getMeta('ol-i18n')?.currentLangCode ?? 'en',
|
||||
true
|
||||
)}
|
||||
</b>
|
||||
</span>
|
||||
</Col>
|
||||
<Col className="d-flex flex-column justify-content-center">
|
||||
<div className="per-user-price-text">{t('per_license')}</div>
|
||||
<div className="per-user-price-text">{t('billed_yearly')}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="feature-list-item">
|
||||
<b>{t('all_features_in_group_standard_plus')}</b>
|
||||
</div>
|
||||
<div className="ps-2 feature-list-item">
|
||||
<MaterialIcon type="check" className="me-1" />
|
||||
{t('unlimited_collaborators_per_project')}
|
||||
</div>
|
||||
<div className="ps-2 feature-list-item">
|
||||
<MaterialIcon type="check" className="me-1" />
|
||||
{t('sso')}
|
||||
</div>
|
||||
<div className="ps-2 feature-list-item">
|
||||
<MaterialIcon type="check" className="me-1" />
|
||||
{t('managed_user_accounts')}
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradeSubscriptionPlanDetails
|
@@ -0,0 +1,114 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, ListGroup } from 'react-bootstrap-5'
|
||||
import { formatCurrency } from '@/shared/utils/currency'
|
||||
import { formatTime } from '@/features/utils/format-date'
|
||||
import {
|
||||
GroupPlanUpgrade,
|
||||
SubscriptionChangePreview,
|
||||
} from '../../../../../../types/subscription/subscription-change-preview'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export type SubscriptionChange = MergeAndOverride<
|
||||
SubscriptionChangePreview,
|
||||
{ change: GroupPlanUpgrade }
|
||||
>
|
||||
|
||||
type UpgradeSummaryProps = {
|
||||
subscriptionChange: SubscriptionChange
|
||||
}
|
||||
|
||||
function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) {
|
||||
const { t } = useTranslation()
|
||||
const totalLicenses = getMeta('ol-totalLicenses')
|
||||
|
||||
return (
|
||||
<Card className="card-gray card-description-secondary">
|
||||
<Card.Body className="d-grid gap-2 p-3">
|
||||
<div>
|
||||
<div className="fw-bold">{t('upgrade_summary')}</div>
|
||||
{t('you_have_x_licenses_on_your_subscription', {
|
||||
groupSize: totalLicenses,
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<ListGroup>
|
||||
<ListGroup.Item className="bg-transparent border-0 px-0 gap-3 card-description-secondary">
|
||||
<span className="me-auto">
|
||||
{subscriptionChange.nextInvoice.plan.name} x {totalLicenses}{' '}
|
||||
{t('licenses')}
|
||||
</span>
|
||||
<span data-testid="subtotal">
|
||||
{formatCurrency(
|
||||
subscriptionChange.immediateCharge.subtotal,
|
||||
subscriptionChange.currency
|
||||
)}
|
||||
</span>
|
||||
</ListGroup.Item>
|
||||
{subscriptionChange.immediateCharge.discount !== 0 && (
|
||||
<ListGroup.Item className="bg-transparent border-0 px-0 gap-3 card-description-secondary">
|
||||
<span className="me-auto">{t('discount')}</span>
|
||||
<span data-testid="discount">
|
||||
(
|
||||
{formatCurrency(
|
||||
subscriptionChange.immediateCharge.discount,
|
||||
subscriptionChange.currency
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
</ListGroup.Item>
|
||||
)}
|
||||
<ListGroup.Item className="bg-transparent border-0 px-0 gap-3 card-description-secondary">
|
||||
<span className="me-auto">{t('vat')}</span>
|
||||
<span data-testid="tax">
|
||||
{formatCurrency(
|
||||
subscriptionChange.immediateCharge.tax,
|
||||
subscriptionChange.currency
|
||||
)}
|
||||
</span>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="bg-transparent border-0 px-0 gap-3 card-description-secondary">
|
||||
<strong className="me-auto">{t('total_due_today')}</strong>
|
||||
<strong data-testid="total">
|
||||
{formatCurrency(
|
||||
subscriptionChange.immediateCharge.total,
|
||||
subscriptionChange.currency
|
||||
)}
|
||||
</strong>
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'we_will_charge_you_now_for_your_new_plan_based_on_the_remaining_months_of_your_current_subscription'
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'after_that_well_bill_you_x_total_y_subtotal_z_tax_annually_on_date_unless_you_cancel',
|
||||
{
|
||||
totalAmount: formatCurrency(
|
||||
subscriptionChange.nextInvoice.total,
|
||||
subscriptionChange.currency
|
||||
),
|
||||
subtotalAmount: formatCurrency(
|
||||
subscriptionChange.nextInvoice.subtotal,
|
||||
subscriptionChange.currency
|
||||
),
|
||||
taxAmount: formatCurrency(
|
||||
subscriptionChange.nextInvoice.tax.amount,
|
||||
subscriptionChange.currency
|
||||
),
|
||||
date: formatTime(subscriptionChange.nextInvoice.date, 'MMMM D'),
|
||||
}
|
||||
)}
|
||||
{subscriptionChange.immediateCharge.discount !== 0 &&
|
||||
` ${t('coupons_not_included')}.`}
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradeSummary
|
@@ -0,0 +1,149 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { Card, Row, Col } from 'react-bootstrap-5'
|
||||
import IconButton from '@/features/ui/components/bootstrap-5/icon-button'
|
||||
import Button from '@/features/ui/components/bootstrap-5/button'
|
||||
import UpgradeSubscriptionPlanDetails from './upgrade-subscription-plan-details'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import RequestStatus from '../request-status'
|
||||
import UpgradeSummary, {
|
||||
SubscriptionChange,
|
||||
} from './upgrade-subscription-upgrade-summary'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { sendMB } from '../../../../infrastructure/event-tracking'
|
||||
|
||||
function UpgradeSubscription() {
|
||||
const { t } = useTranslation()
|
||||
const groupName = getMeta('ol-groupName')
|
||||
const preview = getMeta('ol-subscriptionChangePreview') as SubscriptionChange
|
||||
const { isError, runAsync, isSuccess, isLoading } = useAsync()
|
||||
const onSubmit = () => {
|
||||
sendMB('flex-upgrade-form', {
|
||||
action: 'click-upgrade-button',
|
||||
})
|
||||
runAsync(postJSON('/user/subscription/group/upgrade-subscription'))
|
||||
.then(() => {
|
||||
sendMB('flex-upgrade-success')
|
||||
})
|
||||
.catch(() => {
|
||||
debugConsole.error()
|
||||
sendMB('flex-upgrade-error')
|
||||
})
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<RequestStatus
|
||||
variant="primary"
|
||||
icon="check_circle"
|
||||
title={t('youve_upgraded_your_plan')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<RequestStatus
|
||||
variant="danger"
|
||||
icon="error"
|
||||
title={t('something_went_wrong')}
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="it_looks_like_that_didnt_work_you_can_try_again_or_get_in_touch"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a
|
||||
href="/contact"
|
||||
onClick={() => {
|
||||
sendMB('flex-upgrade-form', {
|
||||
action: 'click-get-in-touch-link',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Row>
|
||||
<Col xl={{ span: 8, offset: 2 }}>
|
||||
<div className="group-heading" data-testid="group-heading">
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
href="/user/subscription"
|
||||
size="lg"
|
||||
icon="arrow_back"
|
||||
accessibilityLabel={t('back_to_subscription')}
|
||||
/>
|
||||
<h2>{groupName || t('group_subscription')}</h2>
|
||||
</div>
|
||||
<Card className="card-description-secondary group-subscription-upgrade-card">
|
||||
<Card.Body className="d-grid gap-2">
|
||||
<b className="title">{t('upgrade_your_subscription')}</b>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="group_plan_upgrade_description"
|
||||
values={{
|
||||
currentPlan: preview.change.prevPlan.name,
|
||||
nextPlan: preview.nextInvoice.plan.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
<a href="/contact" />,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
<Row className="mb-2">
|
||||
<Col md={{ span: 6 }} className="mb-2">
|
||||
<UpgradeSubscriptionPlanDetails />
|
||||
</Col>
|
||||
<Col md={{ span: 6 }}>
|
||||
<UpgradeSummary subscriptionChange={preview} />
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="d-flex align-items-center justify-content-end gap-2">
|
||||
<a
|
||||
href="/user/subscription/group/add-users"
|
||||
className="me-auto"
|
||||
onClick={() => sendMB('flex-add-users')}
|
||||
>
|
||||
{t('add_more_licenses_to_my_plan')}
|
||||
</a>
|
||||
<Button
|
||||
href="/user/subscription"
|
||||
variant="secondary"
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
sendMB('flex-upgrade-form', {
|
||||
action: 'click-cancel-button',
|
||||
})
|
||||
}}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSubmit}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t('upgrade')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradeSubscription
|
Reference in New Issue
Block a user