Ver Fonte

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

GitOrigin-RevId: d7447d180f1c0320989d7f86bb1d46173e235e46
Kristina há 9 meses atrás
pai
commit
16db851978

+ 8 - 1
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) {

+ 2 - 0
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) {

+ 5 - 0
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

+ 5 - 0
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'

+ 5 - 0
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

+ 5 - 0
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

+ 8 - 1
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<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"

+ 4 - 1
services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription.tsx

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

+ 20 - 1
services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx

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

+ 2 - 0
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() {
     <div className="container">
       <OLRow>
         <OLCol md={{ offset: 2, span: 8 }}>
+          <RedirectedPaymentErrorNotification />
           <OLCard className="p-3">
             {preview.change.type === 'add-on-purchase' ? (
               <h1>

+ 30 - 0
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 (
+    <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"
+    />
+  )
+}

+ 18 - 2
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 {

+ 53 - 1
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',