diff --git a/services/web/app/src/Features/Subscription/SubscriptionFormatters.js b/services/web/app/src/Features/Subscription/SubscriptionFormatters.js
index 0a82c35a84..91f5d127f3 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionFormatters.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionFormatters.js
@@ -68,15 +68,23 @@ function formatPriceLocalized(priceInCents, currency = 'USD', locale) {
return formatCurrencyLocalized(priceInCurrencyUnit, currency, locale)
}
-function formatDate(date) {
+function formatDateTime(date) {
if (!date) {
return null
}
return dateformat(date, 'mmmm dS, yyyy h:MM TT Z', true)
}
+function formatDate(date) {
+ if (!date) {
+ return null
+ }
+ return dateformat(date, 'mmmm dS, yyyy', true)
+}
+
module.exports = {
formatPriceDefault,
formatPriceLocalized,
+ formatDateTime,
formatDate,
}
diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
index 2bd3722a27..30ae83b675 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
@@ -250,17 +250,13 @@ async function buildUsersSubscriptionViewModel(
// Note: tax_in_cents already includes the tax for any addon.
let addOnPrice = 0
let additionalLicenses = 0
- if (
- plan.membersLimitAddOn &&
- Array.isArray(recurlySubscription.subscription_add_ons)
- ) {
- recurlySubscription.subscription_add_ons.forEach(addOn => {
- if (addOn.add_on_code === plan.membersLimitAddOn) {
- addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
- additionalLicenses += addOn.quantity
- }
- })
- }
+ const addOns = recurlySubscription.subscription_add_ons || []
+ addOns.forEach(addOn => {
+ addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
+ if (addOn.add_on_code === plan.membersLimitAddOn) {
+ additionalLicenses += addOn.quantity
+ }
+ })
const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
personalSubscription.recurly = {
tax,
@@ -270,14 +266,17 @@ async function buildUsersSubscriptionViewModel(
billingDetailsLink: buildHostedLink('billing-details'),
accountManagementLink: buildHostedLink('account-management'),
additionalLicenses,
- addOns: recurlySubscription.subscription_add_ons || [],
+ addOns,
totalLicenses,
- nextPaymentDueAt: SubscriptionFormatters.formatDate(
+ nextPaymentDueAt: SubscriptionFormatters.formatDateTime(
+ recurlySubscription.current_period_ends_at
+ ),
+ nextPaymentDueDate: SubscriptionFormatters.formatDate(
recurlySubscription.current_period_ends_at
),
currency: recurlySubscription.currency,
state: recurlySubscription.state,
- trialEndsAtFormatted: SubscriptionFormatters.formatDate(
+ trialEndsAtFormatted: SubscriptionFormatters.formatDateTime(
recurlySubscription.trial_ends_at
),
trial_ends_at: recurlySubscription.trial_ends_at,
@@ -297,24 +296,18 @@ async function buildUsersSubscriptionViewModel(
let pendingAddOnTax = 0
let pendingAddOnPrice = 0
if (recurlySubscription.pending_subscription.subscription_add_ons) {
- if (
- pendingPlan.membersLimitAddOn &&
- Array.isArray(
- recurlySubscription.pending_subscription.subscription_add_ons
- )
- ) {
- recurlySubscription.pending_subscription.subscription_add_ons.forEach(
- addOn => {
- if (addOn.add_on_code === pendingPlan.membersLimitAddOn) {
- pendingAddOnPrice += addOn.quantity * addOn.unit_amount_in_cents
- pendingAdditionalLicenses += addOn.quantity
- }
- }
- )
- }
+ const pendingRecurlyAddons =
+ recurlySubscription.pending_subscription.subscription_add_ons
+ pendingRecurlyAddons.forEach(addOn => {
+ pendingAddOnPrice += addOn.quantity * addOn.unit_amount_in_cents
+ if (addOn.add_on_code === pendingPlan.membersLimitAddOn) {
+ pendingAdditionalLicenses += addOn.quantity
+ }
+ })
// Need to calculate tax ourselves as we don't get tax amounts for pending subs
pendingAddOnTax =
personalSubscription.recurly.taxRate * pendingAddOnPrice
+ pendingPlan.addOns = pendingRecurlyAddons
}
const pendingSubscriptionTax =
personalSubscription.recurly.taxRate *
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 7fa4b1aaaa..12c8acaa37 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -66,6 +66,7 @@
"add_more_managers": "",
"add_more_members": "",
"add_new_email": "",
+ "add_ons_are": "",
"add_or_remove_project_from_tag": "",
"add_people": "",
"add_role_and_department": "",
@@ -120,6 +121,7 @@
"are_you_getting_an_undefined_control_sequence_error": "",
"are_you_still_at": "",
"are_you_sure": "",
+ "are_you_sure_you_want_to_cancel_add_on": "",
"as_email": "",
"ask_proj_owner_to_unlink_from_current_github": "",
"ask_proj_owner_to_upgrade_for_full_history": "",
@@ -162,6 +164,7 @@
"can_now_relink_dropbox": "",
"can_view": "",
"cancel": "",
+ "cancel_add_on": "",
"cancel_anytime": "",
"cancel_my_account": "",
"cancel_my_subscription": "",
@@ -1037,6 +1040,7 @@
"pdf_viewer": "",
"pdf_viewer_error": "",
"pending_additional_licenses": "",
+ "pending_addon_cancellation": "",
"pending_invite": "",
"percent_discount_for_groups": "",
"percent_is_the_percentage_of_the_line_width": "",
@@ -1187,6 +1191,7 @@
"remote_service_error": "",
"remove": "",
"remove_access": "",
+ "remove_add_on": "",
"remove_collaborator": "",
"remove_from_group": "",
"remove_link": "",
@@ -1470,6 +1475,7 @@
"sure_you_want_to_change_plan": "",
"sure_you_want_to_delete": "",
"sure_you_want_to_leave_group": "",
+ "switch_plan": "",
"switch_to_editor": "",
"switch_to_pdf": "",
"symbol_palette": "",
@@ -1511,6 +1517,7 @@
"thanks_for_subscribing": "",
"thanks_for_subscribing_you_help_sl": "",
"thanks_settings_updated": "",
+ "the_add_on_will_remain_active_until": "",
"the_following_files_already_exist_in_this_project": "",
"the_following_files_and_folders_already_exist_in_this_project": "",
"the_following_folder_already_exists_in_this_project": "",
@@ -1694,6 +1701,7 @@
"untrash": "",
"update": "",
"update_account_info": "",
+ "update_billing_details": "",
"update_dropbox_settings": "",
"update_your_billing_details": "",
"updates_to_project_sharing": "",
@@ -1743,6 +1751,7 @@
"view_hub_subtext": "",
"view_in_template_gallery": "",
"view_invitation": "",
+ "view_invoices": "",
"view_labs_experiments": "",
"view_less": "",
"view_logs": "",
@@ -1856,6 +1865,7 @@
"your_new_plan": "",
"your_password_was_detected": "",
"your_plan": "",
+ "your_plan_is": "",
"your_plan_is_changing_at_term_end": "",
"your_plan_is_limited_to_n_editors": "",
"your_plan_is_limited_to_n_editors_plural": "",
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/free-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/free-plan.tsx
index dec397f3a1..bb670e19a5 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/free-plan.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/free-plan.tsx
@@ -1,5 +1,6 @@
import { useTranslation, Trans } from 'react-i18next'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
+import { AI_STANDALONE_PLAN_CODE } from '../../data/add-on-codes'
function FreePlan() {
const { t } = useTranslation()
@@ -30,7 +31,7 @@ function FreePlan() {
{isSplitTestEnabled('ai-add-on') && (
{t('buy_overleaf_assist')}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
index 2116a1c25f..90ed5f6ab2 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
@@ -1,11 +1,17 @@
import { Trans, useTranslation } from 'react-i18next'
import { RecurlySubscription } from '../../../../../../types/subscription/dashboard/subscription'
import { ActiveSubscription } from './states/active/active'
+import { ActiveAiAddonSubscription } from './states/active/active-ai-addon'
import { CanceledSubscription } from './states/canceled'
import { ExpiredSubscription } from './states/expired'
import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
import PersonalSubscriptionRecurlySyncEmail from './personal-subscription-recurly-sync-email'
import OLNotification from '@/features/ui/components/ol/ol-notification'
+import {
+ AI_STANDALONE_PLAN_CODE,
+ AI_STANDALONE_ANNUAL_PLAN_CODE,
+ AI_ADD_ON_CODE,
+} from '../../data/add-on-codes'
function PastDueSubscriptionAlert({
subscription,
@@ -40,7 +46,20 @@ function PersonalSubscriptionStates({
const { t } = useTranslation()
const state = subscription?.recurly.state
- if (state === 'active') {
+ const hasAiAddon = subscription?.addOns?.some(
+ addOn => addOn.addOnCode === AI_ADD_ON_CODE
+ )
+
+ const onAiStandalonePlan = [
+ AI_STANDALONE_PLAN_CODE,
+ AI_STANDALONE_ANNUAL_PLAN_CODE,
+ ].includes(subscription.planCode)
+
+ const planHasAi = onAiStandalonePlan || hasAiAddon
+
+ if (state === 'active' && planHasAi) {
+ return
+ } else if (state === 'active') {
return
} else if (state === 'canceled') {
return
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-ai-addon.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-ai-addon.tsx
new file mode 100644
index 0000000000..ec2728b25b
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-ai-addon.tsx
@@ -0,0 +1,188 @@
+import { useTranslation, Trans } from 'react-i18next'
+import { PriceExceptions } from '../../../shared/price-exceptions'
+import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
+import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+import { CancelSubscription } from './cancel-plan/cancel-subscription'
+import { PendingPlanChange } from './pending-plan-change'
+import SubscriptionRemainder from './subscription-remainder'
+import { ChangePlanModal } from './change-plan/modals/change-plan-modal'
+import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
+import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
+import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
+import { CancelAiAddOnModal } from './change-plan/modals/cancel-ai-add-on-modal'
+import {
+ AI_STANDALONE_PLAN_CODE,
+ ADD_ON_NAME,
+ AI_STANDALONE_PLAN_NAME,
+ AI_STANDALONE_ANNUAL_PLAN_CODE,
+} from '../../../../data/add-on-codes'
+import { CancelSubscriptionButton } from './cancel-subscription-button'
+
+import OLButton from '@/features/ui/components/ol/ol-button'
+
+export function ActiveAiAddonSubscription({
+ subscription,
+}: {
+ subscription: RecurlySubscription
+}) {
+ const { t } = useTranslation()
+ const { recurlyLoadError, showCancellation, setModalIdShown } =
+ useSubscriptionDashboardContext()
+ if (showCancellation) return
+
+ const onStandalonePlan = [
+ AI_STANDALONE_PLAN_CODE,
+ AI_STANDALONE_ANNUAL_PLAN_CODE,
+ ].includes(subscription.planCode)
+
+ const handlePlanChange = () => setModalIdShown('change-plan')
+
+ const handleCancelClick = () => setModalIdShown('cancel-ai-add-on')
+
+ return (
+ <>
+
+ }}
+ />
+
+
+ }}
+ />
+
+
+ {subscription.pendingPlan && (
+
+ )}
+
+ {subscription.pendingPlan &&
+ subscription.pendingPlan.name !== subscription.plan.name && (
+ {t('want_change_to_apply_before_plan_end')}
+ )}
+
+ ,
+ // eslint-disable-next-line react/jsx-key
+ ,
+ ]}
+ />
+
+
+ {!recurlyLoadError && (
+
+
+
+
+
+ )}
+ {!recurlyLoadError && (
+
+ {onStandalonePlan ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {t('view_invoices')}
+
+
+
+
+ {t('update_billing_details')}
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+function StandaloneAiPlanActions({
+ handlePlanChange,
+ handleCancelClick,
+}: {
+ handlePlanChange(): void
+ handleCancelClick(): void
+}) {
+ const { t } = useTranslation()
+ return (
+ <>
+
+ {t('upgrade')}
+
+
+
+ {t('cancel_add_on')}
+
+ >
+ )
+}
+
+function PlanWithAddonsActions({
+ handlePlanChange,
+ handleCancelClick,
+}: {
+ handlePlanChange(): void
+ handleCancelClick(): void
+}) {
+ const { t } = useTranslation()
+ return (
+ <>
+
+ {t('switch_plan')}
+
+
+ <>
+
+ {t('remove_add_on')}
+ {' '}
+
+ >
+ >
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx
index 81c21609df..98b9ea5dc1 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx
@@ -15,7 +15,7 @@ import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan
import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
-import { BuyAiAddOnButton } from './change-plan/modals/buy-ai-add-on-modal'
+import { BuyAiAddOnButton } from './change-plan/buy-ai-add-on-button'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
export function ActiveSubscription({
@@ -30,6 +30,7 @@ export function ActiveSubscription({
if (showCancellation) return
const aiAddOnAvailable = isSplitTestEnabled('ai-add-on')
+
return (
<>
@@ -148,6 +149,7 @@ export function ActiveSubscription({
+ {/* to be removed once we have purchasing options ready */}
{aiAddOnAvailable && }
>
)
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/buy-ai-add-on-button.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/buy-ai-add-on-button.tsx
new file mode 100644
index 0000000000..9be397135c
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/buy-ai-add-on-button.tsx
@@ -0,0 +1,12 @@
+import { Button } from 'react-bootstrap'
+import { postJSON } from '@/infrastructure/fetch-json'
+import { AI_ADD_ON_CODE } from '../../../../../data/add-on-codes'
+
+export function BuyAiAddOnButton() {
+ const addAiAddon = async () => {
+ await postJSON(`/user/subscription/addon/${AI_ADD_ON_CODE}/add`)
+ location.reload()
+ }
+
+ return
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/buy-ai-add-on-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/buy-ai-add-on-modal.tsx
deleted file mode 100644
index d324ac0261..0000000000
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/buy-ai-add-on-modal.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Button } from 'react-bootstrap'
-import { postJSON } from '@/infrastructure/fetch-json'
-import { useSubscriptionDashboardContext } from '@/features/subscription/context/subscription-dashboard-context'
-import { RecurlySubscription } from '../../../../../../../../../../types/subscription/dashboard/subscription'
-
-const AI_ADDON_CODE = 'assistant'
-
-export function BuyAiAddOnButton() {
- const { personalSubscription } = useSubscriptionDashboardContext()
- const recurlySub = personalSubscription as RecurlySubscription
-
- const hasSub = recurlySub?.recurly?.addOns?.some(
- addOn => addOn.add_on_code === AI_ADDON_CODE
- )
-
- const paths = {
- purchaseAddon: `/user/subscription/addon/${AI_ADDON_CODE}/add`,
- removeAddon: `/user/subscription/addon/${AI_ADDON_CODE}/remove`,
- }
-
- const addAiAddon = async () => {
- await postJSON(paths.purchaseAddon)
- location.reload()
- }
-
- // gotta change here to change the time to next billing cycle
- const removeAiAddon = async () => {
- await postJSON(paths.removeAddon)
- location.reload()
- }
-
- return hasSub ? (
-
- ) : (
-
- )
-}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal.tsx
new file mode 100644
index 0000000000..34a8ccae99
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal.tsx
@@ -0,0 +1,120 @@
+import { useState } from 'react'
+import { useTranslation, Trans } from 'react-i18next'
+import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
+import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
+import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
+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'
+import {
+ AI_ADD_ON_CODE,
+ AI_STANDALONE_PLAN_CODE,
+ ADD_ON_NAME,
+} from '../../../../../../data/add-on-codes'
+import {
+ cancelSubscriptionUrl,
+ redirectAfterCancelSubscriptionUrl,
+} from '../../../../../../data/subscription-url'
+
+export function CancelAiAddOnModal() {
+ const modalId: SubscriptionDashModalIds = 'cancel-ai-add-on'
+ const [error, setError] = useState(false)
+ const [inflight, setInflight] = useState(false)
+ const { t } = useTranslation()
+ const { handleCloseModal, modalIdShown, personalSubscription } =
+ useSubscriptionDashboardContext()
+ const location = useLocation()
+
+ if (!personalSubscription) return null
+
+ const onStandalone = personalSubscription.planCode === AI_STANDALONE_PLAN_CODE
+
+ const cancellationEndpoint = onStandalone
+ ? cancelSubscriptionUrl
+ : `/user/subscription/addon/${AI_ADD_ON_CODE}/remove`
+
+ async function handleConfirmChange() {
+ setError(false)
+ setInflight(true)
+
+ try {
+ await postJSON(cancellationEndpoint)
+ location.assign(redirectAfterCancelSubscriptionUrl)
+ } catch (e) {
+ setError(true)
+ setInflight(false)
+ }
+ }
+
+ if (modalIdShown !== modalId) return null
+
+ return (
+
+
+ {t('cancel_add_on')}
+
+
+
+ {error && (
+
+ {t('generic_something_went_wrong')}. {t('try_again')}.{' '}
+ {t('generic_if_problem_continues_contact_us')}.
+ >
+ }
+ />
+ )}
+
+ }}
+ />
+
+ {t('the_add_on_will_remain_active_until')}
+
+
+
+
+ {t('back')}
+
+
+ {t('cancel_add_on')}
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx
index 5d375c822a..94bbd3e241 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx
@@ -1,5 +1,7 @@
import { Trans } from 'react-i18next'
import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+import { PendingRecurlyPlan } from '../../../../../../../../types/subscription/plan'
+import { AI_ADD_ON_CODE, ADD_ON_NAME } from '../../../../data/add-on-codes'
export function PendingPlanChange({
subscription,
@@ -8,6 +10,21 @@ export function PendingPlanChange({
}) {
if (!subscription.pendingPlan) return null
+ const pendingPlan = subscription.pendingPlan as PendingRecurlyPlan
+
+ const hasAiAddon = subscription.addOns?.some(
+ addOn => addOn.addOnCode === AI_ADD_ON_CODE
+ )
+
+ const pendingAiAddonCancellation =
+ hasAiAddon &&
+ !pendingPlan.addOns?.some(addOn => addOn.add_on_code === AI_ADD_ON_CODE)
+
+ const pendingAdditionalLicenses =
+ (subscription.recurly.pendingAdditionalLicenses &&
+ subscription.recurly.pendingAdditionalLicenses > 0) ||
+ subscription.recurly.additionalLicenses > 0
+
return (
<>
{subscription.pendingPlan.name !== subscription.plan.name && (
@@ -25,9 +42,7 @@ export function PendingPlanChange({
/>
)}
- {((subscription.recurly.pendingAdditionalLicenses &&
- subscription.recurly.pendingAdditionalLicenses > 0) ||
- subscription.recurly.additionalLicenses > 0) && (
+ {pendingAdditionalLicenses && (
<>
{' '}
>
)}
+
+ {pendingAiAddonCancellation && (
+ <>
+ {' '}
+ }}
+ />
+ >
+ )}
>
)
}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/subscription-remainder.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/subscription-remainder.tsx
index cb343f5f60..16544e1507 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/subscription-remainder.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/subscription-remainder.tsx
@@ -3,19 +3,26 @@ import { RecurlySubscription } from '../../../../../../../../types/subscription/
type SubscriptionRemainderProps = {
subscription: RecurlySubscription
+ hideTime?: boolean
}
-function SubscriptionRemainder({ subscription }: SubscriptionRemainderProps) {
+function SubscriptionRemainder({
+ subscription,
+ hideTime,
+}: SubscriptionRemainderProps) {
const stillInATrial =
subscription.recurly.trialEndsAtFormatted &&
subscription.recurly.trial_ends_at &&
new Date(subscription.recurly.trial_ends_at).getTime() > Date.now()
+ const terminationDate = hideTime
+ ? subscription.recurly.nextPaymentDueDate
+ : subscription.recurly.nextPaymentDueAt
return stillInATrial ? (
{t('buy_overleaf_assist')}
diff --git a/services/web/frontend/js/features/subscription/data/add-on-codes.ts b/services/web/frontend/js/features/subscription/data/add-on-codes.ts
new file mode 100644
index 0000000000..cb32f1fa4c
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/data/add-on-codes.ts
@@ -0,0 +1,6 @@
+export const AI_STANDALONE_PLAN_CODE = 'assistant'
+export const AI_ADD_ON_CODE = 'assistant'
+// we dont want translations on plan or add-on names
+export const ADD_ON_NAME = "Error Assist"
+export const AI_STANDALONE_PLAN_NAME = "Overleaf Free"
+export const AI_STANDALONE_ANNUAL_PLAN_CODE = 'assistant-annual'
\ No newline at end of file
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 11b9055b4f..313cf32eff 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -78,6 +78,7 @@
"add_more_managers": "Add more managers",
"add_more_members": "Add more members",
"add_new_email": "Add new email",
+ "add_ons_are": "Add-ons: __addOnName__",
"add_or_remove_project_from_tag": "Add or remove project from tag __tagName__",
"add_people": "Add people",
"add_role_and_department": "Add role and department",
@@ -154,6 +155,7 @@
"are_you_getting_an_undefined_control_sequence_error": "Are you getting an Undefined Control Sequence error? If you are, make sure you’ve loaded the graphicx package—<0>\\usepackage{graphicx}0>—in the preamble (first section of code) in your document. <1>Learn more1>",
"are_you_still_at": "Are you still at <0>__institutionName__0>?",
"are_you_sure": "Are you sure?",
+ "are_you_sure_you_want_to_cancel_add_on": "Are you sure you want to cancel the __addOnName__ add-on?",
"article": "Article",
"articles": "Articles",
"as_a_member_of_sso_required": "As a member of __institutionName__, you must log in to __appName__ through your institution.",
@@ -223,6 +225,7 @@
"can_now_relink_dropbox": "You can now <0>relink your Dropbox account0>.",
"can_view": "Can view",
"cancel": "Cancel",
+ "cancel_add_on": "Cancel add-on",
"cancel_anytime": "We’re confident that you’ll love __appName__, but if not you can cancel anytime. We’ll give you your money back, no questions asked, if you let us know within 30 days.",
"cancel_my_account": "Cancel my subscription",
"cancel_my_subscription": "Cancel my subscription",
@@ -1495,6 +1498,7 @@
"pdf_viewer_error": "There was a problem displaying the PDF for this project.",
"pending": "Pending",
"pending_additional_licenses": "Your subscription is changing to include <0>__pendingAdditionalLicenses__0> additional license(s) for a total of <1>__pendingTotalLicenses__1> licenses.",
+ "pending_addon_cancellation": "Your subscription will change to remove the __addOnName__ add-on at the end of the current billing period.",
"pending_invite": "Pending invite",
"per_month": "per month",
"per_user": "per user",
@@ -1698,6 +1702,7 @@
"remote_service_error": "The remote service produced an error",
"remove": "Remove",
"remove_access": "Remove access",
+ "remove_add_on": "Remove add-on",
"remove_collaborator": "Remove collaborator",
"remove_from_group": "Remove from group",
"remove_link": "Remove link",
@@ -2053,6 +2058,7 @@
"sure_you_want_to_delete": "Are you sure you want to permanently delete the following files?",
"sure_you_want_to_leave_group": "Are you sure you want to leave this group?",
"sv": "Swedish",
+ "switch_plan": "Switch plan",
"switch_to_editor": "Switch to editor",
"switch_to_pdf": "Switch to PDF",
"symbol_palette": "Symbol palette",
@@ -2112,6 +2118,7 @@
"thanks_for_subscribing": "Thanks for subscribing!",
"thanks_for_subscribing_you_help_sl": "Thank you for subscribing to the __planName__ plan. It’s support from people like yourself that allows __appName__ to continue to grow and improve.",
"thanks_settings_updated": "Thanks, your settings have been updated.",
+ "the_add_on_will_remain_active_until": "The add-on will remain active until the end of the current billing period.",
"the_file_supplied_is_of_an_unsupported_type ": "The link to open this content on Overleaf pointed to the wrong kind of file. Valid file types are .tex documents and .zip files. If this keeps happening for links on a particular site, please report this to them.",
"the_following_files_already_exist_in_this_project": "The following files already exist in this project:",
"the_following_files_and_folders_already_exist_in_this_project": "The following files and folders already exist in this project:",
@@ -2326,6 +2333,7 @@
"up_to": "Up to",
"update": "Update",
"update_account_info": "Update Account Info",
+ "update_billing_details": "Update billing details",
"update_dropbox_settings": "Update Dropbox Settings",
"update_your_billing_details": "Update Your Billing Details",
"updates_to_project_sharing": "Updates to project sharing",
@@ -2387,6 +2395,7 @@
"view_hub_subtext": "Access and download subscription statistics and a list of users",
"view_in_template_gallery": "View it in the template gallery",
"view_invitation": "View Invitation",
+ "view_invoices": "View Invoices",
"view_labs_experiments": "View Labs Experiments",
"view_less": "View less",
"view_logs": "View logs",
@@ -2527,6 +2536,7 @@
"your_password_has_been_successfully_changed": "Your password has been successfully changed",
"your_password_was_detected": "Your password is on a <0>public list of known compromised passwords0>. Keep your account safe by changing your password now.",
"your_plan": "Your plan",
+ "your_plan_is": "Your plan: __planName__",
"your_plan_is_changing_at_term_end": "Your plan is changing to <0>__pendingPlanName__0> at the end of the current billing period.",
"your_plan_is_limited_to_n_editors": "Your plan allows __count__ collaborator with edit access and unlimited viewers.",
"your_plan_is_limited_to_n_editors_plural": "Your plan allows __count__ collaborators with edit access and unlimited viewers.",
diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
index 4d8b07ccfb..f823e9f003 100644
--- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
+++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
@@ -8,6 +8,7 @@ import dateformat from 'dateformat'
const today = new Date()
const oneYearFromToday = new Date().setFullYear(today.getFullYear() + 1)
const nextPaymentDueAt = dateformat(oneYearFromToday, 'dS mmmm yyyy')
+const nextPaymentDueDate = dateformat(oneYearFromToday, 'dS mmmm yyyy')
const sevenDaysFromToday = new Date().setDate(today.getDate() + 7)
const sevenDaysFromTodayFormatted = dateformat(
sevenDaysFromToday,
@@ -40,6 +41,7 @@ export const annualActiveSubscription: RecurlySubscription = {
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
+ nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
@@ -80,6 +82,7 @@ export const annualActiveSubscriptionEuro: RecurlySubscription = {
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
+ nextPaymentDueDate,
currency: 'EUR',
state: 'active',
trialEndsAtFormatted: null,
@@ -119,6 +122,7 @@ export const annualActiveSubscriptionPro: RecurlySubscription = {
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
+ nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
@@ -159,6 +163,7 @@ export const pastDueExpiredSubscription: RecurlySubscription = {
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
+ nextPaymentDueDate,
currency: 'USD',
state: 'expired',
trialEndsAtFormatted: null,
@@ -199,6 +204,7 @@ export const canceledSubscription: RecurlySubscription = {
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
+ nextPaymentDueDate,
currency: 'USD',
state: 'canceled',
trialEndsAtFormatted: null,
@@ -239,6 +245,7 @@ export const pendingSubscriptionChange: RecurlySubscription = {
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
+ nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
@@ -290,6 +297,7 @@ export const groupActiveSubscription: GroupSubscription = {
additionalLicenses: 0,
totalLicenses: 10,
nextPaymentDueAt,
+ nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
@@ -335,6 +343,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
additionalLicenses: 11,
totalLicenses: 21,
nextPaymentDueAt,
+ nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
@@ -398,6 +407,7 @@ export const trialSubscription: RecurlySubscription = {
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt: sevenDaysFromTodayFormatted,
+ nextPaymentDueDate: sevenDaysFromTodayFormatted,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: sevenDaysFromTodayFormatted,
@@ -469,6 +479,7 @@ export const trialCollaboratorSubscription: RecurlySubscription = {
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt: sevenDaysFromTodayFormatted,
+ nextPaymentDueDate: sevenDaysFromTodayFormatted,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: sevenDaysFromTodayFormatted,
@@ -518,6 +529,7 @@ export const monthlyActiveCollaborator: RecurlySubscription = {
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
+ nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
diff --git a/services/web/types/subscription/dashboard/modal-ids.ts b/services/web/types/subscription/dashboard/modal-ids.ts
index 9e332657ce..680dfc1135 100644
--- a/services/web/types/subscription/dashboard/modal-ids.ts
+++ b/services/web/types/subscription/dashboard/modal-ids.ts
@@ -4,3 +4,4 @@ export type SubscriptionDashModalIds =
| 'keep-current-plan'
| 'leave-group'
| 'change-plan'
+ | 'cancel-ai-add-on'
diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts
index 047dade746..7afe6fa815 100644
--- a/services/web/types/subscription/dashboard/subscription.ts
+++ b/services/web/types/subscription/dashboard/subscription.ts
@@ -1,26 +1,16 @@
import { CurrencyCode } from '../currency'
import { Nullable } from '../../utils'
-import { Plan } from '../plan'
+import { Plan, AddOn } from '../plan'
import { User } from '../../user'
type SubscriptionState = 'active' | 'canceled' | 'expired'
-// the add-ons attached to a recurly subsription
-export type AddOn = {
- add_on_code: string
- add_on_type: string
- quantity: number
- revenue_schedule_type: string
- unit_amount_in_cents: number
-}
-
// when puchasing a new add-on in recurly, we only need to provide the code
export type PurchasingAddOnCode = {
code: string
}
type Recurly = {
- addOns?: AddOn[]
tax: number
taxRate: number
billingDetailsLink: string
@@ -28,6 +18,7 @@ type Recurly = {
additionalLicenses: number
totalLicenses: number
nextPaymentDueAt: string
+ nextPaymentDueDate: string
currency: CurrencyCode
state?: SubscriptionState
trialEndsAtFormatted: Nullable
@@ -73,6 +64,7 @@ export type Subscription = {
recurlySubscription_id: string
plan: Plan
pendingPlan?: Plan
+ addOns?: AddOn[]
}
export type RecurlySubscription = Subscription & {
diff --git a/services/web/types/subscription/plan.ts b/services/web/types/subscription/plan.ts
index dac1bc1019..1afb7a567b 100644
--- a/services/web/types/subscription/plan.ts
+++ b/services/web/types/subscription/plan.ts
@@ -15,10 +15,40 @@ type Features = {
zotero: boolean
}
+// add-ons stored on the subscription
+export type AddOn = {
+ addOnCode: string
+ quantity: number
+ unitAmountInCents: number
+}
+
+// add-ons directly accessed through recurly
+export type RecurlyAddOn = {
+ add_on_code: string
+ quantity: number
+ unit_amount_in_cents: number
+}
+
+export type PendingRecurlyPlan = {
+ annual?: boolean
+ displayPrice?: string
+ featureDescription?: Record[]
+ addOns?: RecurlyAddOn[]
+ features?: Features
+ groupPlan?: boolean
+ hideFromUsers?: boolean
+ membersLimit?: number
+ membersLimitAddOn?: string
+ name: string
+ planCode: string
+ price_in_cents: number
+}
+
export type Plan = {
annual?: boolean
displayPrice?: string
featureDescription?: Record[]
+ addOns?: AddOn[]
features?: Features
groupPlan?: boolean
hideFromUsers?: boolean