diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx index 699e4b85330..0195c78d2e9 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx @@ -16,10 +16,14 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import UpcomingInvoice from './UpcomingInvoice' +import { MANAGED_BY } from 'lib/constants/infrastructure' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' const BillingBreakdown = () => { const { slug: orgSlug } = useParams() + const { data: selectedOrganization } = useSelectedOrganizationQuery() + const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') @@ -44,26 +48,43 @@ const BillingBreakdown = () => {

Upcoming Invoice

- + {selectedOrganization?.managed_by !== MANAGED_BY.AWS_MARKETPLACE && ( + + )}

- Your upcoming invoice (excluding credits) will continue to update until the end of your - billing cycle on {billingCycleEnd.format('MMMM DD')}. For a more detailed breakdown, - visit the usage page. + {selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE ? ( + <> + You'll receive two invoices from AWS Marketplace: one on the 3rd of{' '} + {billingCycleEnd.format('MMMM')} for your usage in{' '} + {billingCycleStart.format('MMMM')} and one on {billingCycleEnd.format('MMMM DD')}{' '} + for the fixed subscription fee. + + ) : ( + <> + Your upcoming invoice (excluding credits) will continue to update until the end of + your billing cycle on {billingCycleEnd.format('MMMM DD')}. + + )} + <> + {' '} + For a more detailed breakdown, visit the{' '} + usage page. +


diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx index a137720baa9..54d50a87d77 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx @@ -118,7 +118,7 @@ export const BillingCustomerData = () => { {selectedOrganization?.managed_by !== undefined && selectedOrganization?.managed_by !== 'supabase' ? ( { const { slug } = useParams() const { resolvedTheme } = useTheme() + const { data: selectedOrganization } = useSelectedOrganizationQuery() const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') @@ -47,6 +53,8 @@ const CostControl = ({}: CostControlProps) => { const canChangeTier = !projectUpdateDisabled && !['team', 'enterprise'].includes(currentPlan?.id || '') + const costControlDisabled = selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE + return ( <> @@ -97,7 +105,22 @@ const CostControl = ({}: CostControlProps) => { {isError && } - {isSuccess && ( + {isSuccess && costControlDisabled && ( + + + + + The Spend Cap is not available for organizations managed by{' '} + {PARTNER_TO_NAME[selectedOrganization?.managed_by]}. + + + )} + + {isSuccess && !costControlDisabled && (

{['team', 'enterprise'].includes(currentPlan?.id || '') ? ( { const { slug } = useParams() @@ -66,9 +67,9 @@ const PaymentMethods = () => { {selectedOrganization?.managed_by !== undefined && - selectedOrganization?.managed_by !== 'supabase' ? ( + selectedOrganization?.managed_by !== MANAGED_BY.SUPABASE ? ( { + if (selectedOrganization.managed_by === MANAGED_BY.VERCEL_MARKETPLACE) { + return { + installationId: selectedOrganization?.partner_id, + path: '/settings', + message: 'Change Plan on Vercel Marketplace', + } + } + if (selectedOrganization.managed_by === MANAGED_BY.AWS_MARKETPLACE) { + return { + organizationSlug: selectedOrganization?.slug, + } + } +} const PlanUpdateSidePanel = () => { const router = useRouter() const { slug } = useParams() @@ -144,16 +160,11 @@ const PlanUpdateSidePanel = () => {
} > - {selectedOrganization?.managed_by === 'vercel-marketplace' && ( + {selectedOrganization && selectedOrganization.managed_by !== MANAGED_BY.SUPABASE && ( )} @@ -216,8 +227,10 @@ const PlanUpdateSidePanel = () => { disabled={ subscription?.plan?.id === 'enterprise' || // Downgrades to free are still allowed through the dashboard given we have much better control about showing customers the impact + any possible issues with downgrading to free - (selectedOrganization?.managed_by !== 'supabase' && + (selectedOrganization?.managed_by !== MANAGED_BY.SUPABASE && plan.id !== 'tier_free') || + // Orgs managed by AWS marketplace are not allowed to change the plan + selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE || hasOrioleProjects || !canUpdateSubscription } @@ -243,7 +256,10 @@ const PlanUpdateSidePanel = () => { ? 'Your organization has projects that are using the OrioleDB extension which is only available on the Free plan. Remove all OrioleDB projects before changing your plan.' : !canUpdateSubscription ? 'You do not have permission to change the subscription plan' - : undefined, + : selectedOrganization?.managed_by === + MANAGED_BY.AWS_MARKETPLACE + ? 'You cannot change the plan for an organization managed by AWS Marketplace' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx index c963d1006c5..04e6537c15c 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx @@ -3,6 +3,7 @@ import PartnerManagedResource from 'components/ui/PartnerManagedResource' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Admonition } from 'ui-patterns' import { DeleteOrganizationButton } from './DeleteOrganizationButton' +import { MANAGED_BY } from 'lib/constants/infrastructure' const OrganizationDeletePanel = () => { const { data: selectedOrganization } = useSelectedOrganizationQuery() @@ -20,7 +21,7 @@ const OrganizationDeletePanel = () => { ) : ( { + if (selectedOrganization.managed_by === MANAGED_BY.VERCEL_MARKETPLACE) { + return { + installationId: selectedOrganization?.partner_id, + path: '/invoices', + } + } + if (selectedOrganization.managed_by === MANAGED_BY.AWS_MARKETPLACE) { + return { + organizationSlug: selectedOrganization?.slug, + overrideUrl: 'https://console.aws.amazon.com/billing/home#/bills', + } + } +} const InvoicesSettings = () => { const [page, setPage] = useState(1) @@ -62,13 +78,9 @@ const InvoicesSettings = () => { ) { return ( ) } diff --git a/apps/studio/components/layouts/OrganizationLayout.tsx b/apps/studio/components/layouts/OrganizationLayout.tsx index a705d2c6d3f..ce196c37f72 100644 --- a/apps/studio/components/layouts/OrganizationLayout.tsx +++ b/apps/studio/components/layouts/OrganizationLayout.tsx @@ -7,12 +7,33 @@ import { useVercelRedirectQuery } from 'data/integrations/vercel-redirect-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { withAuth } from 'hooks/misc/withAuth' import { Alert_Shadcn_, AlertTitle_Shadcn_, Button, cn } from 'ui' +import { useAwsRedirectQuery } from 'data/integrations/aws-redirect-query' +import { MANAGED_BY } from 'lib/constants/infrastructure' const OrganizationLayoutContent = ({ children }: PropsWithChildren<{}>) => { const { data: selectedOrganization } = useSelectedOrganizationQuery() - const { data, isSuccess } = useVercelRedirectQuery({ - installationId: selectedOrganization?.partner_id, - }) + + const vercelQuery = useVercelRedirectQuery( + { + installationId: selectedOrganization?.partner_id, + }, + { + enabled: selectedOrganization?.managed_by === MANAGED_BY.VERCEL_MARKETPLACE, + } + ) + + const awsQuery = useAwsRedirectQuery( + { + organizationSlug: selectedOrganization?.slug, + }, + { + enabled: selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE, + } + ) + + // Select the appropriate query based on partner + const { data, isSuccess } = + selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE ? awsQuery : vercelQuery return (
diff --git a/apps/studio/components/ui/PartnerIcon.tsx b/apps/studio/components/ui/PartnerIcon.tsx index f4c3ff3fa03..acd3aa7b952 100644 --- a/apps/studio/components/ui/PartnerIcon.tsx +++ b/apps/studio/components/ui/PartnerIcon.tsx @@ -1,5 +1,7 @@ import { Organization } from 'types' import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { MANAGED_BY } from 'lib/constants/infrastructure' +import { PARTNER_TO_NAME } from './PartnerManagedResource' interface PartnerIconProps { organization: Pick @@ -8,27 +10,71 @@ interface PartnerIconProps { size?: 'small' | 'medium' | 'large' } +function getPartnerIcon( + organization: Pick, + size: 'small' | 'medium' | 'large' +) { + switch (organization.managed_by) { + case MANAGED_BY.VERCEL_MARKETPLACE: + return ( + + + + ) + case MANAGED_BY.AWS_MARKETPLACE: + return ( + + + + + + + + + + ) + default: + return null + } +} + function PartnerIcon({ organization, showTooltip = true, - tooltipText = 'This organization is managed by Vercel Marketplace.', + tooltipText, size = 'small', }: PartnerIconProps) { - if (organization.managed_by === 'vercel-marketplace') { - const icon = ( - - - - ) + if ( + organization.managed_by === MANAGED_BY.VERCEL_MARKETPLACE || + organization.managed_by === MANAGED_BY.AWS_MARKETPLACE + ) { + const icon = getPartnerIcon(organization, size) if (!showTooltip) { return ( @@ -45,6 +91,8 @@ function PartnerIcon({ ) } + const defaultTooltipText = `This organization is managed by ${PARTNER_TO_NAME[organization.managed_by]}` + return ( @@ -59,7 +107,7 @@ function PartnerIcon({ {icon}
- {tooltipText} + {tooltipText ?? defaultTooltipText} ) } diff --git a/apps/studio/components/ui/PartnerManagedResource.tsx b/apps/studio/components/ui/PartnerManagedResource.tsx index 2acd744ed9a..137a7b9b7ff 100644 --- a/apps/studio/components/ui/PartnerManagedResource.tsx +++ b/apps/studio/components/ui/PartnerManagedResource.tsx @@ -1,15 +1,18 @@ import { ExternalLink } from 'lucide-react' import { useVercelRedirectQuery } from 'data/integrations/vercel-redirect-query' +import { useAwsRedirectQuery } from 'data/integrations/aws-redirect-query' import { Alert_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui' import PartnerIcon from './PartnerIcon' import { MANAGED_BY, ManagedBy } from 'lib/constants/infrastructure' interface PartnerManagedResourceProps { - partner: ManagedBy + managedBy: ManagedBy resource: string cta?: { installationId?: string + organizationSlug?: string + overrideUrl?: string path?: string message?: string } @@ -21,35 +24,47 @@ export const PARTNER_TO_NAME = { [MANAGED_BY.SUPABASE]: 'Supabase', } as const -function PartnerManagedResource({ partner, resource, cta }: PartnerManagedResourceProps) { - const isManagedBySupabase = partner === MANAGED_BY.SUPABASE +function PartnerManagedResource({ managedBy, resource, cta }: PartnerManagedResourceProps) { const ctaEnabled = cta !== undefined - const { data, isLoading, isError } = useVercelRedirectQuery( + // Use appropriate redirect query based on partner + const vercelQuery = useVercelRedirectQuery( { installationId: cta?.installationId, }, { - enabled: ctaEnabled && !isManagedBySupabase, + enabled: ctaEnabled && managedBy === MANAGED_BY.VERCEL_MARKETPLACE, } ) - if (isManagedBySupabase) return null + const awsQuery = useAwsRedirectQuery( + { + organizationSlug: cta?.organizationSlug, + }, + { + enabled: ctaEnabled && managedBy === MANAGED_BY.AWS_MARKETPLACE, + } + ) + + if (managedBy === MANAGED_BY.SUPABASE) return null + + const { data, isLoading, isError } = + managedBy === MANAGED_BY.VERCEL_MARKETPLACE ? vercelQuery : awsQuery const ctaUrl = (data?.url ?? '') + (cta?.path ?? '') return ( - + - {resource} are managed by {PARTNER_TO_NAME[partner]}. + {resource} are managed by {PARTNER_TO_NAME[managedBy]}. {ctaEnabled && ( )} diff --git a/apps/studio/data/integrations/aws-redirect-query.ts b/apps/studio/data/integrations/aws-redirect-query.ts new file mode 100644 index 00000000000..e9a030cb258 --- /dev/null +++ b/apps/studio/data/integrations/aws-redirect-query.ts @@ -0,0 +1,38 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import type { ResponseError } from 'types' +import { integrationKeys } from './keys' +import { get, handleError } from 'data/fetchers' + +export type AwsRedirectVariables = { + organizationSlug?: string +} + +export async function getAwsRedirect( + { organizationSlug }: AwsRedirectVariables, + signal?: AbortSignal +) { + if (!organizationSlug) throw new Error('organizationSlug is required') + + const { data, error } = await get(`/platform/organizations/{slug}/cloud-marketplace/redirect`, { + params: { path: { slug: organizationSlug } }, + signal, + }) + if (error) handleError(error) + return data +} + +export type AwsRedirectData = Awaited> +export type AwsRedirectError = ResponseError + +export const useAwsRedirectQuery = ( + { organizationSlug }: AwsRedirectVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + integrationKeys.awsRedirect(organizationSlug), + ({ signal }) => getAwsRedirect({ organizationSlug }, signal), + { + enabled: enabled && typeof organizationSlug !== 'undefined', + ...options, + } + ) diff --git a/apps/studio/data/integrations/keys.ts b/apps/studio/data/integrations/keys.ts index 1a86cfdfa40..a90b6fbe3bd 100644 --- a/apps/studio/data/integrations/keys.ts +++ b/apps/studio/data/integrations/keys.ts @@ -25,4 +25,5 @@ export const integrationKeys = { githubConnectionsList: (organizationId: number | undefined) => ['organizations', organizationId, 'github-connections'] as const, vercelRedirect: (installationId?: string) => ['vercel-redirect', installationId] as const, + awsRedirect: (organizationSlug?: string) => ['aws-redirect', organizationSlug] as const, } diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 420312bb5a8..ff03c622b78 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -60,6 +60,7 @@ import { DEFAULT_MINIMUM_PASSWORD_STRENGTH, DEFAULT_PROVIDER, FLY_REGIONS_DEFAULT, + MANAGED_BY, PROJECT_STATUS, PROVIDERS, } from 'lib/constants' @@ -961,7 +962,7 @@ const Wizard: NextPageWithLayout = () => { ) : isManagedByVercel ? (