diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index c37f477861c..6cc226b233d 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -7,7 +7,6 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { useParams } from 'common' import SpendCapModal from 'components/interfaces/Billing/SpendCapModal' import Panel from 'components/ui/Panel' import { useOrganizationCreateMutation } from 'data/organizations/organization-create-mutation' @@ -15,9 +14,10 @@ import { invalidateOrganizationsQuery, useOrganizationsQuery, } from 'data/organizations/organizations-query' -import { useProfile } from 'lib/profile' import { BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants' import { getURL } from 'lib/helpers' +import { useProfile } from 'lib/profile' +import { parseAsBoolean, parseAsString, parseAsStringLiteral, useQueryStates } from 'nuqs' import { Button, Input, Listbox, Toggle } from 'ui' const ORG_KIND_TYPES = { @@ -53,50 +53,50 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { const stripe = useStripe() const elements = useElements() const queryClient = useQueryClient() - const { plan, name, kind, size, spend_cap } = useParams() + const [state, setState] = useQueryStates({ + plan: parseAsStringLiteral(Object.keys(PRICING_TIER_LABELS_ORG)).withDefault('FREE'), + name: parseAsString.withDefault(''), + kind: parseAsStringLiteral(Object.keys(ORG_KIND_TYPES)).withDefault(ORG_KIND_DEFAULT), + size: parseAsString.withDefault(ORG_SIZE_DEFAULT), + spend_cap: parseAsBoolean.withDefault(true), + returnTo: parseAsString.withDefault(''), + auth_id: parseAsString.withDefault(''), + }) + const { + name: orgName, + kind: orgKind, + spend_cap: isSpendCapEnabled, + plan: dbPricingTierKey, + size: orgSize, + returnTo, + auth_id, + } = state - const [orgName, setOrgName] = useState(name || '') - const [orgKind, setOrgKind] = useState( - kind && Object.keys(ORG_KIND_TYPES).includes(kind) ? kind : ORG_KIND_DEFAULT - ) - const [orgSize, setOrgSize] = useState(size || ORG_SIZE_DEFAULT) // [Joshen] Separate loading state here as there's 2 async processes const [newOrgLoading, setNewOrgLoading] = useState(false) const [paymentMethod, setPaymentMethod] = useState() - // URL param support for passing plan - const [dbPricingTierKey, setDbPricingTierKey] = useState( - plan && ['free', 'team', 'pro'].includes(plan) ? plan.toUpperCase() : 'FREE' - ) - const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false) - const [isSpendCapEnabled, setIsSpendCapEnabled] = useState(spend_cap ? Boolean(spend_cap) : true) - - useEffect(() => { - const query: Record = {} - query.plan = dbPricingTierKey.toLowerCase() - if (orgName) query.name = orgName - if (orgKind) query.kind = orgKind - if (orgSize) query.size = orgSize - if (isSpendCapEnabled) query.spend_cap = isSpendCapEnabled.toString() - - router.push({ query }) - }, [dbPricingTierKey, orgName, orgKind, orgSize, isSpendCapEnabled]) useEffect(() => { if (!orgName && organizations?.length === 0 && !user.isLoading) { const prefilledOrgName = user.profile?.username ? user.profile.username + `'s Org` : 'My Org' - setOrgName(prefilledOrgName) + setState({ ...state, name: prefilledOrgName }) } }, [isSuccess]) const { mutate: createOrganization } = useOrganizationCreateMutation({ - onSuccess: async (org: any) => { + onSuccess: async (org) => { await invalidateOrganizationsQuery(queryClient) const prefilledProjectName = user.profile?.username ? user.profile.username + `'s Project` : 'My Project' - router.push(`/new/${org.slug}?projectName=${prefilledProjectName}`) + + if (returnTo && auth_id) { + router.push(`${returnTo}?auth_id=${auth_id}`, undefined, { shallow: false }) + } else { + router.push(`/new/${org.slug}?projectName=${prefilledProjectName}`) + } }, onError: () => { resetPaymentMethod() @@ -110,19 +110,19 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { } function onOrgNameChange(e: any) { - setOrgName(e.target.value) + setState({ ...state, name: e.target.value }) } function onOrgKindChange(value: any) { - setOrgKind(value) + setState({ ...state, kind: value }) } function onOrgSizeChange(value: any) { - setOrgSize(value) + setState({ ...state, size: value }) } function onDbPricingPlanChange(value: string) { - setDbPricingTierKey(value) + setState({ ...state, plan: value as any }) } async function createOrg(paymentMethodId?: string) { @@ -341,7 +341,7 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { } checked={isSpendCapEnabled} - onChange={() => setIsSpendCapEnabled(!isSpendCapEnabled)} + onChange={() => setState({ ...state, spend_cap: !isSpendCapEnabled })} descriptionText={

diff --git a/apps/studio/data/stripe/setup-intent-mutation.ts b/apps/studio/data/stripe/setup-intent-mutation.ts index 58c2ff859c8..fe4bb833e84 100644 --- a/apps/studio/data/stripe/setup-intent-mutation.ts +++ b/apps/studio/data/stripe/setup-intent-mutation.ts @@ -1,6 +1,7 @@ import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { toast } from 'sonner' +import { components } from 'api-types' import { handleError, post } from 'data/fetchers' import type { ResponseError } from 'types' @@ -8,6 +9,8 @@ export type SetupIntentVariables = { hcaptchaToken: string } +export type SetupIntentResponse = components['schemas']['SetupIntentResponse'] + export async function setupIntent({ hcaptchaToken }: SetupIntentVariables) { const { data, error } = await post('/platform/stripe/setup-intent', { body: { hcaptchaToken }, diff --git a/apps/studio/pages/authorize.tsx b/apps/studio/pages/authorize.tsx index 0cb3e37402c..401ab899e8a 100644 --- a/apps/studio/pages/authorize.tsx +++ b/apps/studio/pages/authorize.tsx @@ -1,5 +1,7 @@ import { useParams } from 'common' import dayjs from 'dayjs' +import { AlertCircle } from 'lucide-react' +import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { toast } from 'sonner' @@ -14,7 +16,14 @@ import { useApiAuthorizationQuery } from 'data/api-authorization/api-authorizati import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { withAuth } from 'hooks/misc/withAuth' import type { NextPageWithLayout } from 'types' -import { Alert, Button, Listbox } from 'ui' +import { + Alert, + Alert_Shadcn_, + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Button, + Listbox, +} from 'ui' // Need to handle if no organizations in account // Need to handle if not logged in yet state @@ -141,6 +150,15 @@ const APIAuthorizationPage: NextPageWithLayout = () => { ) } + const searchParams = new URLSearchParams(location.search) + let pathname = location.pathname + const basePath = process.env.NEXT_PUBLIC_BASE_PATH + if (basePath) { + pathname = pathname.replace(basePath, '') + } + + searchParams.set('returnTo', pathname) + return ( Authorize API access for {requester?.name}

} @@ -155,13 +173,21 @@ const APIAuthorizationPage: NextPageWithLayout = () => { > Decline - + {isLoadingOrganizations ? ( + + ) : isSuccessOrganizations && organizations.length === 0 ? ( + + + + ) : ( + + )}
} @@ -188,6 +214,18 @@ const APIAuthorizationPage: NextPageWithLayout = () => { + ) : organizations?.length === 0 ? ( + + + + Organization is needed for installing an integration + + + Your account isn't associated with any organizations. To use this integration, it must + be installed within an organization. You'll be redirected to create an organization + first. + + ) : ( { const { resolvedTheme } = useTheme() - const [intent, setIntent] = useState() + const [intent, setIntent] = useState() const captchaLoaded = useIsHCaptchaLoaded() const [captchaToken, setCaptchaToken] = useState(null) @@ -42,7 +42,7 @@ const Wizard: NextPageWithLayout = () => { const options = { clientSecret: intent ? intent.client_secret : '', appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' }, - } as any + } as const const loadPaymentForm = async () => { if (captchaRef && captchaLoaded) {