mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 22:23:42 +01:00
Merge pull request #27093 from overleaf/ls-support-3ds-in-group-plan-update-flows
Support 3DS verification in group plan update flows GitOrigin-RevId: 3206f612e5699f39ac44864daf6610da2956e6ca
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
|||||||
SubtotalLimitExceededError,
|
SubtotalLimitExceededError,
|
||||||
HasPastDueInvoiceError,
|
HasPastDueInvoiceError,
|
||||||
HasNoAdditionalLicenseWhenManuallyCollectedError,
|
HasNoAdditionalLicenseWhenManuallyCollectedError,
|
||||||
|
PaymentActionRequiredError,
|
||||||
} from './Errors.js'
|
} from './Errors.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,6 +274,14 @@ async function createAddSeatsSubscriptionChange(req, res) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof PaymentActionRequiredError) {
|
||||||
|
return res.status(402).json({
|
||||||
|
message: 'Payment action required',
|
||||||
|
clientSecret: error.info.clientSecret,
|
||||||
|
publicKey: error.info.publicKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
logger.err(
|
logger.err(
|
||||||
{ error },
|
{ error },
|
||||||
'error trying to create "add seats" subscription change'
|
'error trying to create "add seats" subscription change'
|
||||||
@@ -369,6 +378,13 @@ async function upgradeSubscription(req, res) {
|
|||||||
await SubscriptionGroupHandler.promises.upgradeGroupPlan(userId)
|
await SubscriptionGroupHandler.promises.upgradeGroupPlan(userId)
|
||||||
return res.sendStatus(200)
|
return res.sendStatus(200)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof PaymentActionRequiredError) {
|
||||||
|
return res.status(402).json({
|
||||||
|
message: 'Payment action required',
|
||||||
|
clientSecret: error.info.clientSecret,
|
||||||
|
publicKey: error.info.publicKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
logger.err({ error }, 'error trying to upgrade subscription')
|
logger.err({ error }, 'error trying to upgrade subscription')
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-21
@@ -30,6 +30,7 @@ import {
|
|||||||
} from '../../../../../../types/subscription/subscription-change-preview'
|
} from '../../../../../../types/subscription/subscription-change-preview'
|
||||||
import { MergeAndOverride, Nullable } from '../../../../../../types/utils'
|
import { MergeAndOverride, Nullable } from '../../../../../../types/utils'
|
||||||
import { sendMB } from '../../../../infrastructure/event-tracking'
|
import { sendMB } from '../../../../infrastructure/event-tracking'
|
||||||
|
import handleStripePaymentAction from '@/features/subscription/util/handle-stripe-payment-action'
|
||||||
|
|
||||||
export const MAX_NUMBER_OF_USERS = 20
|
export const MAX_NUMBER_OF_USERS = 20
|
||||||
export const MAX_NUMBER_OF_PO_NUMBER_CHARACTERS = 50
|
export const MAX_NUMBER_OF_PO_NUMBER_CHARACTERS = 50
|
||||||
@@ -60,13 +61,12 @@ function AddSeats() {
|
|||||||
reset: resetCostSummaryData,
|
reset: resetCostSummaryData,
|
||||||
error: errorCostSummary,
|
error: errorCostSummary,
|
||||||
} = useAsync<CostSummaryData, FetchError>()
|
} = useAsync<CostSummaryData, FetchError>()
|
||||||
const {
|
const [isAddingSeats, setIsAddingSeats] = useState(false)
|
||||||
isLoading: isAddingSeats,
|
const [isErrorAddingSeats, setIsErrorAddingSeats] = useState(false)
|
||||||
isError: isErrorAddingSeats,
|
const [isSuccessAddingSeats, setIsSuccessAddingSeats] = useState(false)
|
||||||
isSuccess: isSuccessAddingSeats,
|
const [addedSeatsData, setAddedSeatsData] = useState<{
|
||||||
runAsync: runAsyncAddSeats,
|
adding: number
|
||||||
data: addedSeatsData,
|
} | null>(null)
|
||||||
} = useAsync<{ adding: number }>()
|
|
||||||
const {
|
const {
|
||||||
isLoading: isSendingMailToSales,
|
isLoading: isSendingMailToSales,
|
||||||
isError: isErrorSendingMailToSales,
|
isError: isErrorSendingMailToSales,
|
||||||
@@ -217,21 +217,34 @@ function AddSeats() {
|
|||||||
sendMB('flex-add-users-form', {
|
sendMB('flex-add-users-form', {
|
||||||
action: 'click-add-user-button',
|
action: 'click-add-user-button',
|
||||||
})
|
})
|
||||||
const post = postJSON('/user/subscription/group/add-users/create', {
|
setIsAddingSeats(true)
|
||||||
signal: addSeatsSignal,
|
try {
|
||||||
body: {
|
const response = await postJSON<{
|
||||||
adding: Number(rawSeats),
|
adding: number
|
||||||
poNumber,
|
}>('/user/subscription/group/add-users/create', {
|
||||||
},
|
signal: addSeatsSignal,
|
||||||
})
|
body: {
|
||||||
runAsyncAddSeats(post)
|
adding: Number(rawSeats),
|
||||||
.then(() => {
|
poNumber,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
sendMB('flex-add-users-success')
|
||||||
|
setIsSuccessAddingSeats(true)
|
||||||
|
setAddedSeatsData(response)
|
||||||
|
} catch (error) {
|
||||||
|
const { handled } = await handleStripePaymentAction(error as FetchError)
|
||||||
|
if (handled) {
|
||||||
sendMB('flex-add-users-success')
|
sendMB('flex-add-users-success')
|
||||||
})
|
setIsSuccessAddingSeats(true)
|
||||||
.catch(() => {
|
setAddedSeatsData({ adding: Number(rawSeats) })
|
||||||
debugConsole.error()
|
return
|
||||||
sendMB('flex-add-users-error')
|
}
|
||||||
})
|
debugConsole.error(error)
|
||||||
|
sendMB('flex-add-users-error')
|
||||||
|
setIsErrorAddingSeats(true)
|
||||||
|
} finally {
|
||||||
|
setIsAddingSeats(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-11
@@ -1,35 +1,49 @@
|
|||||||
import getMeta from '@/utils/meta'
|
import getMeta from '@/utils/meta'
|
||||||
import { postJSON } from '@/infrastructure/fetch-json'
|
import { FetchError, postJSON } from '@/infrastructure/fetch-json'
|
||||||
import { useTranslation, Trans } from 'react-i18next'
|
import { useTranslation, Trans } from 'react-i18next'
|
||||||
import { Card, Row, Col } from 'react-bootstrap'
|
import { Card, Row, Col } from 'react-bootstrap'
|
||||||
import IconButton from '@/features/ui/components/bootstrap-5/icon-button'
|
import IconButton from '@/features/ui/components/bootstrap-5/icon-button'
|
||||||
import Button from '@/features/ui/components/bootstrap-5/button'
|
import Button from '@/features/ui/components/bootstrap-5/button'
|
||||||
import UpgradeSubscriptionPlanDetails from './upgrade-subscription-plan-details'
|
import UpgradeSubscriptionPlanDetails from './upgrade-subscription-plan-details'
|
||||||
import useAsync from '@/shared/hooks/use-async'
|
|
||||||
import RequestStatus from '../request-status'
|
import RequestStatus from '../request-status'
|
||||||
import UpgradeSummary, {
|
import UpgradeSummary, {
|
||||||
SubscriptionChange,
|
SubscriptionChange,
|
||||||
} from './upgrade-subscription-upgrade-summary'
|
} from './upgrade-subscription-upgrade-summary'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
import { sendMB } from '../../../../infrastructure/event-tracking'
|
import { sendMB } from '../../../../infrastructure/event-tracking'
|
||||||
|
import handleStripePaymentAction from '@/features/subscription/util/handle-stripe-payment-action'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
function UpgradeSubscription() {
|
function UpgradeSubscription() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const groupName = getMeta('ol-groupName')
|
const groupName = getMeta('ol-groupName')
|
||||||
const preview = getMeta('ol-subscriptionChangePreview') as SubscriptionChange
|
const preview = getMeta('ol-subscriptionChangePreview') as SubscriptionChange
|
||||||
const { isError, runAsync, isSuccess, isLoading } = useAsync()
|
const [isError, setIsError] = useState(false)
|
||||||
const onSubmit = () => {
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isSuccess, setIsSuccess] = useState(false)
|
||||||
|
const onSubmit = async () => {
|
||||||
sendMB('flex-upgrade-form', {
|
sendMB('flex-upgrade-form', {
|
||||||
action: 'click-upgrade-button',
|
action: 'click-upgrade-button',
|
||||||
})
|
})
|
||||||
runAsync(postJSON('/user/subscription/group/upgrade-subscription'))
|
setIsLoading(true)
|
||||||
.then(() => {
|
|
||||||
|
try {
|
||||||
|
await postJSON('/user/subscription/group/upgrade-subscription')
|
||||||
|
sendMB('flex-upgrade-success')
|
||||||
|
setIsSuccess(true)
|
||||||
|
} catch (error) {
|
||||||
|
const { handled } = await handleStripePaymentAction(error as FetchError)
|
||||||
|
if (handled) {
|
||||||
sendMB('flex-upgrade-success')
|
sendMB('flex-upgrade-success')
|
||||||
})
|
setIsSuccess(true)
|
||||||
.catch(() => {
|
return
|
||||||
debugConsole.error()
|
}
|
||||||
sendMB('flex-upgrade-error')
|
debugConsole.error(error)
|
||||||
})
|
sendMB('flex-upgrade-error')
|
||||||
|
setIsError(true)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
|
|||||||
+10
-1
@@ -2,7 +2,10 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useTranslation, Trans } from 'react-i18next'
|
import { useTranslation, Trans } from 'react-i18next'
|
||||||
import { PaidSubscription } from '../../../../../../../../../../types/subscription/dashboard/subscription'
|
import { PaidSubscription } from '../../../../../../../../../../types/subscription/dashboard/subscription'
|
||||||
import { PriceForDisplayData } from '../../../../../../../../../../types/subscription/plan'
|
import { PriceForDisplayData } from '../../../../../../../../../../types/subscription/plan'
|
||||||
import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
|
import {
|
||||||
|
postJSON,
|
||||||
|
FetchError,
|
||||||
|
} from '../../../../../../../../infrastructure/fetch-json'
|
||||||
import getMeta from '../../../../../../../../utils/meta'
|
import getMeta from '../../../../../../../../utils/meta'
|
||||||
import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
|
import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
|
||||||
import GenericErrorAlert from '../../../../generic-error-alert'
|
import GenericErrorAlert from '../../../../generic-error-alert'
|
||||||
@@ -23,6 +26,7 @@ import { useContactUsModal } from '@/shared/hooks/use-contact-us-modal'
|
|||||||
import { UserProvider } from '@/shared/context/user-context'
|
import { UserProvider } from '@/shared/context/user-context'
|
||||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||||
|
import handleStripePaymentAction from '@/features/subscription/util/handle-stripe-payment-action'
|
||||||
|
|
||||||
const educationalPercentDiscount = 40
|
const educationalPercentDiscount = 40
|
||||||
|
|
||||||
@@ -136,6 +140,11 @@ export function ChangeToGroupModal() {
|
|||||||
})
|
})
|
||||||
location.reload()
|
location.reload()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
const { handled } = await handleStripePaymentAction(e as FetchError)
|
||||||
|
if (handled) {
|
||||||
|
location.reload()
|
||||||
|
return
|
||||||
|
}
|
||||||
setError(true)
|
setError(true)
|
||||||
setInflight(false)
|
setInflight(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ describe('SubscriptionGroupController', function () {
|
|||||||
SubtotalLimitExceededError: class extends Error {},
|
SubtotalLimitExceededError: class extends Error {},
|
||||||
HasPastDueInvoiceError: class extends Error {},
|
HasPastDueInvoiceError: class extends Error {},
|
||||||
HasNoAdditionalLicenseWhenManuallyCollectedError: class extends Error {},
|
HasNoAdditionalLicenseWhenManuallyCollectedError: class extends Error {},
|
||||||
|
PaymentActionRequiredError: class extends Error {
|
||||||
|
constructor(info) {
|
||||||
|
super('Payment action required')
|
||||||
|
this.info = info
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
vi.doMock(
|
vi.doMock(
|
||||||
@@ -718,6 +724,38 @@ describe('SubscriptionGroupController', function () {
|
|||||||
ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
|
ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should send 402 response with PaymentActionRequiredError', async function (ctx) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
const adding = 2
|
||||||
|
ctx.req.body = { adding }
|
||||||
|
const error = new ctx.Errors.PaymentActionRequiredError({
|
||||||
|
clientSecret: 'secret',
|
||||||
|
publicKey: 'key',
|
||||||
|
})
|
||||||
|
ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
|
||||||
|
sinon.stub().throws(error)
|
||||||
|
|
||||||
|
const res = {
|
||||||
|
status: statusCode => {
|
||||||
|
statusCode.should.equal(402)
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: data => {
|
||||||
|
data.should.deep.equal({
|
||||||
|
message: 'Payment action required',
|
||||||
|
clientSecret: error.info.clientSecret,
|
||||||
|
publicKey: error.info.publicKey,
|
||||||
|
})
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('submitForm', function () {
|
describe('submitForm', function () {
|
||||||
@@ -896,5 +934,34 @@ describe('SubscriptionGroupController', function () {
|
|||||||
ctx.Controller.upgradeSubscription(ctx.req, res)
|
ctx.Controller.upgradeSubscription(ctx.req, res)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should send 402 response with PaymentActionRequiredError', async function (ctx) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
const error = new ctx.Errors.PaymentActionRequiredError({
|
||||||
|
clientSecret: 'secret',
|
||||||
|
publicKey: 'public',
|
||||||
|
})
|
||||||
|
ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
|
||||||
|
.stub()
|
||||||
|
.rejects(error)
|
||||||
|
const res = {
|
||||||
|
status: code => {
|
||||||
|
code.should.equal(402)
|
||||||
|
return {
|
||||||
|
json: data => {
|
||||||
|
data.should.deep.equal({
|
||||||
|
message: 'Payment action required',
|
||||||
|
clientSecret: error.info.clientSecret,
|
||||||
|
publicKey: error.info.publicKey,
|
||||||
|
})
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Controller.upgradeSubscription(ctx.req, res)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user