Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ breadcrumb: 'Migrations'

## Before you begin

<Admonition type="note">

Dashboard backups are only available for older projects that still use logical backups. Projects that use physical backups should follow steps in [Backup and Restore using the CLI](/docs/guides/platform/migrating-within-supabase/backup-restore).

</Admonition>

<Accordion
type="default"
openBehaviour="multiple"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import React, { ComponentProps, useEffect, useMemo, useState } from 'react'

import { DowngradeModal } from './DowngradeModal'
import { ExitSurveyModal } from './ExitSurveyModal'
import MembersExceedLimitModal from './MembersExceedLimitModal'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query'
import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query'
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { MANAGED_BY } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'

type CancellationFlowProps = {
onDowngrade?: () => void
onCancel?: () => void
visible: boolean
}

type CancellationFlowState =
| 'closed'
| 'show-downgrade-modal'
| 'show-downgrade-error'
| 'show-exit-survey'

export const CancellationFlow = (props: CancellationFlowProps) => {
const { slug } = useParams()

const [flowState, setFlowState] = useState<CancellationFlowState>(
props.visible ? 'show-downgrade-modal' : 'closed'
)
useEffect(() => {
setFlowState(props.visible ? 'show-downgrade-modal' : 'closed')
}, [props.visible])

const { data: membersExceededLimit, isLoading: projectLimitQueryLoading } =
useFreeProjectLimitCheckQuery({ slug }, { enabled: flowState !== 'closed' })

const { data: orgProjectsData, isLoading: orgProjectsQueryLoading } = useOrgProjectsInfiniteQuery(
{ slug },
{ enabled: flowState !== 'closed' }
)

const { data: subscription, isLoading: orgSubscriptionQueryLoading } = useOrgSubscriptionQuery({
orgSlug: slug,
})

const isLoading =
projectLimitQueryLoading || orgProjectsQueryLoading || orgSubscriptionQueryLoading

const orgProjects =
useMemo(
() => orgProjectsData?.pages.flatMap((page) => page.projects),
[orgProjectsData?.pages]
) || []

// [Joshen] Note that orgProjects is paginated so there's a chance this may omit certain projects
// Although I don't foresee this affecting a majority of users. Ideally perhaps we could return
// this data from the organization query
const hasRunningProjects =
orgProjects.filter((it) => it.status !== 'INACTIVE' && it.status !== 'GOING_DOWN').length > 0

const hasMembersExceedingFreeTierLimit =
(membersExceededLimit || []).length > 0 && hasRunningProjects

const onConfirmDowngrade = () => {
if (hasMembersExceedingFreeTierLimit) {
setFlowState('show-downgrade-error')
} else {
setFlowState('show-exit-survey')
}
}

const onCancelFlow = () => {
setFlowState('closed')
props.onCancel?.()
}

return (
<>
<DowngradeModal
visible={flowState === 'show-downgrade-modal'}
subscription={subscription}
onClose={onCancelFlow}
confirmDisabled={isLoading}
onConfirm={onConfirmDowngrade}
projects={orgProjects}
/>

<MembersExceedLimitModal
visible={flowState === 'show-downgrade-error'}
onClose={onCancelFlow}
/>

<ExitSurveyModal
visible={flowState === 'show-exit-survey'}
projects={orgProjects}
onClose={(success?: boolean) => {
if (success) {
setFlowState('closed')
props.onDowngrade?.()
} else {
onCancelFlow()
}
}}
/>
</>
)
}

type InitiateCancellationFlowButtonProps = Pick<
ComponentProps<typeof ButtonTooltip>,
'children' | 'type'
>

