mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-25 02:00:10 +02:00
Merge pull request #22184 from overleaf/ls-group-plan-upgrade-page
Group plan upgrade page GitOrigin-RevId: 6c99173c013d84943276dbd43f468026c4d44558
This commit is contained in:
@@ -216,6 +216,30 @@ class RecurlySubscription {
|
||||
addOnUpdates,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade group plan with the plan code provided
|
||||
*
|
||||
* @param {string} newPlanCode
|
||||
* @return {RecurlySubscriptionChangeRequest}
|
||||
*/
|
||||
getRequestForFlexibleLicensingGroupPlanUpgrade(newPlanCode) {
|
||||
// Ensure all the existing add-ons are added to the new plan
|
||||
const existingAddOns = this.addOns.map(
|
||||
addOn =>
|
||||
new RecurlySubscriptionAddOnUpdate({
|
||||
code: addOn.code,
|
||||
quantity: addOn.quantity,
|
||||
})
|
||||
)
|
||||
|
||||
return new RecurlySubscriptionChangeRequest({
|
||||
subscription: this,
|
||||
timeframe: 'now',
|
||||
addOnUpdates: existingAddOns,
|
||||
planCode: newPlanCode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,9 +12,10 @@ import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
|
||||
import ErrorController from '../Errors/ErrorController.js'
|
||||
import SalesContactFormController from '../../../../modules/cms/app/src/controllers/SalesContactFormController.mjs'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import { Subscription } from '../../models/Subscription.js'
|
||||
|
||||
/**
|
||||
* @import { Subscription } from "../../../../types/subscription/dashboard/subscription"
|
||||
* @import { Subscription } from "../../../../types/subscription/dashboard/subscription.js"
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -220,6 +221,36 @@ async function flexibleLicensingSplitTest(req, res, next) {
|
||||
next()
|
||||
}
|
||||
|
||||
async function subscriptionUpgradePage(req, res) {
|
||||
try {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
const changePreview =
|
||||
await SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview(userId)
|
||||
const olSubscription = await Subscription.findOne({
|
||||
admin_id: userId,
|
||||
}).exec()
|
||||
res.render('subscriptions/upgrade-group-subscription-react', {
|
||||
changePreview,
|
||||
totalLicenses: olSubscription.membersLimit,
|
||||
groupName: olSubscription.teamName,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.err({ error }, 'error loading upgrade subscription page')
|
||||
return res.render('/user/subscription')
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeSubscription(req, res) {
|
||||
try {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
await SubscriptionGroupHandler.promises.upgradeGroupPlan(userId)
|
||||
return res.sendStatus(200)
|
||||
} catch (error) {
|
||||
logger.err({ error }, 'error trying to upgrade subscription')
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
removeUserFromGroup: expressify(removeUserFromGroup),
|
||||
removeSelfFromGroup: expressify(removeSelfFromGroup),
|
||||
@@ -232,4 +263,6 @@ export default {
|
||||
createAddSeatsSubscriptionChange: expressify(
|
||||
createAddSeatsSubscriptionChange
|
||||
),
|
||||
subscriptionUpgradePage: expressify(subscriptionUpgradePage),
|
||||
upgradeSubscription: expressify(upgradeSubscription),
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ const SessionManager = require('../Authentication/SessionManager')
|
||||
const RecurlyClient = require('./RecurlyClient')
|
||||
const PlansLocator = require('./PlansLocator')
|
||||
|
||||
const PLAN_UPGRADE_MAP = {
|
||||
group_collaborator: 'group_professional',
|
||||
group_collaborator_educational: 'group_professional_educational',
|
||||
}
|
||||
|
||||
async function removeUserFromGroup(subscriptionId, userIdToRemove) {
|
||||
await SubscriptionUpdater.promises.removeUserFromGroup(
|
||||
subscriptionId,
|
||||
@@ -140,12 +145,58 @@ async function createAddSeatsSubscriptionChange(req) {
|
||||
return { adding: req.body.adding }
|
||||
}
|
||||
|
||||
async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
|
||||
if (!Object.keys(PLAN_UPGRADE_MAP).includes(subscription.planCode)) {
|
||||
throw new Error('Not eligible for group plan upgrade')
|
||||
}
|
||||
|
||||
return PLAN_UPGRADE_MAP[subscription.planCode]
|
||||
}
|
||||
|
||||
async function _getGroupPlanUpgradeChangeRequest(ownerId) {
|
||||
const olSubscription =
|
||||
await SubscriptionLocator.promises.getUsersSubscription(ownerId)
|
||||
|
||||
const newPlanCode = await _getUpgradeTargetPlanCodeMaybeThrow(olSubscription)
|
||||
|
||||
const recurlySubscription = await RecurlyClient.promises.getSubscription(
|
||||
olSubscription.recurlySubscription_id
|
||||
)
|
||||
return recurlySubscription.getRequestForFlexibleLicensingGroupPlanUpgrade(
|
||||
newPlanCode
|
||||
)
|
||||
}
|
||||
|
||||
async function getGroupPlanUpgradePreview(ownerId) {
|
||||
const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
|
||||
const subscriptionChange =
|
||||
await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
|
||||
const paymentMethod = await RecurlyClient.promises.getPaymentMethod(ownerId)
|
||||
return SubscriptionController.makeChangePreview(
|
||||
{
|
||||
type: 'group-plan-upgrade',
|
||||
prevPlan: {
|
||||
name: subscriptionChange.subscription.planName,
|
||||
},
|
||||
},
|
||||
subscriptionChange,
|
||||
paymentMethod
|
||||
)
|
||||
}
|
||||
|
||||
async function upgradeGroupPlan(ownerId) {
|
||||
const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
|
||||
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
removeUserFromGroup: callbackify(removeUserFromGroup),
|
||||
replaceUserReferencesInGroups: callbackify(replaceUserReferencesInGroups),
|
||||
ensureFlexibleLicensingEnabled: callbackify(ensureFlexibleLicensingEnabled),
|
||||
getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
|
||||
isUserPartOfGroup: callbackify(isUserPartOfGroup),
|
||||
getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
|
||||
upgradeGroupPlan: callbackify(upgradeGroupPlan),
|
||||
promises: {
|
||||
removeUserFromGroup,
|
||||
replaceUserReferencesInGroups,
|
||||
@@ -155,5 +206,7 @@ module.exports = {
|
||||
getUsersGroupSubscriptionDetails,
|
||||
previewAddSeatsSubscriptionChange,
|
||||
createAddSeatsSubscriptionChange,
|
||||
getGroupPlanUpgradePreview,
|
||||
upgradeGroupPlan,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -108,6 +108,21 @@ export default {
|
||||
SubscriptionGroupController.submitForm
|
||||
)
|
||||
|
||||
webRouter.get(
|
||||
'/user/subscription/group/upgrade-subscription',
|
||||
AuthenticationController.requireLogin(),
|
||||
RateLimiterMiddleware.rateLimit(subscriptionRateLimiter),
|
||||
SubscriptionGroupController.flexibleLicensingSplitTest,
|
||||
SubscriptionGroupController.subscriptionUpgradePage
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/user/subscription/group/upgrade-subscription',
|
||||
AuthenticationController.requireLogin(),
|
||||
RateLimiterMiddleware.rateLimit(subscriptionRateLimiter),
|
||||
SubscriptionGroupController.upgradeSubscription
|
||||
)
|
||||
|
||||
// Team invites
|
||||
webRouter.get(
|
||||
'/subscription/invites/:token/',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
extends ../layout-marketing
|
||||
|
||||
block vars
|
||||
- bootstrap5PageStatus = 'enabled' // Enforce BS5 version
|
||||
|
||||
block entrypointVar
|
||||
- entrypoint = 'pages/user/subscription/group-management/upgrade-group-subscription'
|
||||
|
||||
block append meta
|
||||
meta(name="ol-subscriptionChangePreview" data-type="json" content=changePreview)
|
||||
meta(name="ol-totalLicenses", data-type="number", content=totalLicenses)
|
||||
meta(name="ol-groupName", data-type="string", content=groupName)
|
||||
|
||||
block content
|
||||
main.content.content-alt#upgrade-group-subscription-root
|
||||
@@ -75,6 +75,7 @@
|
||||
"add_more_managers": "",
|
||||
"add_more_members": "",
|
||||
"add_more_users": "",
|
||||
"add_more_users_to_my_plan": "",
|
||||
"add_new_email": "",
|
||||
"add_ons_are": "",
|
||||
"add_or_remove_project_from_tag": "",
|
||||
@@ -108,6 +109,7 @@
|
||||
"ai_feedback_there_was_no_code_fix_suggested": "",
|
||||
"alignment": "",
|
||||
"all_borders": "",
|
||||
"all_features_in_group_standard_plus": "",
|
||||
"all_premium_features": "",
|
||||
"all_premium_features_including": "",
|
||||
"all_projects": "",
|
||||
@@ -159,6 +161,7 @@
|
||||
"beta_program_benefits": "",
|
||||
"beta_program_not_participating": "",
|
||||
"better_bibliographies": "",
|
||||
"billed_yearly": "",
|
||||
"binary_history_error": "",
|
||||
"blank_project": "",
|
||||
"blocked_filename": "",
|
||||
@@ -619,6 +622,7 @@
|
||||
"group_libraries": "",
|
||||
"group_managed_by_group_administrator": "",
|
||||
"group_plan_tooltip": "",
|
||||
"group_plan_upgrade_description": "",
|
||||
"group_plan_with_name_tooltip": "",
|
||||
"group_sso_configuration_idp_metadata": "",
|
||||
"group_sso_configure_service_provider_in_idp": "",
|
||||
@@ -896,6 +900,7 @@
|
||||
"manage_sessions": "",
|
||||
"manage_subscription": "",
|
||||
"managed": "",
|
||||
"managed_user_accounts": "",
|
||||
"managed_user_invite_has_been_sent_to_email": "",
|
||||
"managed_users": "",
|
||||
"managed_users_explanation": "",
|
||||
@@ -1076,6 +1081,7 @@
|
||||
"pending_additional_licenses": "",
|
||||
"pending_addon_cancellation": "",
|
||||
"pending_invite": "",
|
||||
"per_user": "",
|
||||
"percent_discount_for_groups": "",
|
||||
"percent_is_the_percentage_of_the_line_width": "",
|
||||
"permanently_disables_the_preview": "",
|
||||
@@ -1723,6 +1729,7 @@
|
||||
"university": "",
|
||||
"university_school": "",
|
||||
"unknown": "",
|
||||
"unlimited_collaborators_per_project": "",
|
||||
"unlimited_collabs": "",
|
||||
"unlimited_projects": "",
|
||||
"unlink": "",
|
||||
@@ -1760,10 +1767,12 @@
|
||||
"upgrade_for_12x_more_compile_time": "",
|
||||
"upgrade_my_plan": "",
|
||||
"upgrade_now": "",
|
||||
"upgrade_summary": "",
|
||||
"upgrade_to_add_more_editors": "",
|
||||
"upgrade_to_add_more_editors_and_access_collaboration_features": "",
|
||||
"upgrade_to_get_feature": "",
|
||||
"upgrade_to_track_changes": "",
|
||||
"upgrade_your_subscription": "",
|
||||
"upload": "",
|
||||
"upload_from_computer": "",
|
||||
"upload_project": "",
|
||||
@@ -1784,6 +1793,7 @@
|
||||
"user_first_name_attribute": "",
|
||||
"user_last_name_attribute": "",
|
||||
"user_sessions": "",
|
||||
"users": "",
|
||||
"using_latex": "",
|
||||
"using_premium_features": "",
|
||||
"using_the_overleaf_editor": "",
|
||||
@@ -1830,6 +1840,7 @@
|
||||
"we_logged_you_in": "",
|
||||
"we_sent_new_code": "",
|
||||
"we_will_charge_you_now_for_the_cost_of_your_additional_users_based_on_remaining_months": "",
|
||||
"we_will_charge_you_now_for_your_new_plan_based_on_the_remaining_months_of_your_current_subscription": "",
|
||||
"webinars": "",
|
||||
"website_status": "",
|
||||
"wed_love_you_to_stay": "",
|
||||
@@ -1901,6 +1912,7 @@
|
||||
"you_have_been_invited_to_transfer_management_of_your_account_to": "",
|
||||
"you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "",
|
||||
"you_have_x_users_and_your_plan_supports_up_to_y": "",
|
||||
"you_have_x_users_on_your_subscription": "",
|
||||
"you_need_to_configure_your_sso_settings": "",
|
||||
"you_will_be_able_to_reassign_subscription": "",
|
||||
"youll_get_best_results_in_visual_but_can_be_used_in_source": "",
|
||||
@@ -1949,6 +1961,7 @@
|
||||
"youve_added_x_more_users_to_your_subscription_invite_people": "",
|
||||
"youve_lost_edit_access": "",
|
||||
"youve_unlinked_all_users": "",
|
||||
"youve_upgraded_your_plan": "",
|
||||
"zoom_in": "",
|
||||
"zoom_out": "",
|
||||
"zoom_to": "",
|
||||
|
||||
@@ -9,7 +9,7 @@ import classnames from 'classnames'
|
||||
type RequestStatusProps = {
|
||||
icon: string
|
||||
title: string
|
||||
content: React.ReactNode
|
||||
content?: React.ReactNode
|
||||
variant?: 'primary' | 'danger'
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ function RequestStatus({ icon, title, content, variant }: RequestStatusProps) {
|
||||
<h3 className="mb-0 fw-bold" data-testid="title">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="card-description-secondary">{content}</div>
|
||||
{content && (
|
||||
<div className="card-description-secondary">{content}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<Button variant="secondary" href="/user/subscription">
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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 { formatCurrencyLocalized } from '@/shared/utils/currency'
|
||||
|
||||
const LICENSE_ADD_ON = 'additional-license'
|
||||
|
||||
function UpgradeSubscriptionPlanDetails() {
|
||||
const { t } = useTranslation()
|
||||
const preview = getMeta('ol-subscriptionChangePreview')
|
||||
|
||||
const licenseUnitPrice = useMemo(
|
||||
() =>
|
||||
preview.nextInvoice.addOns.filter(
|
||||
addOn => addOn.code === LICENSE_ADD_ON
|
||||
)[0].unitAmount,
|
||||
[preview]
|
||||
)
|
||||
|
||||
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">
|
||||
<b>
|
||||
{formatCurrencyLocalized(
|
||||
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_user')}</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,88 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, ListGroup } from 'react-bootstrap-5'
|
||||
import { formatCurrencyLocalized } 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_users_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('users')}
|
||||
</span>
|
||||
<span>
|
||||
{formatCurrencyLocalized(
|
||||
subscriptionChange.immediateCharge.subtotal,
|
||||
subscriptionChange.currency
|
||||
)}
|
||||
</span>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="bg-transparent border-0 px-0 gap-3 card-description-secondary">
|
||||
<span className="me-auto">{t('sales_tax')}</span>
|
||||
<span>
|
||||
{formatCurrencyLocalized(
|
||||
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>
|
||||
{formatCurrencyLocalized(
|
||||
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_annually_on_date_unless_you_cancel', {
|
||||
subtotal: formatCurrencyLocalized(
|
||||
subscriptionChange.nextInvoice.subtotal,
|
||||
subscriptionChange.currency
|
||||
),
|
||||
date: formatTime(subscriptionChange.nextInvoice.date, 'MMMM D'),
|
||||
})}
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradeSummary
|
||||
@@ -0,0 +1,122 @@
|
||||
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'
|
||||
|
||||
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 = () => {
|
||||
runAsync(postJSON('/user/subscription/group/upgrade-subscription'))
|
||||
}
|
||||
|
||||
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"
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
components={[<a href="/contact" />]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Row>
|
||||
<Col xl={{ span: 8, offset: 2 }}>
|
||||
<div className="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"
|
||||
>
|
||||
{t('add_more_users_to_my_plan')}
|
||||
</a>
|
||||
<Button
|
||||
href="/user/subscription"
|
||||
variant="secondary"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSubmit}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t('upgrade')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradeSubscription
|
||||
@@ -0,0 +1,8 @@
|
||||
import '../base'
|
||||
import ReactDOM from 'react-dom'
|
||||
import UpgradeSubscription from '@/features/group-management/components/upgrade-subscription/upgrade-subscription'
|
||||
|
||||
const element = document.getElementById('upgrade-group-subscription-root')
|
||||
if (element) {
|
||||
ReactDOM.render(<UpgradeSubscription />, element)
|
||||
}
|
||||
@@ -382,3 +382,31 @@
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-subscription-upgrade-features {
|
||||
.material-symbols {
|
||||
@include body-sm;
|
||||
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.per-user-price {
|
||||
@include heading-lg;
|
||||
|
||||
color: var(--content-primary);
|
||||
}
|
||||
|
||||
.per-user-price-text {
|
||||
@include body-xs;
|
||||
}
|
||||
|
||||
.feature-list-item {
|
||||
@include body-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.group-subscription-upgrade-card {
|
||||
.title {
|
||||
@include body-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"add_more_managers": "Add more managers",
|
||||
"add_more_members": "Add more members",
|
||||
"add_more_users": "Add more users",
|
||||
"add_more_users_to_my_plan": "Add more users to my plan",
|
||||
"add_new_email": "Add new email",
|
||||
"add_ons_are": "<strong>Add-ons:</strong> __addOnName__",
|
||||
"add_or_remove_project_from_tag": "Add or remove project from tag __tagName__",
|
||||
@@ -128,6 +129,7 @@
|
||||
"alignment": "Alignment",
|
||||
"all": "All",
|
||||
"all_borders": "All borders",
|
||||
"all_features_in_group_standard_plus": "All features in Group Standard, plus:",
|
||||
"all_our_group_plans_offer_educational_discount": "All of our <0>group plans</0> offer an <1>educational discount</1> for students and faculty",
|
||||
"all_premium_features": "All premium features",
|
||||
"all_premium_features_including": "All premium features, including:",
|
||||
@@ -213,6 +215,7 @@
|
||||
"beta_program_opt_out_action": "Opt-Out of Beta Program",
|
||||
"better_bibliographies": "Better bibliographies",
|
||||
"bibliographies": "Bibliographies",
|
||||
"billed_yearly": "billed yearly",
|
||||
"billing_period_sentence_case": "Billing period",
|
||||
"binary_history_error": "Preview not available for this file type",
|
||||
"blank_project": "Blank Project",
|
||||
@@ -888,6 +891,7 @@
|
||||
"group_members_get_access_to_info": "These features are available only to group members (subscribers).",
|
||||
"group_plan_admins_can_easily_add_and_remove_users_from_a_group": "Group plan admins can easily add and remove users from a group. For site-wide plans, users are automatically upgraded when they register or add their email address to Overleaf (domain-based enrollment or SSO).",
|
||||
"group_plan_tooltip": "You are on the __plan__ plan as a member of a group subscription. Click to find out how to make the most of your Overleaf premium features.",
|
||||
"group_plan_upgrade_description": "You’re on the <0>__currentPlan__</0> plan and you’re upgrading to the <0>__nextPlan__</0> plan. If you’re interested in a site-wide Overleaf Commons plan please <1>get in touch</1>.",
|
||||
"group_plan_with_name_tooltip": "You are on the __plan__ plan as a member of a group subscription, __groupName__. Click to find out how to make the most of your Overleaf premium features.",
|
||||
"group_plans": "Group Plans",
|
||||
"group_professional": "Group Professional",
|
||||
@@ -2392,10 +2396,12 @@
|
||||
"upgrade_for_12x_more_compile_time": "Upgrade to get 12x more compile time",
|
||||
"upgrade_my_plan": "Upgrade my plan",
|
||||
"upgrade_now": "Upgrade now",
|
||||
"upgrade_summary": "Upgrade summary",
|
||||
"upgrade_to_add_more_editors": "Upgrade to add more editors to your project",
|
||||
"upgrade_to_add_more_editors_and_access_collaboration_features": "Upgrade to add more editors and access collaboration features like track changes and full project history.",
|
||||
"upgrade_to_get_feature": "Upgrade to get __feature__, plus:",
|
||||
"upgrade_to_track_changes": "Upgrade to track changes",
|
||||
"upgrade_your_subscription": "Upgrade your subscription",
|
||||
"upload": "Upload",
|
||||
"upload_failed": "Upload failed",
|
||||
"upload_from_computer": "Upload from computer",
|
||||
@@ -2427,6 +2433,7 @@
|
||||
"user_not_found": "User not found",
|
||||
"user_sessions": "User Sessions",
|
||||
"user_wants_you_to_see_project": "__username__ would like you to join __projectname__",
|
||||
"users": "users",
|
||||
"using_latex": "Using LaTeX",
|
||||
"using_premium_features": "Using premium features",
|
||||
"using_the_overleaf_editor": "Using the __appName__ Editor",
|
||||
@@ -2477,6 +2484,7 @@
|
||||
"we_may_also_contact_you_from_time_to_time_by_email_with_a_survey": "<0>We may also contact you</0> from time to time by email with a survey, or to see if you would like to participate in other user research initiatives",
|
||||
"we_sent_new_code": "We’ve sent a new code. If it doesn’t arrive, make sure to check your spam and any promotions folders.",
|
||||
"we_will_charge_you_now_for_the_cost_of_your_additional_users_based_on_remaining_months": "We’ll charge you now for the cost of your additional users based on the remaining months of your current subscription.",
|
||||
"we_will_charge_you_now_for_your_new_plan_based_on_the_remaining_months_of_your_current_subscription": "We’ll charge you now for your new plan based on the remaining months of your current subscription.",
|
||||
"webinars": "Webinars",
|
||||
"website_status": "Website status",
|
||||
"wed_love_you_to_stay": "We’d love you to stay",
|
||||
@@ -2565,6 +2573,7 @@
|
||||
"you_have_been_invited_to_transfer_management_of_your_account_to": "You have been invited to transfer management of your account to __groupName__.",
|
||||
"you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.",
|
||||
"you_have_x_users_and_your_plan_supports_up_to_y": "You have __addedUsersSize__ users and your plan supports up to __groupSize__.",
|
||||
"you_have_x_users_on_your_subscription": "You have __groupSize__ users on your subscription.",
|
||||
"you_need_to_configure_your_sso_settings": "You need to configure and test your SSO settings before enabling SSO",
|
||||
"you_plus_1": "You + 1",
|
||||
"you_plus_10": "You + 10",
|
||||
@@ -2622,6 +2631,7 @@
|
||||
"youve_added_x_more_users_to_your_subscription_invite_people": "You’ve added __users__ more users to your subscription. <0>Invite people</0>.",
|
||||
"youve_lost_edit_access": "You’ve lost edit access",
|
||||
"youve_unlinked_all_users": "You’ve unlinked all users",
|
||||
"youve_upgraded_your_plan": "You’ve upgraded your plan!",
|
||||
"zh-CN": "Chinese",
|
||||
"zip_contents_too_large": "Zip contents too large",
|
||||
"zoom_in": "Zoom in",
|
||||
|
||||
@@ -77,6 +77,12 @@ describe('SubscriptionGroupController', function () {
|
||||
},
|
||||
}
|
||||
|
||||
this.RecurlyClient = {}
|
||||
|
||||
this.SubscriptionController = {}
|
||||
|
||||
this.SubscriptionModel = { Subscription: {} }
|
||||
|
||||
this.Controller = await esmock.strict(modulePath, {
|
||||
'../../../../app/src/Features/Subscription/SubscriptionGroupHandler':
|
||||
this.SubscriptionGroupHandler,
|
||||
@@ -94,6 +100,11 @@ describe('SubscriptionGroupController', function () {
|
||||
(this.ErrorController = {
|
||||
notFound: sinon.stub(),
|
||||
}),
|
||||
'../../../../app/src/Features/Subscription/SubscriptionController':
|
||||
this.SubscriptionController,
|
||||
'../../../../app/src/Features/Subscription/RecurlyClient':
|
||||
this.RecurlyClient,
|
||||
'../../../../app/src/models/Subscription': this.SubscriptionModel,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export type SubscriptionChangeDescription =
|
||||
| AddOnPurchase
|
||||
| AddOnUpdate
|
||||
| PremiumSubscription
|
||||
| GroupPlanUpgrade
|
||||
|
||||
export type AddOnPurchase = {
|
||||
type: 'add-on-purchase'
|
||||
@@ -51,6 +52,13 @@ export type AddOnUpdate = {
|
||||
}
|
||||
}
|
||||
|
||||
export type GroupPlanUpgrade = {
|
||||
type: 'group-plan-upgrade'
|
||||
prevPlan: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
type PremiumSubscription = {
|
||||
type: 'premium-subscription'
|
||||
plan: {
|
||||
|
||||
Reference in New Issue
Block a user