mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-31 21:01:33 +02:00
[web] use the correct Stripe payment confirmation function for upgrades (#29151)
GitOrigin-RevId: d7447d180f1c0320989d7f86bb1d46173e235e46
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user