first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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