diff --git a/packages/client/modules/userDashboard/components/OrgBilling/BillingForm.tsx b/packages/client/modules/userDashboard/components/OrgBilling/BillingForm.tsx index aeb45f9a9e4..e86bb080242 100644 --- a/packages/client/modules/userDashboard/components/OrgBilling/BillingForm.tsx +++ b/packages/client/modules/userDashboard/components/OrgBilling/BillingForm.tsx @@ -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() + const {onError, onCompleted, submitMutation, submitting, error} = useMutationProps() const [hasStarted, setHasStarted] = useState(false) const [cardNumberError, setCardNumberError] = useState() const [expiryDateError, setExpiryDateError] = useState() @@ -94,22 +92,16 @@ 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) => { 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({ @@ -117,8 +109,7 @@ const BillingForm = (props: Props) => { card: cardElement }) if (error) { - setErrorMsg(error.message) - setIsLoading(false) + onError(new Error(error.message)) return } @@ -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) => { @@ -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) @@ -237,14 +226,14 @@ const BillingForm = (props: Props) => { - {errorMsg && {errorMsg}} + {error && {error.message}} - {isLoading ? ( + {submitting ? ( <> Upgrading diff --git a/packages/server/billing/stripeWebhookHandler.ts b/packages/server/billing/stripeWebhookHandler.ts index dff889d7399..4acd744c486 100644 --- a/packages/server/billing/stripeWebhookHandler.ts +++ b/packages/server/billing/stripeWebhookHandler.ts @@ -82,17 +82,6 @@ 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, @@ -100,7 +89,7 @@ const eventLookup = { }), query: ` mutation StripeCreateSubscription($customerId: ID!, $subscriptionId: ID!) { - stripeUpdateSubscription(customerId: $customerId, subscriptionId: $subscriptionId) + stripeCreateSubscription(customerId: $customerId, subscriptionId: $subscriptionId) } ` }, diff --git a/packages/server/graphql/mutations/helpers/oldUpgradeToTeamTier.ts b/packages/server/graphql/mutations/helpers/oldUpgradeToTeamTier.ts index 361416e52ab..5e998500973 100644 --- a/packages/server/graphql/mutations/helpers/oldUpgradeToTeamTier.ts +++ b/packages/server/graphql/mutations/helpers/oldUpgradeToTeamTier.ts @@ -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) diff --git a/packages/server/graphql/private/mutations/draftEnterpriseInvoice.ts b/packages/server/graphql/private/mutations/draftEnterpriseInvoice.ts index aa0ea60f306..258c70bdeaf 100644 --- a/packages/server/graphql/private/mutations/draftEnterpriseInvoice.ts +++ b/packages/server/graphql/private/mutations/draftEnterpriseInvoice.ts @@ -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 { diff --git a/packages/server/graphql/private/mutations/stripeUpdateSubscription.ts b/packages/server/graphql/private/mutations/stripeCreateSubscription.ts similarity index 66% rename from packages/server/graphql/private/mutations/stripeUpdateSubscription.ts rename to packages/server/graphql/private/mutations/stripeCreateSubscription.ts index 12831a9fab4..55d0a872078 100644 --- a/packages/server/graphql/private/mutations/stripeUpdateSubscription.ts +++ b/packages/server/graphql/private/mutations/stripeCreateSubscription.ts @@ -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} @@ -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) @@ -39,4 +49,4 @@ const stripeUpdateSubscription: MutationResolvers['stripeUpdateSubscription'] = return true } -export default stripeUpdateSubscription +export default stripeCreateSubscription diff --git a/packages/server/graphql/private/typeDefs/_legacy.graphql b/packages/server/graphql/private/typeDefs/_legacy.graphql index 406fd1d39c0..7530f49fefe 100644 --- a/packages/server/graphql/private/typeDefs/_legacy.graphql +++ b/packages/server/graphql/private/typeDefs/_legacy.graphql @@ -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 """ @@ -1122,7 +1122,7 @@ type JiraServerIssue implements TaskIntegration { The description converted into raw HTML """ descriptionHTML: String! - + """ The timestamp the issue was last updated """ diff --git a/packages/server/graphql/public/mutations/createStripeSubscription.ts b/packages/server/graphql/public/mutations/createStripeSubscription.ts index 671078946aa..bebad7a017c 100644 --- a/packages/server/graphql/public/mutations/createStripeSubscription.ts +++ b/packages/server/graphql/public/mutations/createStripeSubscription.ts @@ -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) diff --git a/packages/server/utils/stripe/StripeManager.ts b/packages/server/utils/stripe/StripeManager.ts index 346d5456776..06594ed064a 100644 --- a/packages/server/utils/stripe/StripeManager.ts +++ b/packages/server/utils/stripe/StripeManager.ts @@ -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(