Merge pull request #22184 from overleaf/ls-group-plan-upgrade-page

Group plan upgrade page

GitOrigin-RevId: 6c99173c013d84943276dbd43f468026c4d44558
This commit is contained in:
Liangjun Song
2024-12-05 13:32:29 +01:00
committed by Copybot
parent a3b4c47a6f
commit 3351ac3dc8
15 changed files with 500 additions and 3 deletions

View File

@@ -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,
})
}
}
/**

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": "",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": "Youre on the <0>__currentPlan__</0> plan and youre upgrading to the <0>__nextPlan__</0> plan. If youre 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": "Weve sent a new code. If it doesnt 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": "Well 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": "Well 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": "Wed 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": "Youve added __users__ more users to your subscription. <0>Invite people</0>.",
"youve_lost_edit_access": "Youve lost edit access",
"youve_unlinked_all_users": "Youve unlinked all users",
"youve_upgraded_your_plan": "Youve upgraded your plan!",
"zh-CN": "Chinese",
"zip_contents_too_large": "Zip contents too large",
"zoom_in": "Zoom in",

View File

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

View File

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