mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
[web] handle 3DS challenges for Stripe (#25918)
* handle 3DS challenges on the subscription dashboard * add `/user/subscription/sync` endpoint * upgrade `stripe-js` & rm `react-stripe-js` * group related unit tests together * add modules `SubscriptionController` unit tests and convert to async/await * add `StripeClient` unit tests for 3DS failure GitOrigin-RevId: 9da4758703f6ef4ec08248b328abddbbdd8e44ad
This commit is contained in:
35
package-lock.json
generated
35
package-lock.json
generated
@@ -11575,29 +11575,6 @@
|
||||
"storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/react-stripe-js": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.5.0.tgz",
|
||||
"integrity": "sha512-oo5J2SNbuAUjE9XmQv/SOD7vgZCa1Y9OcZyRAfvQPkyrDrru35sg5c64ANdHEmOWUibism3+25rKdARSw3HOfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stripe/stripe-js": ">=1.44.1 <7.0.0",
|
||||
"react": ">=16.8.0 <20.0.0",
|
||||
"react-dom": ">=16.8.0 <20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/stripe-js": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.10.0.tgz",
|
||||
"integrity": "sha512-PTigkxMdMUP6B5ISS7jMqJAKhgrhZwjprDqR1eATtFfh0OpKVNp110xiH+goeVdrJ29/4LeZJR4FaHHWstsu0A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
@@ -44687,8 +44664,7 @@
|
||||
"@overleaf/settings": "*",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@slack/webhook": "^7.0.2",
|
||||
"@stripe/react-stripe-js": "^3.1.1",
|
||||
"@stripe/stripe-js": "^5.6.0",
|
||||
"@stripe/stripe-js": "^7.3.0",
|
||||
"@xmldom/xmldom": "^0.7.13",
|
||||
"accepts": "^1.3.7",
|
||||
"ajv": "^8.12.0",
|
||||
@@ -45175,6 +45151,15 @@
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"services/web/node_modules/@stripe/stripe-js": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.3.0.tgz",
|
||||
"integrity": "sha512-xnCyFIEI5SQnQrKkCxVj7nS5fWTZap+zuIGzmmxLMdlmgahFJaihK4zogqE8YyKKTLtrp/EldkEijSgtXsRVDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
},
|
||||
"services/web/node_modules/@transloadit/prettier-bytes": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.9.tgz",
|
||||
|
||||
@@ -26,10 +26,17 @@ class SubtotalLimitExceededError extends OError {}
|
||||
|
||||
class HasPastDueInvoiceError extends OError {}
|
||||
|
||||
class PaymentActionRequiredError extends OError {
|
||||
constructor(info) {
|
||||
super('Payment action required', info)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RecurlyTransactionError,
|
||||
DuplicateAddOnError,
|
||||
AddOnNotPresentError,
|
||||
PaymentActionRequiredError,
|
||||
MissingBillingInfoError,
|
||||
ManuallyCollectedError,
|
||||
PendingChangeError,
|
||||
|
||||
@@ -15,7 +15,11 @@ const AnalyticsManager = require('../Analytics/AnalyticsManager')
|
||||
const RecurlyEventHandler = require('./RecurlyEventHandler')
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const { DuplicateAddOnError, AddOnNotPresentError } = require('./Errors')
|
||||
const {
|
||||
DuplicateAddOnError,
|
||||
AddOnNotPresentError,
|
||||
PaymentActionRequiredError,
|
||||
} = require('./Errors')
|
||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
const AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
@@ -425,6 +429,11 @@ async function purchaseAddon(req, res, next) {
|
||||
'Your subscription already includes this add-on',
|
||||
{ addon: addOnCode }
|
||||
)
|
||||
} else if (err instanceof PaymentActionRequiredError) {
|
||||
return res.status(402).json({
|
||||
message: 'Payment action required',
|
||||
clientSecret: err.info.clientSecret,
|
||||
})
|
||||
} else {
|
||||
if (err instanceof Error) {
|
||||
OError.tag(err, 'something went wrong purchasing add-ons', {
|
||||
|
||||
@@ -27,6 +27,7 @@ block append meta
|
||||
meta(name="ol-user" data-type="json" content=user)
|
||||
if (personalSubscription && personalSubscription.payment)
|
||||
meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
|
||||
meta(name="ol-stripeApiKey" content=settings.apis.stripe.publishableKey)
|
||||
meta(name="ol-recommendedCurrency" content=personalSubscription.payment.currency)
|
||||
meta(name="ol-groupPlans" data-type="json" content=groupPlans)
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
|
||||
import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
|
||||
import {
|
||||
postJSON,
|
||||
FetchError,
|
||||
} from '../../../../../../../../infrastructure/fetch-json'
|
||||
import getMeta from '../../../../../../../../utils/meta'
|
||||
import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
|
||||
import { subscriptionUpdateUrl } from '../../../../../../data/subscription-url'
|
||||
@@ -14,6 +17,7 @@ import OLModal, {
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
import handleStripePaymentAction from '@/features/subscription/util/handle-stripe-payment-action'
|
||||
|
||||
export function ConfirmChangePlanModal() {
|
||||
const modalId: SubscriptionDashModalIds = 'change-to-plan'
|
||||
@@ -37,8 +41,13 @@ export function ConfirmChangePlanModal() {
|
||||
})
|
||||
location.reload()
|
||||
} catch (e) {
|
||||
setError(true)
|
||||
setInflight(false)
|
||||
const { handled } = await handleStripePaymentAction(e as FetchError)
|
||||
if (handled) {
|
||||
location.reload()
|
||||
} else {
|
||||
setError(true)
|
||||
setInflight(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { formatCurrency } from '@/shared/utils/currency'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { useLocation } from '@/shared/hooks/use-location'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { FetchError, postJSON } from '@/infrastructure/fetch-json'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import OLCard from '@/features/ui/components/ol/ol-card'
|
||||
import OLRow from '@/features/ui/components/ol/ol-row'
|
||||
@@ -21,6 +21,7 @@ import { subscriptionUpdateUrl } from '@/features/subscription/data/subscription
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
import sparkleText from '@/shared/svgs/ai-sparkle-text.svg'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import handleStripePaymentAction from '../../util/handle-stripe-payment-action'
|
||||
|
||||
function PreviewSubscriptionChange() {
|
||||
const preview = getMeta(
|
||||
@@ -279,16 +280,25 @@ function PreviewSubscriptionChange() {
|
||||
}
|
||||
|
||||
async function payNow(preview: SubscriptionChangePreview) {
|
||||
if (preview.change.type === 'add-on-purchase') {
|
||||
await postJSON(`/user/subscription/addon/${preview.change.addOn.code}/add`)
|
||||
} else if (preview.change.type === 'premium-subscription') {
|
||||
await postJSON(subscriptionUpdateUrl, {
|
||||
body: { plan_code: preview.change.plan.code },
|
||||
})
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unknown subscription change preview type: ${preview.change}`
|
||||
)
|
||||
try {
|
||||
if (preview.change.type === 'add-on-purchase') {
|
||||
await postJSON(
|
||||
`/user/subscription/addon/${preview.change.addOn.code}/add`
|
||||
)
|
||||
} else if (preview.change.type === 'premium-subscription') {
|
||||
await postJSON(subscriptionUpdateUrl, {
|
||||
body: { plan_code: preview.change.plan.code },
|
||||
})
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unknown subscription change preview type: ${preview.change}`
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
const { handled } = await handleStripePaymentAction(e as FetchError)
|
||||
if (!handled) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { FetchError, postJSON } from '@/infrastructure/fetch-json'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { loadStripe } from '@stripe/stripe-js/pure'
|
||||
|
||||
export default async function handleStripePaymentAction(
|
||||
error: FetchError
|
||||
): Promise<{ handled: boolean }> {
|
||||
const clientSecret = error?.data?.clientSecret
|
||||
|
||||
if (clientSecret) {
|
||||
const stripePublicKey = getMeta('ol-stripeApiKey')
|
||||
const stripe = await loadStripe(stripePublicKey)
|
||||
if (stripe) {
|
||||
const manualConfirmationFlow =
|
||||
await stripe.confirmCardPayment(clientSecret)
|
||||
if (!manualConfirmationFlow.error) {
|
||||
try {
|
||||
await postJSON(`/user/subscription/sync`)
|
||||
} catch (error) {
|
||||
// if the sync fails, there may be stale data until the webhook is
|
||||
// processed but we can't do any special handling for that in here
|
||||
}
|
||||
return { handled: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { handled: false }
|
||||
}
|
||||
@@ -224,6 +224,7 @@ export interface Meta {
|
||||
'ol-splitTestVariants': { [name: string]: string }
|
||||
'ol-ssoDisabled': boolean
|
||||
'ol-ssoErrorMessage': string
|
||||
'ol-stripeApiKey': string
|
||||
'ol-subscription': any // TODO: mixed types, split into two fields
|
||||
'ol-subscriptionChangePreview': SubscriptionChangePreview
|
||||
'ol-subscriptionId': string
|
||||
|
||||
@@ -89,8 +89,7 @@
|
||||
"@overleaf/settings": "*",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@slack/webhook": "^7.0.2",
|
||||
"@stripe/stripe-js": "^5.6.0",
|
||||
"@stripe/react-stripe-js": "^3.1.1",
|
||||
"@stripe/stripe-js": "^7.3.0",
|
||||
"@xmldom/xmldom": "^0.7.13",
|
||||
"accepts": "^1.3.7",
|
||||
"ajv": "^8.12.0",
|
||||
|
||||
Reference in New Issue
Block a user