[web] use the correct Stripe payment confirmation function for upgrades (#29151)

GitOrigin-RevId: d7447d180f1c0320989d7f86bb1d46173e235e46
This commit is contained in:
Kristina
2025-10-21 12:08:03 +02:00
committed by Copybot
parent 129ea72d36
commit 16db851978
13 changed files with 165 additions and 7 deletions

View File

@@ -204,6 +204,7 @@ async function userSubscriptionPage(req, res) {
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
)
@@ -306,6 +307,7 @@ async function userSubscriptionPage(req, res) {
user,
hasSubscription,
fromPlansPage,
redirectedPaymentErrorCode,
personalSubscription,
userCanExtendTrial,
memberGroupSubscriptions,
@@ -474,6 +476,7 @@ async function previewAddonPurchase(req, res) {
const userId = user._id
const addOnCode = req.params.addOnCode
const purchaseReferrer = req.query.purchaseReferrer
const redirectedPaymentErrorCode = req.query.errorCode
if (addOnCode !== AI_ADD_ON_CODE) {
return HttpErrorHandler.notFound(req, res, `Unknown add-on: ${addOnCode}`)
@@ -571,6 +574,7 @@ async function previewAddonPurchase(req, res) {
res.render('subscriptions/preview-change', {
changePreview,
purchaseReferrer,
redirectedPaymentErrorCode,
})
}
@@ -763,7 +767,10 @@ async function previewSubscription(req, res, next) {
paymentMethod[0]
)
res.render('subscriptions/preview-change', { changePreview })
res.render('subscriptions/preview-change', {
changePreview,
redirectedPaymentErrorCode: req.query.errorCode,
})
}
function cancelPendingSubscriptionChange(req, res, next) {

View File

@@ -170,6 +170,7 @@ async function addSeatsToGroupSubscription(req, res) {
isProfessional: isProfessionalGroupPlan(subscription),
isCollectionMethodManual:
paymentProviderSubscription.isCollectionMethodManual,
redirectedPaymentErrorCode: req.query.errorCode,
})
} catch (error) {
if (error instanceof MissingBillingInfoError) {
@@ -363,6 +364,7 @@ async function subscriptionUpgradePage(req, res) {
changePreview,
totalLicenses: olSubscription.membersLimit,
groupName: olSubscription.teamName,
redirectedPaymentErrorCode: req.query.errorCode,
})
} catch (error) {
if (error instanceof MissingBillingInfoError) {

View File

@@ -14,6 +14,11 @@ block append meta
data-type='boolean'
content=isCollectionMethodManual
)
meta(
name='ol-subscriptionPaymentErrorCode'
data-type='string'
content=redirectedPaymentErrorCode
)
block content
main#main-content.content.content-alt

View File

@@ -45,6 +45,11 @@ block append meta
)
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'
content=redirectedPaymentErrorCode
)
meta(name='ol-plans' data-type='json' content=plans)
meta(
name='ol-groupSettingsAdvertisedFor'

View File

@@ -11,6 +11,11 @@ block append meta
content=changePreview
)
meta(name='ol-purchaseReferrer' data-type='string' content=purchaseReferrer)
meta(
name='ol-subscriptionPaymentErrorCode'
data-type='string'
content=redirectedPaymentErrorCode
)
block content
main#main-content.content.content-alt

View File

@@ -12,6 +12,11 @@ block append meta
)
meta(name='ol-totalLicenses' data-type='number' content=totalLicenses)
meta(name='ol-groupName' data-type='string' content=groupName)
meta(
name='ol-subscriptionPaymentErrorCode'
data-type='string'
content=redirectedPaymentErrorCode
)
block content
main#upgrade-group-subscription-root.content.content-alt

View File