export const InitiateCancellationFlowButton = (props: InitiateCancellationFlowButtonProps) => {
const { slug } = useParams()
const track = useTrack()

const { data: selectedOrganization } = useSelectedOrganizationQuery()
const isAwsManaged = selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE

const { can: canUpdateSubscription } = useAsyncCheckPermissions(
PermissionAction.BILLING_WRITE,
'stripe.subscriptions'
)

const { data: subscription } = useOrgSubscriptionQuery({
orgSlug: slug,
})

const isDowngradeablePlan = !subscription || ['pro', 'team'].includes(subscription.plan.id)

const [visible, setVisible] = useState(false)

const tooltipText = [
[isAwsManaged, 'You cannot change the plan for an organization managed by AWS Marketplace.'],
[!isDowngradeablePlan, 'Reach out to us via support to update your plan.'],
[!canUpdateSubscription, "You need additional permissions to change your organization's plan."],
].find(([cond]) => cond)?.[1]

return (
<>
<ButtonTooltip
type={props.type}
disabled={!isDowngradeablePlan || !canUpdateSubscription || isAwsManaged}
tooltip={{
content: {
side: 'bottom',
text: tooltipText,
},
}}
onClick={() => {
track('studio_billing_cancel_subscription_clicked', {
currentPlan: subscription?.plan.id || 'free',
})
setVisible(true)
}}
>
{props.children ?? 'Cancel Subscription'}
</ButtonTooltip>

<CancellationFlow
onCancel={() => setVisible(false)}
onDowngrade={() => setVisible(false)}
visible={visible}
/>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface DowngradeModalProps {
onClose: () => void
onConfirm: () => void
projects: OrgProject[]
confirmDisabled?: boolean
}

const ProjectDowngradeListItem = ({ projectAddon }: { projectAddon: ProjectAddon }) => {
Expand Down Expand Up @@ -64,6 +65,7 @@ export const DowngradeModal = ({
onClose,
onConfirm,
projects,
confirmDisabled,
}: DowngradeModalProps) => {
const selectedPlan = useMemo(() => subscriptionsPlans.find((tier) => tier.id === 'tier_free'), [])

Expand Down Expand Up @@ -183,7 +185,14 @@ export const DowngradeModal = ({
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="warning" onClick={onConfirm}>
<AlertDialogAction
disabled={confirmDisabled ?? false}
variant="warning"
onClick={(e) => {
e.preventDefault()
onConfirm()
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalP
disabled={subscriptionUpdateDisabled || isSubmitting}
onClick={onSubmit}
>
Confirm downgrade
Downgrade Now
</Button>
</ProjectUpdateDisabledTooltip>
</DialogFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,15 @@ import { plans as subscriptionsPlans } from 'shared-data/plans'
import { Button, cn, SidePanel } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'

import { DowngradeModal } from './DowngradeModal'
import { CancellationFlow } from './CancellationFlow'
import { EnterpriseCard } from './EnterpriseCard'
import { ExitSurveyModal } from './ExitSurveyModal'
import MembersExceedLimitModal from './MembersExceedLimitModal'
import { SubscriptionPlanUpdateDialog } from './SubscriptionPlanUpdateDialog'
import UpgradeSurveyModal from './UpgradeModal'
import { STRIPE_PROJECTS_DOCS_URL } from '@/components/interfaces/Billing/Payment/PaymentMethods/StripePaymentConnection'
import { getPlanChangeType } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import PartnerManagedResource from '@/components/ui/PartnerManagedResource'
import { RequestUpgradeToBillingOwners } from '@/components/ui/RequestUpgradeToBillingOwners'
import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query'
import { isPartnerBillingOrganization } from '@/data/organizations/managed-by-utils'
import { useOrganizationBillingSubscriptionPreview } from '@/data/organizations/organization-billing-subscription-preview'
import { useOrganizationQuery } from '@/data/organizations/organization-query'
Expand Down Expand Up @@ -72,9 +69,7 @@ export const PlanUpdateSidePanel = () => {

const originalPlanRef = useRef<string>(undefined)

const [showExitSurvey, setShowExitSurvey] = useState(false)
const [showUpgradeSurvey, setShowUpgradeSurvey] = useState(false)
const [showDowngradeError, setShowDowngradeError] = useState(false)
const [selectedTier, setSelectedTier] = useState<'tier_free' | 'tier_pro' | 'tier_team'>()
const [latestAddress, setLatestAddress] = useState<CustomerAddress>()
const [latestTaxId, setLatestTaxId] = useState<CustomerTaxId | null>()
Expand Down Expand Up @@ -130,10 +125,6 @@ export const PlanUpdateSidePanel = () => {
{ orgSlug: slug },
{ enabled: visible }
)
const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery(
{ slug },
{ enabled: visible }
)

const subscriptionPreviewData = useOrganizationBillingSubscriptionPreview({
tier: selectedTier,
Expand All @@ -143,12 +134,6 @@ export const PlanUpdateSidePanel = () => {
})

const availablePlans: OrgPlan[] = plans?.plans ?? []
const hasMembersExceedingFreeTierLimit =
(membersExceededLimit || []).length > 0 &&
// [Joshen] Note that orgProjects is paginated so there's a chance this may omit certain projects
// Although I don't foresee this affecting a majority of users. Ideally perhaps we could return
// this data from the organization query
orgProjects.filter((it) => it.status !== 'INACTIVE' && it.status !== 'GOING_DOWN').length > 0

const onPanelOpened = useEffectEvent(
(properties: StudioPricingSidePanelOpenedEvent['properties']) => {
Expand Down Expand Up @@ -182,15 +167,6 @@ export const PlanUpdateSidePanel = () => {
}
}, [visible, isSuccessSubscription, subscription?.plan.id])

const onConfirmDowngrade = () => {
setSelectedTier(undefined)
if (hasMembersExceedingFreeTierLimit) {
setShowDowngradeError(true)
} else {
setShowExitSurvey(true)
}
}

const planMeta = selectedTier
? availablePlans.find((p) => p.id === selectedTier.split('tier_')[1])
: null
Expand Down Expand Up @@ -395,12 +371,10 @@ export const PlanUpdateSidePanel = () => {
</SidePanel.Content>
</SidePanel>

<DowngradeModal
<CancellationFlow
visible={selectedTier === 'tier_free'}
subscription={subscription}
onClose={() => setSelectedTier(undefined)}
onConfirm={onConfirmDowngrade}
projects={orgProjects}
onCancel={() => setSelectedTier(undefined)}
onDowngrade={() => setSelectedTier(undefined)}
/>

<SubscriptionPlanUpdateDialog
Expand All @@ -416,20 +390,6 @@ export const PlanUpdateSidePanel = () => {
onUseAsDefaultBillingAddressChange={handleUseAsDefaultBillingAddressChange}
/>

<MembersExceedLimitModal
visible={showDowngradeError}
onClose={() => setShowDowngradeError(false)}
/>

<ExitSurveyModal
visible={showExitSurvey}
projects={orgProjects}
onClose={(success?: boolean) => {
setShowExitSurvey(false)
if (success) onClose()
}}
/>

<UpgradeSurveyModal
visible={showUpgradeSurvey}
originalPlan={originalPlanRef.current}
Expand Down
Loading
Loading