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 && (
} disabled={isLoading || isError}>
-
- {cta.message || `View ${resource} on ${PARTNER_TO_NAME[partner]}`}
+
+ {cta.message || `View ${resource} on ${PARTNER_TO_NAME[managedBy]}`}
)}
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 ? (