[web] Remove stale "You already have a subscription" notification (#33187)

* Remove stale "You already have a subscription" notification after cancel/plan change

The notification was derived from a server-rendered meta tag set at page load,
so it persisted through cancel and plan-change flows. Now derived directly from
the URL param on the client; the param is stripped on cancel button click
(replaceState) and before plan-change reloads (location.replace via
reloadWithoutHasSubscription helper).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix format

* Update services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix change-plan tests after location.reload → location.replace migration

reloadWithoutHasSubscription calls location.replace() not location.reload(),
so update assertions accordingly. Also stub toString() to return the jsdom
origin so FlashMessage's replaceState call doesn't throw a SecurityError.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Guard reloadWithoutHasSubscription against empty URL

When called after component unmount, useLocation's toString() returns '',
causing new URL('') to throw. No-op early to avoid the exception.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Guard against empty URL in history state replacement for subscription cancellation

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: 8408ee971adf038e2d819eae5df060ace62a7e14
This commit is contained in:
Antoine Clausse
2026-04-30 14:45:21 +02:00
committed by Copybot
parent 353c681d51
commit dd44f4e2e8
11 changed files with 58 additions and 28 deletions

View File

@@ -217,7 +217,6 @@ async function userSubscriptionPage(req, res) {
const userCanExtendTrial = (
await Modules.promises.hooks.fire('userCanExtendTrial', user)
)?.[0]
const fromPlansPage = req.query.hasSubscription
const redirectedPaymentErrorCode = req.query.errorCode
const isInTrial = SubscriptionHelper.isInTrial(
personalSubscription?.payment?.trialEndsAt
@@ -321,7 +320,6 @@ async function userSubscriptionPage(req, res) {
planCodesChangingAtTermEnd: plansData?.planCodesChangingAtTermEnd,
user,
hasSubscription,
fromPlansPage,
redirectedPaymentErrorCode,
personalSubscription,
userCanExtendTrial,

View File

@@ -47,7 +47,6 @@ block append meta
content=currentInstitutionsWithLicence
)
meta(name='ol-hasSubscription' data-type='boolean' content=hasSubscription)
meta(name='ol-fromPlansPage' data-type='boolean' content=fromPlansPage)
meta(
name='ol-subscriptionPaymentErrorCode'
data-type='string'

View File

@@ -4,9 +4,12 @@ import { useSubscriptionDashboardContext } from '../../../../context/subscriptio
import OLButton from '@/shared/components/ol/ol-button'
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useLocation } from '@/shared/hooks/use-location'
import { stripHasSubscription } from '../../../../data/subscription-url'
export function CancelSubscriptionButton() {
const { t } = useTranslation()
const location = useLocation()
const {
recurlyLoadError,
personalSubscription,
@@ -35,6 +38,10 @@ export function CancelSubscriptionButton() {
plan_code: subscription?.planCode,
is_trial: isInTrial,
})
const url = location.toString()
if (url) {
window.history.replaceState(null, '', stripHasSubscription(url))
}
if (enablePause) {
setModalIdShown('pause-subscription')
} else {

View File

@@ -9,7 +9,10 @@ import {
import getMeta from '../../../../../../../../utils/meta'
import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
import GenericErrorAlert from '../../../../generic-error-alert'
import { subscriptionUpdateUrl } from '../../../../../../data/subscription-url'
import {
subscriptionUpdateUrl,
reloadWithoutHasSubscription,
} from '../../../../../../data/subscription-url'
import { getRecurlyGroupPlanCode } from '../../../../../../util/recurly-group-plan-code'
import { useLocation } from '../../../../../../../../shared/hooks/use-location'
import {
@@ -139,11 +142,11 @@ export function ChangeToGroupModal() {
),
},
})
location.reload()
reloadWithoutHasSubscription(location)
} catch (e) {
const { handled } = await handleStripePaymentAction(e as FetchError)
if (handled) {
location.reload()
reloadWithoutHasSubscription(location)
return
}
setError(true)

View File

@@ -7,7 +7,10 @@ import {
} from '../../../../../../../../infrastructure/fetch-json'
import getMeta from '../../../../../../../../utils/meta'
import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
import { subscriptionUpdateUrl } from '../../../../../../data/subscription-url'
import {
subscriptionUpdateUrl,
reloadWithoutHasSubscription,
} from '../../../../../../data/subscription-url'
import { useLocation } from '../../../../../../../../shared/hooks/use-location'
import {
OLModal,
@@ -40,12 +43,12 @@ export function ConfirmChangePlanModal() {
plan_code: planCodeToChangeTo,
},
})
location.reload()
reloadWithoutHasSubscription(location)
} catch (e) {
const fetchError = e as FetchError
const { handled } = await handleStripePaymentAction(fetchError)
if (handled) {
location.reload()
reloadWithoutHasSubscription(location)
} else {
setError(fetchError)
setInflight(false)

View File

@@ -3,7 +3,10 @@ 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 { cancelPendingSubscriptionChangeUrl } from '../../../../../../data/subscription-url'
import {
cancelPendingSubscriptionChangeUrl,
reloadWithoutHasSubscription,
} from '../../../../../../data/subscription-url'
import { useLocation } from '../../../../../../../../shared/hooks/use-location'
import {
OLModal,
@@ -30,7 +33,7 @@ export function KeepCurrentPlanModal() {
try {
await postJSON(cancelPendingSubscriptionChangeUrl)
location.reload()
reloadWithoutHasSubscription(location)
} catch (e) {
setError(true)
setInflight(false)

View File

@@ -27,10 +27,13 @@ function SubscriptionDashboard() {
personalSubscription,
} = useSubscriptionDashboardContext()
const fromPlansPage = new URLSearchParams(window.location.search).has(
'hasSubscription'
)
const subscription = personalSubscription as PaidSubscription
const hasAiAssistViaWritefull = getMeta('ol-hasAiAssistViaWritefull')
const fromPlansPage = getMeta('ol-fromPlansPage')
const hasRedirectedPaymentError = Boolean(
getMeta('ol-subscriptionPaymentErrorCode')
)

View File

@@ -6,3 +6,18 @@ export const redirectAfterCancelSubscriptionUrl = '/user/subscription/canceled'
export const extendTrialUrl = '/user/subscription/extend'
export const reactivateSubscriptionUrl = '/user/subscription/reactivate'
export const billingPortalUrl = '/user/subscription/payment/account-management'
export function stripHasSubscription(url: string): string {
const parsed = new URL(url)
parsed.searchParams.delete('hasSubscription')
return parsed.toString()
}
export function reloadWithoutHasSubscription(location: {
toString(): string
replace(url: string): void
}) {
const url = location.toString()
if (!url) return
location.replace(stripHasSubscription(url))
}

View File

@@ -131,7 +131,6 @@ export interface Meta {
'ol-featureUsage': FeatureUsage
'ol-features': Features
'ol-footer': FooterMetadata
'ol-fromPlansPage': boolean
'ol-galleryTagName': string
'ol-gitBridgeEnabled': boolean
'ol-gitBridgePublicBaseUrl': string

View File

@@ -27,6 +27,7 @@ describe('<ChangePlanModal />', function () {
beforeEach(function () {
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
this.locationWrapperStub.toString.returns('https://www.test-overleaf.com/')
})
afterEach(function () {
@@ -214,9 +215,9 @@ describe('<ChangePlanModal />', function () {
screen.getByRole('button', { name: 'Processing…' })
// page is reloaded on success
const reloadStub = this.locationWrapperStub.reload
const replaceStub = this.locationWrapperStub.replace
await waitFor(() => {
expect(reloadStub).to.have.been.called
expect(replaceStub).to.have.been.called
})
})
@@ -304,9 +305,9 @@ describe('<ChangePlanModal />', function () {
screen.getByRole('button', { name: 'Processing…' })
// page is reloaded on success
const reloadStub = this.locationWrapperStub.reload
const replaceStub = this.locationWrapperStub.replace
await waitFor(() => {
expect(reloadStub).to.have.been.called
expect(replaceStub).to.have.been.called
})
})
@@ -536,9 +537,9 @@ describe('<ChangePlanModal />', function () {
screen.getByRole('button', { name: 'Processing…' })
// page is reloaded on success
const reloadStub = this.locationWrapperStub.reload
const replaceStub = this.locationWrapperStub.replace
await waitFor(() => {
expect(reloadStub).to.have.been.called
expect(replaceStub).to.have.been.called
})
})

View File

@@ -84,15 +84,14 @@ describe('<SubscriptionDashboard />', function () {
})
it('Show a warning when coming from plans page', function () {
renderWithSubscriptionDashContext(<SubscriptionDashboard />, {
metaTags: [
{
name: 'ol-fromPlansPage',
value: true,
},
],
})
window.history.pushState({}, '', '?hasSubscription=true')
screen.getByText('You already have a subscription')
try {
renderWithSubscriptionDashContext(<SubscriptionDashboard />)
screen.getByText('You already have a subscription')
} finally {
window.history.replaceState({}, '', window.location.pathname)
}
})
})