mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat: support pending changes (#36333)
Adds support for the new Orb pending subscription change flow that has been added to the backend. By default, the new feature is disabled (enabled after merge on staging). If disabled, this should work just like before with the regular `confirmPayment` function that triggers 3DS if needed and properly lets a user confirm their additional factor. With the new flag, we only create a payment method, as we will do another payment intent that is set up for future usage and that may require 3DS - so we avoid the possible double confirmation (setup confirmation + payment intent). The organization creation endpoint can either return a full organization (i.e. on Free Plan or immediate payment success) or a payment_intent_secret. In case of the secret being returned, we need to confirm the secret with the Stripe SDK. Moved the Stripe Elements further down to avoid the entire form reloading (especially billing address) in case of a payment failure.
This commit is contained in:
1 parent
de3afc5394
commit
ff318d07f0
10 files changed
+493
-391
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import { useStripe } from '@stripe/react-stripe-js'
|
||||
import { PaymentIntentResult } from '@stripe/stripe-js'
|
||||
import { useEffect } from 'react'
|
||||
import { LoadingLine } from 'ui'
|
||||
|
||||
export const PaymentConfirmation = ({
|
||||
paymentIntentSecret,
|
||||
onPaymentIntentConfirm,
|
||||
onLoadingChange,
|
||||
paymentMethodId,
|
||||
onError,
|
||||
}: {
|
||||
paymentIntentSecret: string
|
||||
paymentMethodId: string
|
||||
onPaymentIntentConfirm: (response: PaymentIntentResult) => void
|
||||
onLoadingChange: (loading: boolean) => void
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const stripe = useStripe()
|
||||
|
||||
useEffect(() => {
|
||||
if (stripe && paymentIntentSecret) {
|
||||
onLoadingChange(true)
|
||||
stripe!
|
||||
.confirmCardPayment(paymentIntentSecret, { payment_method: paymentMethodId })
|
||||
.then((res) => {
|
||||
onPaymentIntentConfirm(res)
|
||||
onLoadingChange(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
onError?.(err)
|
||||
onLoadingChange(false)
|
||||
})
|
||||
}
|
||||
}, [paymentIntentSecret, stripe])
|
||||
|
||||
return <LoadingLine loading={true} />
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Elements, useStripe } from '@stripe/react-stripe-js'
|
||||
import { Elements } from '@stripe/react-stripe-js'
|
||||
import { loadStripe, PaymentIntentResult } from '@stripe/stripe-js'
|
||||
import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/constants'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
@@ -35,10 +35,10 @@ import {
|
||||
Form_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
LoadingLine,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import PaymentMethodSelection from './Subscription/PaymentMethodSelection'
|
||||
import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation'
|
||||
|
||||
const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
|
||||
|
||||
@@ -331,34 +331,3 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PaymentConfirmation = ({
|
||||
paymentIntentSecret,
|
||||
onPaymentIntentConfirm,
|
||||
onLoadingChange,
|
||||
paymentMethodId,
|
||||
}: {
|
||||
paymentIntentSecret: string
|
||||
paymentMethodId: string
|
||||
onPaymentIntentConfirm: (response: PaymentIntentResult) => void
|
||||
onLoadingChange: (loading: boolean) => void
|
||||
}) => {
|
||||
const stripe = useStripe()
|
||||
|
||||
useEffect(() => {
|
||||
if (stripe && paymentIntentSecret) {
|
||||
onLoadingChange(true)
|
||||
stripe!
|
||||
.confirmCardPayment(paymentIntentSecret, { payment_method: paymentMethodId })
|
||||
.then((res) => {
|
||||
onPaymentIntentConfirm(res)
|
||||
onLoadingChange(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
onLoadingChange(false)
|
||||
})
|
||||
}
|
||||
}, [paymentIntentSecret, stripe])
|
||||
|
||||
return <LoadingLine loading={true} />
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js'
|
||||
import type { PaymentMethod } from '@stripe/stripe-js'
|
||||
import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js'
|
||||
import type { PaymentIntentResult, PaymentMethod, StripeElementsOptions } from '@stripe/stripe-js'
|
||||
import _ from 'lodash'
|
||||
import { Edit2, ExternalLink, HelpCircle } from 'lucide-react'
|
||||
import { ExternalLink, HelpCircle } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseAsString, useQueryStates } from 'nuqs'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { z } from 'zod'
|
||||
|
||||
@@ -16,9 +16,7 @@ import { useOrganizationCreateMutation } from 'data/organizations/organization-c
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
|
||||
import { BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants'
|
||||
import { getURL } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { PRICING_TIER_LABELS_ORG, STRIPE_PUBLIC_KEY } from 'lib/constants'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -37,6 +35,13 @@ import {
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { BillingCustomerDataNewOrgDialog } from '../BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog'
|
||||
import { FormCustomerData } from '../BillingSettings/BillingCustomerData/useBillingCustomerDataForm'
|
||||
import { useConfirmPendingSubscriptionChangeMutation } from 'data/subscriptions/org-subscription-confirm-pending-change'
|
||||
import { loadStripe } from '@stripe/stripe-js'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { SetupIntentResponse } from 'data/stripe/setup-intent-mutation'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation'
|
||||
import { getURL } from 'lib/helpers'
|
||||
|
||||
const ORG_KIND_TYPES = {
|
||||
PERSONAL: 'Personal',
|
||||
@@ -59,6 +64,8 @@ const ORG_SIZE_DEFAULT = '1'
|
||||
|
||||
interface NewOrgFormProps {
|
||||
onPaymentMethodReset: () => void
|
||||
setupIntent?: SetupIntentResponse
|
||||
onPlanSelected: (plan: string) => void
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -79,17 +86,18 @@ const formSchema = z.object({
|
||||
|
||||
type FormState = z.infer<typeof formSchema>
|
||||
|
||||
const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
|
||||
|
||||
/**
|
||||
* No org selected yet, create a new one
|
||||
* [Joshen] Need to refactor to use Form_Shadcn here
|
||||
*/
|
||||
const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOrgFormProps) => {
|
||||
const router = useRouter()
|
||||
const user = useProfile()
|
||||
const { data: organizations, isSuccess } = useOrganizationsQuery()
|
||||
const { data: projects } = useProjectsQuery()
|
||||
const stripe = useStripe()
|
||||
const elements = useElements()
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
const [lastVisitedOrganization] = useLocalStorageQuery(
|
||||
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
|
||||
@@ -107,6 +115,21 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
|
||||
const [customerData, setCustomerData] = useState<FormCustomerData | null>(null)
|
||||
|
||||
const stripeOptionsPaymentMethod: StripeElementsOptions = useMemo(
|
||||
() =>
|
||||
({
|
||||
clientSecret: setupIntent ? setupIntent.client_secret! : '',
|
||||
appearance: {
|
||||
theme: resolvedTheme?.includes('dark') ? 'night' : 'flat',
|
||||
labels: 'floating',
|
||||
},
|
||||
...(setupIntent?.pending_subscription_flow_enabled_for_creation === true
|
||||
? { paymentMethodCreation: 'manual' }
|
||||
: {}),
|
||||
}) as const,
|
||||
[setupIntent, resolvedTheme]
|
||||
)
|
||||
|
||||
const [formState, setFormState] = useState<FormState>({
|
||||
plan: 'FREE',
|
||||
name: '',
|
||||
@@ -118,7 +141,6 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
const [searchParams] = useQueryStates({
|
||||
returnTo: parseAsString.withDefault(''),
|
||||
auth_id: parseAsString.withDefault(''),
|
||||
token: parseAsString.withDefault(''),
|
||||
})
|
||||
|
||||
const updateForm = (key: keyof FormState, value: unknown) => {
|
||||
@@ -132,7 +154,10 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
|
||||
if (typeof name === 'string') updateForm('name', name)
|
||||
if (typeof kind === 'string') updateForm('kind', kind)
|
||||
if (typeof plan === 'string') updateForm('plan', plan)
|
||||
if (typeof plan === 'string') {
|
||||
updateForm('plan', plan)
|
||||
onPlanSelected(plan)
|
||||
}
|
||||
if (typeof size === 'string') updateForm('size', size)
|
||||
if (typeof spend_cap === 'string') updateForm('spend_cap', spend_cap === 'true')
|
||||
}, [router.isReady])
|
||||
@@ -147,26 +172,16 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
const [newOrgLoading, setNewOrgLoading] = useState(false)
|
||||
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>()
|
||||
|
||||
const [paymentConfirmationLoading, setPaymentConfirmationLoading] = useState(false)
|
||||
const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false)
|
||||
const [paymentIntentSecret, setPaymentIntentSecret] = useState<string | null>(null)
|
||||
|
||||
const { mutate: createOrganization } = useOrganizationCreateMutation({
|
||||
onSuccess: async (org) => {
|
||||
const prefilledProjectName = user.profile?.username
|
||||
? user.profile.username + `'s Project`
|
||||
: 'My Project'
|
||||
|
||||
if (searchParams.returnTo && searchParams.auth_id) {
|
||||
router.push(
|
||||
`${searchParams.returnTo}?auth_id=${searchParams.auth_id}${
|
||||
searchParams.token && `&token=${searchParams.token}&org=${org.name}`
|
||||
}`,
|
||||
undefined,
|
||||
{
|
||||
shallow: false,
|
||||
}
|
||||
)
|
||||
if ('pending_payment_intent_secret' in org && org.pending_payment_intent_secret) {
|
||||
setPaymentIntentSecret(org.pending_payment_intent_secret)
|
||||
} else {
|
||||
router.push(`/new/${org.slug}?projectName=${prefilledProjectName}`)
|
||||
onOrganizationCreated(org as { slug: string })
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
@@ -176,11 +191,59 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: confirmPendingSubscriptionChange } = useConfirmPendingSubscriptionChangeMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data && 'slug' in data) {
|
||||
onOrganizationCreated({ slug: data.slug })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const paymentIntentConfirmed = async (paymentIntentConfirmation: PaymentIntentResult) => {
|
||||
// Reset payment intent secret to ensure another attempt works as expected
|
||||
setPaymentIntentSecret('')
|
||||
|
||||
if (paymentIntentConfirmation.paymentIntent?.status === 'succeeded') {
|
||||
await confirmPendingSubscriptionChange({
|
||||
payment_intent_id: paymentIntentConfirmation.paymentIntent.id,
|
||||
name: formState.name,
|
||||
kind: formState.kind,
|
||||
size: formState.size,
|
||||
})
|
||||
} else {
|
||||
// If the payment intent is not successful, we reset the payment method and show an error
|
||||
toast.error(`Could not confirm payment. Please try again or use a different card.`)
|
||||
resetPaymentMethod()
|
||||
setNewOrgLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onOrganizationCreated = (org: { slug: string }) => {
|
||||
const prefilledProjectName = user.profile?.username
|
||||
? user.profile.username + `'s Project`
|
||||
: 'My Project'
|
||||
|
||||
if (searchParams.returnTo && searchParams.auth_id) {
|
||||
router.push(`${searchParams.returnTo}?auth_id=${searchParams.auth_id}`, undefined, {
|
||||
shallow: false,
|
||||
})
|
||||
} else {
|
||||
router.push(`/new/${org.slug}?projectName=${prefilledProjectName}`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateOrgName(name: any) {
|
||||
const value = name ? name.trim() : ''
|
||||
return value.length >= 1
|
||||
}
|
||||
|
||||
const stripeOptionsConfirm = useMemo(() => {
|
||||
return {
|
||||
clientSecret: paymentIntentSecret,
|
||||
appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' },
|
||||
} as StripeElementsOptions
|
||||
}, [paymentIntentSecret, resolvedTheme])
|
||||
|
||||
async function createOrg(paymentMethodId?: string) {
|
||||
const dbTier = formState.plan === 'PRO' && !formState.spend_cap ? 'PAYG' : formState.plan
|
||||
|
||||
@@ -201,35 +264,21 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!stripe || !elements) {
|
||||
return console.error('Stripe.js has not loaded')
|
||||
}
|
||||
const paymentRef = useRef<{ createPaymentMethod: () => Promise<any> }>(null)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setNewOrgLoading(true)
|
||||
|
||||
if (formState.plan === 'FREE') {
|
||||
await createOrg()
|
||||
} else if (!paymentMethod) {
|
||||
const { error, setupIntent } = await stripe.confirmSetup({
|
||||
elements,
|
||||
redirect: 'if_required',
|
||||
confirmParams: {
|
||||
return_url: `${getURL()}/new`,
|
||||
expand: ['payment_method'],
|
||||
},
|
||||
})
|
||||
|
||||
if (error || !setupIntent.payment_method) {
|
||||
toast.error(error?.message ?? ' Failed to save card details')
|
||||
const paymentMethod = await paymentRef.current?.createPaymentMethod()
|
||||
if (paymentMethod) {
|
||||
setPaymentMethod(paymentMethod)
|
||||
createOrg(paymentMethod.id)
|
||||
} else {
|
||||
setNewOrgLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const paymentMethodFromSetup = setupIntent.payment_method as PaymentMethod
|
||||
|
||||
setPaymentMethod(paymentMethodFromSetup)
|
||||
createOrg(paymentMethodFromSetup.id)
|
||||
} else {
|
||||
createOrg(paymentMethod.id)
|
||||
}
|
||||
@@ -269,7 +318,7 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
<div key="panel-footer" className="flex w-full items-center justify-between">
|
||||
<Button
|
||||
type="default"
|
||||
disabled={newOrgLoading}
|
||||
disabled={newOrgLoading || paymentConfirmationLoading}
|
||||
onClick={() => {
|
||||
if (!!lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`)
|
||||
else router.push('/organizations')
|
||||
@@ -416,7 +465,10 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
<div className="col-span-2">
|
||||
<Select_Shadcn_
|
||||
value={formState.plan}
|
||||
onValueChange={(value) => updateForm('plan', value)}
|
||||
onValueChange={(value) => {
|
||||
updateForm('plan', value)
|
||||
onPlanSelected(value)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger_Shadcn_ id="plan" className="w-full">
|
||||
<SelectValue_Shadcn_ />
|
||||
@@ -496,41 +548,16 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
</Panel.Content>
|
||||
)}
|
||||
|
||||
{formState.plan !== 'FREE' && (
|
||||
{setupIntent && formState.plan !== 'FREE' && (
|
||||
<Panel.Content>
|
||||
{paymentMethod?.card !== undefined ? (
|
||||
<div key={paymentMethod.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-8">
|
||||
<img
|
||||
alt="Card"
|
||||
src={`${BASE_PATH}/img/payment-methods/${paymentMethod.card.brand
|
||||
.replace(' ', '-')
|
||||
.toLowerCase()}.png`}
|
||||
width="32"
|
||||
/>
|
||||
<Input
|
||||
readOnly
|
||||
className="w-64"
|
||||
size="small"
|
||||
value={`•••• •••• •••• ${paymentMethod.card.last4}`}
|
||||
/>
|
||||
<p className="text-sm tabular-nums">
|
||||
Expires: {paymentMethod.card.exp_month}/{paymentMethod.card.exp_year}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<Edit2 />}
|
||||
onClick={() => resetPaymentMethod()}
|
||||
disabled={newOrgLoading}
|
||||
className="hover:border-muted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PaymentElement />
|
||||
)}
|
||||
<Elements stripe={stripePromise} options={stripeOptionsPaymentMethod}>
|
||||
<Payment
|
||||
ref={paymentRef}
|
||||
pending_subscription_flow_enabled_for_creation={
|
||||
setupIntent?.pending_subscription_flow_enabled_for_creation === true
|
||||
}
|
||||
/>
|
||||
</Elements>
|
||||
</Panel.Content>
|
||||
)}
|
||||
</Panel>
|
||||
@@ -606,8 +633,88 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
})}
|
||||
</ul>
|
||||
</ConfirmationModal>
|
||||
|
||||
{stripePromise && paymentIntentSecret && paymentMethod && (
|
||||
<Elements stripe={stripePromise} options={stripeOptionsConfirm}>
|
||||
<PaymentConfirmation
|
||||
paymentIntentSecret={paymentIntentSecret}
|
||||
onPaymentIntentConfirm={(paymentIntentConfirmation) =>
|
||||
paymentIntentConfirmed(paymentIntentConfirmation)
|
||||
}
|
||||
onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)}
|
||||
paymentMethodId={paymentMethod.id}
|
||||
onError={(err) => {
|
||||
toast.error(`Failed to confirm payment: ${err.message}`)
|
||||
setNewOrgLoading(false)
|
||||
resetPaymentMethod()
|
||||
}}
|
||||
/>
|
||||
</Elements>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewOrgForm
|
||||
|
||||
/**
|
||||
* Set up as a separate component, as we need any component using stripe/elements to be wrapped in Elements.
|
||||
*
|
||||
* If Elements is on a higher level, we risk losing all form state in case a payment fails.
|
||||
*/
|
||||
const Payment = forwardRef(
|
||||
(
|
||||
{
|
||||
pending_subscription_flow_enabled_for_creation,
|
||||
}: { pending_subscription_flow_enabled_for_creation: boolean },
|
||||
ref
|
||||
) => {
|
||||
const stripe = useStripe()
|
||||
const elements = useElements()
|
||||
|
||||
const createPaymentMethod = async () => {
|
||||
if (!stripe || !elements) return
|
||||
await elements.submit()
|
||||
|
||||
if (pending_subscription_flow_enabled_for_creation) {
|
||||
// To avoid double 3DS confirmation, we just create the payment method here, as there might be a confirmation step while doing the actual payment
|
||||
const { error, paymentMethod } = await stripe.createPaymentMethod({
|
||||
elements,
|
||||
})
|
||||
if (error || paymentMethod == null) {
|
||||
toast.error(error?.message ?? ' Failed to process card details')
|
||||
return
|
||||
}
|
||||
return paymentMethod
|
||||
} else {
|
||||
const { error, setupIntent } = await stripe.confirmSetup({
|
||||
elements,
|
||||
redirect: 'if_required',
|
||||
confirmParams: {
|
||||
return_url: `${getURL()}/new`,
|
||||
expand: ['payment_method'],
|
||||
},
|
||||
})
|
||||
|
||||
if (error || !setupIntent.payment_method) {
|
||||
toast.error(error?.message ?? ' Failed to save card details')
|
||||
return
|
||||
}
|
||||
|
||||
return setupIntent.payment_method as PaymentMethod
|
||||
}
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
createPaymentMethod,
|
||||
}))
|
||||
|
||||
return (
|
||||
<Panel.Content>
|
||||
<PaymentElement />
|
||||
</Panel.Content>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Payment.displayName = 'Payment'
|
||||
@@ -62,21 +62,24 @@ export const useOrganizationCreateMutation = ({
|
||||
(vars) => createOrganization(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
// [Joshen] We're manually updating the query client here as the org's subscription is
|
||||
// created async, and the invalidation will happen too quick where the GET organizations
|
||||
// endpoint will error out with a 500 since the subscription isn't created yet.
|
||||
queryClient.setQueriesData(
|
||||
{
|
||||
queryKey: organizationKeys.list(),
|
||||
exact: true,
|
||||
},
|
||||
(prev: any) => {
|
||||
if (!prev) return prev
|
||||
return [...prev, castOrganizationResponseToOrganization(data)]
|
||||
}
|
||||
)
|
||||
if (data && !('pending_payment_intent_secret' in data)) {
|
||||
// [Joshen] We're manually updating the query client here as the org's subscription is
|
||||
// created async, and the invalidation will happen too quick where the GET organizations
|
||||
// endpoint will error out with a 500 since the subscription isn't created yet.
|
||||
queryClient.setQueriesData(
|
||||
{
|
||||
queryKey: organizationKeys.list(),
|
||||
exact: true,
|
||||
},
|
||||
(prev: any) => {
|
||||
if (!prev) return prev
|
||||
return [...prev, castOrganizationResponseToOrganization(data)]
|
||||
}
|
||||
)
|
||||
|
||||
await queryClient.invalidateQueries(permissionKeys.list())
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries(permissionKeys.list())
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -10,6 +10,7 @@ type OrganizationProjectClaimVariables = {
|
||||
}
|
||||
|
||||
async function claimOrganizationProject({ slug, token }: OrganizationProjectClaimVariables) {
|
||||
// @ts-expect-error the endpoint is hidden in the spec for now
|
||||
const { data, error } = await post('/v1/organizations/{slug}/project-claim/{token}', {
|
||||
params: { path: { slug, token } },
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { organizationKeys } from './keys'
|
||||
|
||||
import { components } from 'api-types'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
|
||||
@@ -10,8 +8,42 @@ type OrganizationProjectClaimVariables = {
|
||||
token: string
|
||||
}
|
||||
|
||||
export type OrganizationProjectClaimResponse =
|
||||
components['schemas']['OrganizationProjectClaimResponse']
|
||||
// todo replace with API type once exposed again - components['schemas']['OrganizationProjectClaimResponse']
|
||||
export type OrganizationProjectClaimResponse = {
|
||||
created_at: string
|
||||
/** Format: uuid */
|
||||
created_by: string
|
||||
expires_at: string
|
||||
preview: {
|
||||
errors: {
|
||||
key: string
|
||||
message: string
|
||||
}[]
|
||||
info: {
|
||||
key: string
|
||||
message: string
|
||||
}[]
|
||||
members_exceeding_free_project_limit: {
|
||||
limit: number
|
||||
name: string
|
||||
}[]
|
||||
/** @enum {string} */
|
||||
source_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise'
|
||||
target_organization_eligible: boolean | null
|
||||
target_organization_has_free_project_slots: boolean | null
|
||||
/** @enum {string|null} */
|
||||
target_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise' | null
|
||||
valid: boolean
|
||||
warnings: {
|
||||
key: string
|
||||
message: string
|
||||
}[]
|
||||
}
|
||||
project: {
|
||||
name: string
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
|
||||
async function getOrganizationProjectClaim(
|
||||
{ slug, token }: OrganizationProjectClaimVariables,
|
||||
@@ -19,13 +51,14 @@ async function getOrganizationProjectClaim(
|
||||
) {
|
||||
if (!slug || !token) throw new Error('Slug and token are required')
|
||||
|
||||
// @ts-expect-error the endpoint is hidden in the spec for now
|
||||
const { data, error } = await get(`/v1/organizations/{slug}/project-claim/{token}`, {
|
||||
params: { path: { slug, token } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
return data as OrganizationProjectClaimResponse
|
||||
}
|
||||
|
||||
export type OrganizationProjectClaimData = Awaited<ReturnType<typeof getOrganizationProjectClaim>>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { organizationKeys } from 'data/organizations/keys'
|
||||
import { permissionKeys } from 'data/permissions/keys'
|
||||
import { castOrganizationResponseToOrganization } from 'data/organizations/organizations-query'
|
||||
import type { components } from 'api-types'
|
||||
|
||||
export type PendingSubscriptionChangeVariables = {
|
||||
payment_intent_id: string
|
||||
name: string
|
||||
kind?: string
|
||||
size?: string
|
||||
}
|
||||
|
||||
export async function confirmPendingSubscriptionChange({
|
||||
payment_intent_id,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
}: PendingSubscriptionChangeVariables) {
|
||||
const { data, error } = await post('/platform/organizations/confirm-subscription', {
|
||||
body: {
|
||||
payment_intent_id,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
},
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type PendingSubscriptionChangeData = Awaited<ReturnType<typeof confirmPendingSubscriptionChange>>
|
||||
|
||||
export const useConfirmPendingSubscriptionChangeMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<
|
||||
PendingSubscriptionChangeData,
|
||||
ResponseError,
|
||||
PendingSubscriptionChangeVariables
|
||||
>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
PendingSubscriptionChangeData,
|
||||
ResponseError,
|
||||
PendingSubscriptionChangeVariables
|
||||
>((vars) => confirmPendingSubscriptionChange(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
// [Joshen] We're manually updating the query client here as the org's subscription is
|
||||
// created async, and the invalidation will happen too quick where the GET organizations
|
||||
// endpoint will error out with a 500 since the subscription isn't created yet.
|
||||
queryClient.setQueriesData(
|
||||
{
|
||||
queryKey: organizationKeys.list(),
|
||||
exact: true,
|
||||
},
|
||||
(prev: any) => {
|
||||
if (!prev) return prev
|
||||
return [
|
||||
...prev,
|
||||
castOrganizationResponseToOrganization(
|
||||
data as components['schemas']['OrganizationResponse']
|
||||
),
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
await queryClient.invalidateQueries(permissionKeys.list())
|
||||
|
||||
// todo replace plan in org
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to confirm payment: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { Elements } from '@stripe/react-stripe-js'
|
||||
import { loadStripe } from '@stripe/stripe-js'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { NewOrgForm } from 'components/interfaces/Organization'
|
||||
@@ -9,24 +6,21 @@ import AppLayout from 'components/layouts/AppLayout/AppLayout'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import WizardLayout from 'components/layouts/WizardLayout'
|
||||
import { SetupIntentResponse, useSetupIntent } from 'data/stripe/setup-intent-mutation'
|
||||
import { STRIPE_PUBLIC_KEY } from 'lib/constants'
|
||||
import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
|
||||
|
||||
/**
|
||||
* No org selected yet, create a new one
|
||||
*/
|
||||
const Wizard: NextPageWithLayout = () => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
const [intent, setIntent] = useState<SetupIntentResponse>()
|
||||
const captchaLoaded = useIsHCaptchaLoaded()
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const [captchaRef, setCaptchaRef] = useState<HCaptcha | null>(null)
|
||||
|
||||
const [selectedPlan, setSelectedPlan] = useState<string | null>(null)
|
||||
|
||||
const { mutate: setupIntent } = useSetupIntent({ onSuccess: (res) => setIntent(res) })
|
||||
|
||||
const captchaRefCallback = useCallback((node: any) => {
|
||||
@@ -37,16 +31,15 @@ const Wizard: NextPageWithLayout = () => {
|
||||
if (!hcaptchaToken) return console.error('Hcaptcha token is required')
|
||||
|
||||
// Force a reload of Elements, necessary for Stripe
|
||||
// Also mitigates card testing to some extent as we generate a new captcha token
|
||||
setIntent(undefined)
|
||||
setupIntent({ hcaptchaToken })
|
||||
}
|
||||
|
||||
const options = {
|
||||
clientSecret: intent ? intent.client_secret : '',
|
||||
appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' },
|
||||
} as const
|
||||
const loadPaymentForm = async (force = false) => {
|
||||
if (selectedPlan == null || selectedPlan === 'FREE') return
|
||||
if (intent != null && !force) return
|
||||
|
||||
const loadPaymentForm = async () => {
|
||||
if (captchaRef && captchaLoaded) {
|
||||
let token = captchaToken
|
||||
|
||||
@@ -66,10 +59,11 @@ const Wizard: NextPageWithLayout = () => {
|
||||
|
||||
useEffect(() => {
|
||||
loadPaymentForm()
|
||||
}, [captchaRef, captchaLoaded])
|
||||
}, [captchaRef, captchaLoaded, selectedPlan])
|
||||
|
||||
const resetSetupIntent = () => {
|
||||
return loadPaymentForm()
|
||||
setIntent(undefined)
|
||||
return loadPaymentForm(true)
|
||||
}
|
||||
|
||||
const onLocalCancel = () => {
|
||||
@@ -96,11 +90,11 @@ const Wizard: NextPageWithLayout = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{intent && (
|
||||
<Elements stripe={stripePromise} options={options}>
|
||||
<NewOrgForm onPaymentMethodReset={() => resetSetupIntent()} />
|
||||
</Elements>
|
||||
)}
|
||||
<NewOrgForm
|
||||
setupIntent={intent}
|
||||
onPaymentMethodReset={() => resetSetupIntent()}
|
||||
onPlanSelected={(plan) => setSelectedPlan(plan)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Vendored
+10
-229
@@ -218,24 +218,6 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/v1/organizations/{slug}/project-claim/{token}': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
/** Gets project details for the specified organization and claim token */
|
||||
get: operations['v1-get-organization-project-claim']
|
||||
put?: never
|
||||
/** Claims project for the specified organization */
|
||||
post: operations['v1-claim-project-for-organization']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/v1/projects': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -386,7 +368,7 @@ export interface paths {
|
||||
/** Get project api keys */
|
||||
get: operations['v1-get-project-api-keys']
|
||||
put?: never
|
||||
/** [Alpha] Creates a new API key for the project */
|
||||
/** [Beta] Creates a new API key for the project */
|
||||
post: operations['createApiKey']
|
||||
delete?: never
|
||||
options?: never
|
||||
@@ -401,15 +383,15 @@ export interface paths {
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
/** [Alpha] Get API key */
|
||||
/** [Beta] Get API key */
|
||||
get: operations['getApiKey']
|
||||
put?: never
|
||||
post?: never
|
||||
/** [Alpha] Deletes an API key for the project */
|
||||
/** [Beta] Deletes an API key for the project */
|
||||
delete: operations['deleteApiKey']
|
||||
options?: never
|
||||
head?: never
|
||||
/** [Alpha] Updates an API key for the project */
|
||||
/** [Beta] Updates an API key for the project */
|
||||
patch: operations['updateApiKey']
|
||||
trace?: never
|
||||
}
|
||||
@@ -476,25 +458,6 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/v1/projects/{ref}/claim-token': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
/** Gets project claim token */
|
||||
get: operations['v1-get-project-claim-token']
|
||||
put?: never
|
||||
/** Creates project claim token */
|
||||
post: operations['v1-create-project-claim-token']
|
||||
/** Revokes project claim token */
|
||||
delete: operations['v1-delete-project-claim-token']
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/v1/projects/{ref}/config/auth': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -1606,6 +1569,9 @@ export interface components {
|
||||
external_zoom_client_id: string | null
|
||||
external_zoom_enabled: boolean | null
|
||||
external_zoom_secret: string | null
|
||||
hook_before_user_created_enabled: boolean | null
|
||||
hook_before_user_created_secrets: string | null
|
||||
hook_before_user_created_uri: string | null
|
||||
hook_custom_access_token_enabled: boolean | null
|
||||
hook_custom_access_token_secrets: string | null
|
||||
hook_custom_access_token_uri: string | null
|
||||
@@ -1865,14 +1831,6 @@ export interface components {
|
||||
CreateOrganizationV1: {
|
||||
name: string
|
||||
}
|
||||
CreateProjectClaimTokenResponse: {
|
||||
created_at: string
|
||||
/** Format: uuid */
|
||||
created_by: string
|
||||
expires_at: string
|
||||
token: string
|
||||
token_alias: string
|
||||
}
|
||||
CreateProviderBody: {
|
||||
attribute_mapping?: {
|
||||
keys: {
|
||||
@@ -2292,41 +2250,6 @@ export interface components {
|
||||
/** @enum {string} */
|
||||
token_type: 'Bearer'
|
||||
}
|
||||
OrganizationProjectClaimResponse: {
|
||||
created_at: string
|
||||
/** Format: uuid */
|
||||
created_by: string
|
||||
expires_at: string
|
||||
preview: {
|
||||
errors: {
|
||||
key: string
|
||||
message: string
|
||||
}[]
|
||||
info: {
|
||||
key: string
|
||||
message: string
|
||||
}[]
|
||||
members_exceeding_free_project_limit: {
|
||||
limit: number
|
||||
name: string
|
||||
}[]
|
||||
/** @enum {string} */
|
||||
source_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise'
|
||||
target_organization_eligible: boolean | null
|
||||
target_organization_has_free_project_slots: boolean | null
|
||||
/** @enum {string|null} */
|
||||
target_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise' | null
|
||||
valid: boolean
|
||||
warnings: {
|
||||
key: string
|
||||
message: string
|
||||
}[]
|
||||
}
|
||||
project: {
|
||||
name: string
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
OrganizationResponseV1: {
|
||||
id: string
|
||||
name: string
|
||||
@@ -2368,13 +2291,6 @@ export interface components {
|
||||
jwt_secret?: string
|
||||
max_rows: number
|
||||
}
|
||||
ProjectClaimTokenResponse: {
|
||||
created_at: string
|
||||
/** Format: uuid */
|
||||
created_by: string
|
||||
expires_at: string
|
||||
token_alias: string
|
||||
}
|
||||
ProjectUpgradeEligibilityResponse: {
|
||||
current_app_version: string
|
||||
/** @enum {string} */
|
||||
@@ -2668,6 +2584,9 @@ export interface components {
|
||||
external_zoom_client_id?: string | null
|
||||
external_zoom_enabled?: boolean | null
|
||||
external_zoom_secret?: string | null
|
||||
hook_before_user_created_enabled?: boolean | null
|
||||
hook_before_user_created_secrets?: string | null
|
||||
hook_before_user_created_uri?: string | null
|
||||
hook_custom_access_token_enabled?: boolean | null
|
||||
hook_custom_access_token_secrets?: string | null
|
||||
hook_custom_access_token_uri?: string | null
|
||||
@@ -3676,62 +3595,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
'v1-get-organization-project-claim': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
/** @description Organization slug */
|
||||
slug: string
|
||||
token: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['OrganizationProjectClaimResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
'v1-claim-project-for-organization': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
/** @description Organization slug */
|
||||
slug: string
|
||||
token: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
'v1-list-all-projects': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -4370,88 +4233,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
'v1-get-project-claim-token': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ProjectClaimTokenResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
'v1-create-project-claim-token': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateProjectClaimTokenResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
'v1-delete-project-claim-token': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
'v1-get-auth-service-config': {
|
||||
parameters: {
|
||||
query?: never
|
||||
|
||||
+89
-6
@@ -1496,6 +1496,23 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/platform/organizations/confirm-subscription': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/** Confirm subscription change and apply pending changes */
|
||||
post: operations['OrganizationsController_confirmSubscription']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/platform/organizations/fly/{fly_organization_id}': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -4234,6 +4251,12 @@ export interface components {
|
||||
/** @default 0 */
|
||||
recoveryTimeTarget?: number
|
||||
}
|
||||
ConfirmSubscriptionChangeBody: {
|
||||
kind?: string
|
||||
name: string
|
||||
payment_intent_id: string
|
||||
size?: string
|
||||
}
|
||||
CopyObjectBody: {
|
||||
from: string
|
||||
to: string
|
||||
@@ -4521,6 +4544,31 @@ export interface components {
|
||||
/** @enum {string} */
|
||||
tier: 'tier_free' | 'tier_pro' | 'tier_payg' | 'tier_team' | 'tier_enterprise'
|
||||
}
|
||||
CreateOrganizationResponse:
|
||||
| {
|
||||
pending_payment_intent_secret: string | null
|
||||
}
|
||||
| {
|
||||
billing_email: string | null
|
||||
id: number
|
||||
is_owner: boolean
|
||||
name: string
|
||||
opt_in_tags: string[]
|
||||
plan: {
|
||||
/** @enum {string} */
|
||||
id: 'free' | 'pro' | 'team' | 'enterprise'
|
||||
name: string
|
||||
}
|
||||
restriction_data: {
|
||||
[key: string]: string
|
||||
} | null
|
||||
/** @enum {string|null} */
|
||||
restriction_status: 'grace_period' | 'grace_period_over' | 'restricted' | null
|
||||
slug: string
|
||||
stripe_customer_id: string | null
|
||||
subscription_id: string | null
|
||||
usage_billing_enabled: boolean
|
||||
}
|
||||
CreatePipelineResponse: {
|
||||
id: number
|
||||
}
|
||||
@@ -5622,6 +5670,9 @@ export interface components {
|
||||
EXTERNAL_ZOOM_CLIENT_ID: string
|
||||
EXTERNAL_ZOOM_ENABLED: boolean
|
||||
EXTERNAL_ZOOM_SECRET: string
|
||||
HOOK_BEFORE_USER_CREATED_ENABLED: boolean
|
||||
HOOK_BEFORE_USER_CREATED_SECRETS: string
|
||||
HOOK_BEFORE_USER_CREATED_URI: string
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: boolean
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: string
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_URI: string
|
||||
@@ -7280,11 +7331,7 @@ export interface components {
|
||||
}
|
||||
SetupIntentResponse: {
|
||||
client_secret: string
|
||||
payment_method:
|
||||
| string
|
||||
| {
|
||||
[key: string]: unknown
|
||||
}
|
||||
pending_subscription_flow_enabled_for_creation: boolean
|
||||
}
|
||||
SignedUrlResponse: {
|
||||
signedUrl: string
|
||||
@@ -7819,6 +7866,9 @@ export interface components {
|
||||
EXTERNAL_ZOOM_CLIENT_ID?: string | null
|
||||
EXTERNAL_ZOOM_ENABLED?: boolean | null
|
||||
EXTERNAL_ZOOM_SECRET?: string | null
|
||||
HOOK_BEFORE_USER_CREATED_ENABLED?: boolean | null
|
||||
HOOK_BEFORE_USER_CREATED_SECRETS?: string | null
|
||||
HOOK_BEFORE_USER_CREATED_URI?: string | null
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_ENABLED?: boolean | null
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_SECRETS?: string | null
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_URI?: string | null
|
||||
@@ -7929,6 +7979,9 @@ export interface components {
|
||||
URI_ALLOW_LIST?: string | null
|
||||
}
|
||||
UpdateGoTrueConfigHooksBody: {
|
||||
HOOK_BEFORE_USER_CREATED_ENABLED?: boolean | null
|
||||
HOOK_BEFORE_USER_CREATED_SECRETS?: string | null
|
||||
HOOK_BEFORE_USER_CREATED_URI?: string | null
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_ENABLED?: boolean | null
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_SECRETS?: string | null
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_URI?: string | null
|
||||
@@ -10151,7 +10204,7 @@ export interface operations {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['OrganizationResponse']
|
||||
'application/json': components['schemas']['CreateOrganizationResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unexpected error creating an organization */
|
||||
@@ -11984,6 +12037,36 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
OrganizationsController_confirmSubscription: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ConfirmSubscriptionChangeBody']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateOrganizationResponse']
|
||||
}
|
||||
}
|
||||
/** @description Failed to confirm subscription changes */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
OrganizationsController_getOrganizationByFlyOrganizationId: {
|
||||
parameters: {
|
||||
query?: never
|
||||
|
||||
Reference in new issue
Block a user