@@ -48,6 +48,9 @@ function AddSeats() {
const totalLicenses = getMeta('ol-totalLicenses')
const isProfessional = getMeta('ol-isProfessional')
const isCollectionMethodManual = getMeta('ol-isCollectionMethodManual')
const isRedirectedPaymentError = Boolean(
getMeta('ol-subscriptionPaymentErrorCode')
)
const [addSeatsInputError, setAddSeatsInputError] = useState<string>()
const [poNumberInputError, setPoNumberInputError] = useState<string>()
const [shouldContactSales, setShouldContactSales] = useState(false)
@@ -272,7 +275,11 @@ function AddSeats() {
return () => window.removeEventListener('beforeunload', handleUnload)
}, [])
if (isErrorAddingSeats || isErrorSendingMailToSales) {
if (
isRedirectedPaymentError ||
isErrorAddingSeats ||
isErrorSendingMailToSales
) {
return (
<RequestStatus
variant="danger"

View File

@@ -18,6 +18,9 @@ function UpgradeSubscription() {
const { t } = useTranslation()
const groupName = getMeta('ol-groupName')
const preview = getMeta('ol-subscriptionChangePreview') as SubscriptionChange
const isRedirectedPaymentError = Boolean(
getMeta('ol-subscriptionPaymentErrorCode')
)
const [isError, setIsError] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
@@ -56,7 +59,7 @@ function UpgradeSubscription() {
)
}
if (isError) {
if (isRedirectedPaymentError || isError) {
return (
<RequestStatus
variant="danger"

View File

@@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import ContactSupport from './contact-support-for-custom-subscription'
import GroupSubscriptionMemberships from './group-subscription-memberships'
import InstitutionMemberships from './institution-memberships'
@@ -28,6 +28,9 @@ function SubscriptionDashboard() {
const hasAiAssistViaWritefull = getMeta('ol-hasAiAssistViaWritefull')
const fromPlansPage = getMeta('ol-fromPlansPage')
const hasRedirectedPaymentError = Boolean(
getMeta('ol-subscriptionPaymentErrorCode')
)
return (
<div className="container">
@@ -41,6 +44,22 @@ function SubscriptionDashboard() {
type="warning"
/>
)}
{hasRedirectedPaymentError && (
<OLNotification
className="mb-4"
aria-live="polite"
content={
<Trans
i18nKey="payment_error_generic"
components={[
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
<a href="/contact" target="_blank" />,
]}
/>
}
type="error"
/>
)}
<RedirectAlerts />
<OLPageContentCard>
<div className="page-header">

View File

@@ -22,6 +22,7 @@ import sparkleText from '@/shared/svgs/ai-sparkle-text.svg'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import PaymentErrorNotification from '@/features/subscription/components/shared/payment-error-notification'
import handleStripePaymentAction from '../../util/handle-stripe-payment-action'
import RedirectedPaymentErrorNotification from '../shared/redirected-payment-error-notification'
function PreviewSubscriptionChange() {
const preview = getMeta(
@@ -98,6 +99,7 @@ function PreviewSubscriptionChange() {
<div className="container">
<OLRow>
<OLCol md={{ offset: 2, span: 8 }}>
<RedirectedPaymentErrorNotification />
<OLCard className="p-3">
{preview.change.type === 'add-on-purchase' ? (
<h1>

View File

@@ -0,0 +1,30 @@
import { Trans } from 'react-i18next'
import OLNotification from '@/shared/components/ol/ol-notification'
import getMeta from '@/utils/meta'
export default function RedirectedPaymentErrorNotification() {
const hasRedirectedPaymentError = Boolean(
getMeta('ol-subscriptionPaymentErrorCode')
)
if (!hasRedirectedPaymentError) {
return null
}
return (
<OLNotification
className="mb-4"
aria-live="polite"
content={
<Trans
i18nKey="payment_error_generic"
components={[
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
<a href="/contact" target="_blank" />,
]}
/>
}
type="error"
/>
)
}

View File

@@ -1,4 +1,5 @@
import { FetchError, postJSON } from '@/infrastructure/fetch-json'
import getMeta from '@/utils/meta'
import { loadStripe } from '@stripe/stripe-js/pure'
export default async function handleStripePaymentAction(
@@ -10,8 +11,23 @@ export default async function handleStripePaymentAction(
if (clientSecret && publicKey) {
const stripe = await loadStripe(publicKey)
if (stripe) {
const manualConfirmationFlow =
await stripe.confirmCardPayment(clientSecret)
const currentPath = window.location.pathname
const returnUrl = new URL(
'/user/subscription/offsite',
getMeta('ol-ExposedSettings').siteUrl
)
const returnParams = new URLSearchParams({
path: currentPath,
})
returnUrl.search = returnParams.toString()
const manualConfirmationFlow = await stripe.confirmPayment({
clientSecret,
redirect: 'if_required',
confirmParams: {
return_url: returnUrl.toString(),
},
})
if (manualConfirmationFlow.error) {
const paymentIntentId = manualConfirmationFlow.error.payment_intent?.id
try {

View File

@@ -507,6 +507,28 @@ describe('SubscriptionController', function () {
it('should load an empty list of groups with settings available', function (ctx) {
expect(ctx.data.groupSettingsEnabledFor).to.deep.equal([])
})
describe('when errorCode query param is present', function () {
beforeEach(async function (ctx) {
ctx.req.query.errorCode = 'payment_failed'
await new Promise((resolve, reject) => {
ctx.res.render = (view, data) => {
ctx.data = data
expect(view).to.equal('subscriptions/dashboard-react')
resolve()
}
ctx.SubscriptionController.userSubscriptionPage(
ctx.req,
ctx.res,
ctx.rejectOnError(reject)
)
})
})
it('should pass redirectedPaymentErrorCode to the view', function (ctx) {
expect(ctx.data.redirectedPaymentErrorCode).to.equal('payment_failed')
})
})
})
describe('updateAccountEmailAddress via put', function () {
@@ -1527,13 +1549,43 @@ describe('SubscriptionController', function () {
await ctx.SubscriptionController.previewAddonPurchase(ctx.req, ctx.res)
expect(ctx.res.render).to.have.been.calledWith(
'subscriptions/preview-change'
'subscriptions/preview-change',
sinon.match({
changePreview: sinon.match.object,
purchaseReferrer: 'fake-referrer',
redirectedPaymentErrorCode: undefined,
})
)
expect(
ctx.SubscriptionHandler.promises.previewAddonPurchase
).to.have.been.calledWith(ctx.user._id, 'assistant')
})
it('should pass redirectedPaymentErrorCode to the view when errorCode query param is present', async function (ctx) {
const normalSubscription = {
_id: 'sub-123',
customAccount: false,
collectionMethod: 'automatic',
}
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
normalSubscription
)
ctx.req.query.errorCode = 'payment_failed'
ctx.res.render = sinon.stub()
await ctx.SubscriptionController.previewAddonPurchase(ctx.req, ctx.res)
expect(ctx.res.render).to.have.been.calledWith(
'subscriptions/preview-change',
sinon.match({
changePreview: sinon.match.object,
purchaseReferrer: 'fake-referrer',
redirectedPaymentErrorCode: 'payment_failed',
})
)
})
it('should proceed with preview when customAccount is undefined and collectionMethod is automatic', async function (ctx) {
const normalSubscription = {
_id: 'sub-123',