Skip to content

Commit

Permalink
fix: handle failed 3DS payments (#9924)
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Krick <[email protected]>
  • Loading branch information
mattkrick authored Jul 3, 2024
1 parent 068f91e commit 4663e9e
Show file tree
Hide file tree
Showing 8 changed files with 48 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,8 @@ const BillingForm = (props: Props) => {
const {cardNumberRef, orgId} = props
const stripe = useStripe()
const elements = useElements()
const [isLoading, setIsLoading] = useState(false)
const atmosphere = useAtmosphere()
const {onError, onCompleted} = useMutationProps()
const [errorMsg, setErrorMsg] = useState<null | string>()
const {onError, onCompleted, submitMutation, submitting, error} = useMutationProps()
const [hasStarted, setHasStarted] = useState(false)
const [cardNumberError, setCardNumberError] = useState<null | string>()
const [expiryDateError, setExpiryDateError] = useState<null | string>()
Expand All @@ -94,31 +92,24 @@ const BillingForm = (props: Props) => {
!cardNumberError &&
!expiryDateError &&
!cvcError
const isUpgradeDisabled = isLoading || !stripe || !elements || !hasValidCCDetails
const isUpgradeDisabled = submitting || !stripe || !elements || !hasValidCCDetails

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!stripe || !elements) return
setIsLoading(true)
if (errorMsg) {
setIsLoading(false)
setErrorMsg(null)
return
}
submitMutation()

const cardElement = elements.getElement(CardNumberElement)
if (!cardElement) {
setIsLoading(false)
const newErrorMsg = 'Something went wrong. Please try again.'
setErrorMsg(newErrorMsg)
onError(new Error('Something went wrong. Please try again.'))
return
}
const {paymentMethod, error} = await stripe.createPaymentMethod({
type: 'card',
card: cardElement
})
if (error) {
setErrorMsg(error.message)
setIsLoading(false)
onError(new Error(error.message))
return
}

Expand All @@ -130,14 +121,12 @@ const BillingForm = (props: Props) => {
const newErrMsg =
createStripeSubscription.error?.message ??
'Something went wrong. Please try again or contact support.'
setIsLoading(false)
setErrorMsg(newErrMsg)
onError(new Error(newErrMsg))
return
}
const {error} = await stripe.confirmCardPayment(stripeSubscriptionClientSecret)
if (error) {
setErrorMsg(error.message)
setIsLoading(false)
onError(new Error(error.message))
return
}
commitLocalUpdate(atmosphere, (store) => {
Expand All @@ -157,7 +146,7 @@ const BillingForm = (props: Props) => {

const handleChange =
(type: 'CardNumber' | 'ExpiryDate' | 'CVC') => (event: StripeElementChangeEvent) => {
if (errorMsg) setErrorMsg(null)
if (error) onCompleted()
if (!hasStarted && !event.empty) {
SendClientSideEvent(atmosphere, 'Payment Details Started', {orgId})
setHasStarted(true)
Expand Down Expand Up @@ -237,14 +226,14 @@ const BillingForm = (props: Props) => {
</div>
</div>
<ButtonBlock>
{errorMsg && <ErrorMsg>{errorMsg}</ErrorMsg>}
{error && <ErrorMsg>{error.message}</ErrorMsg>}
<UpgradeButton
size='medium'
disabled={isUpgradeDisabled}
isDisabled={isUpgradeDisabled}
type={'submit'}
>
{isLoading ? (
{submitting ? (
<>
Upgrading <Ellipsis />
</>
Expand Down
13 changes: 1 addition & 12 deletions packages/server/billing/stripeWebhookHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,14 @@ const eventLookup = {
}
},
subscription: {
updated: {
getVars: ({customer, id}: {customer: string; id: string}) => ({
customerId: customer,
subscriptionId: id
}),
query: `
mutation StripeUpdateSubscription($customerId: ID!, $subscriptionId: ID!) {
stripeUpdateSubscription(customerId: $customerId, subscriptionId: $subscriptionId)
}
`
},
created: {
getVars: ({customer, id}: {customer: string; id: string}) => ({
customerId: customer,
subscriptionId: id
}),
query: `
mutation StripeCreateSubscription($customerId: ID!, $subscriptionId: ID!) {
stripeUpdateSubscription(customerId: $customerId, subscriptionId: $subscriptionId)
stripeCreateSubscription(customerId: $customerId, subscriptionId: $subscriptionId)
}
`
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const oldUpgradeToTeamTier = async (
const customer = stripeId
? await manager.updatePayment(stripeId, source)
: await manager.createCustomer(orgId, email, undefined, source)

if (customer instanceof Error) throw customer
let subscriptionFields = {}
if (!stripeSubscriptionId) {
const subscription = await manager.createTeamSubscriptionOld(customer.id, orgId, quantity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const draftEnterpriseInvoice: MutationResolvers['draftEnterpriseInvoice'] = asyn
if (!stripeId) {
// create the customer
const customer = await manager.createCustomer(orgId, apEmail || user.email)
if (customer instanceof Error) throw customer
await r.table('Organization').get(orgId).update({stripeId: customer.id}).run()
customerId = customer.id
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Stripe from 'stripe'
import getRethink from '../../../database/rethinkDriver'
import {isSuperUser} from '../../../utils/authorization'
import {getStripeManager} from '../../../utils/stripe'
import {MutationResolvers} from '../resolverTypes'

const stripeUpdateSubscription: MutationResolvers['stripeUpdateSubscription'] = async (
const stripeCreateSubscription: MutationResolvers['stripeCreateSubscription'] = async (
_source,
{customerId, subscriptionId},
{authToken}
Expand All @@ -28,6 +29,15 @@ const stripeUpdateSubscription: MutationResolvers['stripeUpdateSubscription'] =
throw new Error(`orgId not found on metadata for customer ${customerId}`)
}

const subscription = await manager.retrieveSubscription(subscriptionId)
const invalidStatuses: Stripe.Subscription.Status[] = [
'canceled',
'incomplete',
'incomplete_expired'
]
const isSubscriptionInvalid = invalidStatuses.some((status) => (subscription.status = status))
if (isSubscriptionInvalid) return false

await r
.table('Organization')
.get(orgId)
Expand All @@ -39,4 +49,4 @@ const stripeUpdateSubscription: MutationResolvers['stripeUpdateSubscription'] =
return true
}

export default stripeUpdateSubscription
export default stripeCreateSubscription
4 changes: 2 additions & 2 deletions packages/server/graphql/private/typeDefs/_legacy.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ type Mutation {
"""
When stripe tells us a subscription was updated, update the details in our own DB
"""
stripeUpdateSubscription(
stripeCreateSubscription(
"""
The stripe customer ID, or stripeId
"""
Expand Down Expand Up @@ -1122,7 +1122,7 @@ type JiraServerIssue implements TaskIntegration {
The description converted into raw HTML
"""
descriptionHTML: String!

"""
The timestamp the issue was last updated
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ const createStripeSubscription: MutationResolvers['createStripeSubscription'] =
// cannot updateDefaultPaymentMethod until it is attached to the customer
await manager.updateDefaultPaymentMethod(customerId, paymentMethodId)
} else {
customer = await manager.createCustomer(orgId, email, paymentMethodId)
const maybeCustomer = await manager.createCustomer(orgId, email, paymentMethodId)
if (maybeCustomer instanceof Error) return {error: {message: maybeCustomer.message}}
customer = maybeCustomer
}

const subscription = await manager.createTeamSubscription(customer.id, orgId, orgUsersCount)
Expand Down
30 changes: 17 additions & 13 deletions packages/server/utils/stripe/StripeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,23 @@ export default class StripeManager {
paymentMethodId?: string | undefined,
source?: string
) {
return this.stripe.customers.create({
email,
source,
payment_method: paymentMethodId,
invoice_settings: paymentMethodId
? {
default_payment_method: paymentMethodId
}
: undefined,
metadata: {
orgId
}
})
try {
return await this.stripe.customers.create({
email,
source,
payment_method: paymentMethodId,
invoice_settings: paymentMethodId
? {
default_payment_method: paymentMethodId
}
: undefined,
metadata: {
orgId
}
})
} catch (e) {
return e as Error
}
}

async createEnterpriseSubscription(
Expand Down

0 comments on commit 4663e9e

Please sign in to comment.