From 16db8519785b57e73215fa48937ef07c96ca02c4 Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:08:03 +0200 Subject: [PATCH] [web] use the correct Stripe payment confirmation function for upgrades (#29151) GitOrigin-RevId: d7447d180f1c0320989d7f86bb1d46173e235e46 --- .../Subscription/SubscriptionController.mjs | 9 +++- .../SubscriptionGroupController.mjs | 2 + .../web/app/views/subscriptions/add-seats.pug | 5 ++ .../views/subscriptions/dashboard-react.pug | 5 ++ .../views/subscriptions/preview-change.pug | 5 ++ .../upgrade-group-subscription-react.pug | 5 ++ .../components/add-seats/add-seats.tsx | 9 +++- .../upgrade-subscription.tsx | 5 +- .../dashboard/subscription-dashboard.tsx | 21 +++++++- .../preview-subscription-change/root.tsx | 2 + .../redirected-payment-error-notification.tsx | 30 +++++++++++ .../util/handle-stripe-payment-action.ts | 20 ++++++- .../SubscriptionController.test.mjs | 54 ++++++++++++++++++- 13 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 services/web/frontend/js/features/subscription/components/shared/redirected-payment-error-notification.tsx diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.mjs b/services/web/app/src/Features/Subscription/SubscriptionController.mjs index 70f7ca8274..2623a4b29f 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionController.mjs @@ -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) { diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs index b61a9d9860..07873d6486 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs @@ -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) { diff --git a/services/web/app/views/subscriptions/add-seats.pug b/services/web/app/views/subscriptions/add-seats.pug index 6fa644ee46..8fc20a4f11 100644 --- a/services/web/app/views/subscriptions/add-seats.pug +++ b/services/web/app/views/subscriptions/add-seats.pug @@ -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 diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug index 854f788b79..4048a0fa11 100644 --- a/services/web/app/views/subscriptions/dashboard-react.pug +++ b/services/web/app/views/subscriptions/dashboard-react.pug @@ -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' diff --git a/services/web/app/views/subscriptions/preview-change.pug b/services/web/app/views/subscriptions/preview-change.pug index 2eaca5ac6a..96029d5926 100644 --- a/services/web/app/views/subscriptions/preview-change.pug +++ b/services/web/app/views/subscriptions/preview-change.pug @@ -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 diff --git a/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug b/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug index 0c7f4ce993..08a1618b50 100644 --- a/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug +++ b/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug @@ -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 diff --git a/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx b/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx index 340cbf3ce7..85919655d0 100644 --- a/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx +++ b/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx @@ -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() const [poNumberInputError, setPoNumberInputError] = useState() const [shouldContactSales, setShouldContactSales] = useState(false) @@ -272,7 +275,11 @@ function AddSeats() { return () => window.removeEventListener('beforeunload', handleUnload) }, []) - if (isErrorAddingSeats || isErrorSendingMailToSales) { + if ( + isRedirectedPaymentError || + isErrorAddingSeats || + isErrorSendingMailToSales + ) { return ( @@ -41,6 +44,22 @@ function SubscriptionDashboard() { type="warning" /> )} + {hasRedirectedPaymentError && ( + , + ]} + /> + } + type="error" + /> + )}
diff --git a/services/web/frontend/js/features/subscription/components/preview-subscription-change/root.tsx b/services/web/frontend/js/features/subscription/components/preview-subscription-change/root.tsx index 15652bf5be..7ebcfc301e 100644 --- a/services/web/frontend/js/features/subscription/components/preview-subscription-change/root.tsx +++ b/services/web/frontend/js/features/subscription/components/preview-subscription-change/root.tsx @@ -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() {
+ {preview.change.type === 'add-on-purchase' ? (

diff --git a/services/web/frontend/js/features/subscription/components/shared/redirected-payment-error-notification.tsx b/services/web/frontend/js/features/subscription/components/shared/redirected-payment-error-notification.tsx new file mode 100644 index 0000000000..1a56d042da --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/shared/redirected-payment-error-notification.tsx @@ -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 ( + , + ]} + /> + } + type="error" + /> + ) +} diff --git a/services/web/frontend/js/features/subscription/util/handle-stripe-payment-action.ts b/services/web/frontend/js/features/subscription/util/handle-stripe-payment-action.ts index 97952dedd5..399ec435a5 100644 --- a/services/web/frontend/js/features/subscription/util/handle-stripe-payment-action.ts +++ b/services/web/frontend/js/features/subscription/util/handle-stripe-payment-action.ts @@ -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 { diff --git a/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs index c880a9164a..70ced6f580 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs @@ -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',