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
54 changes: 40 additions & 14 deletions apps/studio/components/interfaces/Billing/Billing.constants.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
export const USAGE_APPROACHING_THRESHOLD = 0.8

export const CANCELLATION_REASONS = [
'Pricing',
"My project isn't getting traction",
'Poor customer service',
'Missing feature',
"I didn't see the value",
"Supabase didn't meet my needs",
'Dashboard is too complicated',
'Postgres is too complicated',
'Problem not solved',
'Too many bugs/issues',
'I decided to use something else',
'My work has finished/discontinued',
'I’m migrating to/starting a new project',
'None of the above',
{
value: 'I was just exploring, or it was a hobby/student project.',
},
{
value: 'I was not satisfied with the customer support I received.',
label: 'Could you tell us more about your experience with our support team?',
},
{
value: 'Supabase is missing a specific feature I need.',
label: 'What specific feature(s) are we missing?',
},
{
value: 'I found it difficult to use or build with.',
label: 'What specific parts of Supabase did you find difficult or frustrating?',
},
{
value: 'Performance or reliability insufficient.',
label:
'Could you tell us more about the specific issues you encountered (e.g., UI bugs, API latency, downtime)?',
},
{
value: 'My project was cancelled or put on hold.',
},
{
value: 'Too expensive',
label: 'We appreciate your perspective on our pricing, what aspects of the cost felt too high?',
},
{
value: 'The pricing is unpredictable and hard to budget for.',
label:
'Which aspects of our pricing model made it difficult for you to predict your monthly costs?',
},
{
value: 'My company went out of business or was acquired.',
},
{
value: 'I lost trust in the company or its future direction.',
label:
'Building and maintaining your trust is our highest priority, could you please share the specific event or reason that led to this loss of trust?',
},
]

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { includes, without } from 'lodash'
import { useReducer, useState } from 'react'
import { useState } from 'react'
import { toast } from 'sonner'

import { useParams } from 'common'
import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants'
import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send'
import { ProjectInfo } from 'data/projects/projects-query'
import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation'
import { useFlag } from 'hooks/ui/useFlag'
import { Alert, Button, Input, Modal } from 'ui'
import type { ProjectInfo } from '../../../../../data/projects/projects-query'
import { CANCELLATION_REASONS } from '../BillingSettings.constants'
import { Alert, Button, cn, Input, Modal } from 'ui'
import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip'

export interface ExitSurveyModalProps {
Expand All @@ -18,11 +17,11 @@ export interface ExitSurveyModalProps {
}

// [Joshen] For context - Exit survey is only when going to Free Plan from a paid plan
const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) => {
export const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) => {
const { slug } = useParams()

const [message, setMessage] = useState('')
const [selectedReasons, dispatchSelectedReasons] = useReducer(reducer, [])
const [selectedReason, setSelectedReason] = useState<string[]>([])

const subscriptionUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const { mutate: updateOrgSubscription, isLoading: isUpdating } = useOrgSubscriptionUpdateMutation(
Expand All @@ -42,17 +41,26 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) =

const hasProjectsWithComputeDowngrade = projectsWithComputeDowngrade.length > 0

function reducer(state: any, action: any) {
if (includes(state, action.target.value)) {
return without(state, action.target.value)
} else {
return [...state, action.target.value]
}
const [shuffledReasons] = useState(() => [
...CANCELLATION_REASONS.sort(() => Math.random() - 0.5),
{ value: 'None of the above' },
])

const onSelectCancellationReason = (reason: string) => {
setSelectedReason([reason])
}

// Helper to get label for selected reason
const getReasonLabel = (reason: string | undefined) => {
const found = CANCELLATION_REASONS.find((r) => r.value === reason)
return found?.label || 'What can we improve on?'
}

const textareaLabel = getReasonLabel(selectedReason[0])

const onSubmit = async () => {
if (selectedReasons.length === 0) {
return toast.error('Please select at least one reason for canceling your subscription')
if (selectedReason.length === 0) {
return toast.error('Please select a reason for canceling your subscription')
}

await downgradeOrganization()
Expand All @@ -70,7 +78,7 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) =
try {
await sendExitSurvey({
orgSlug: slug,
reasons: selectedReasons.reduce((a, b) => `${a}- ${b}\n`, ''),
reasons: selectedReason.reduce((a, b) => `${a}- ${b}\n`, ''),
message,
exitAction: 'downgrade',
})
Expand All @@ -92,99 +100,87 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) =
}

return (
<>
<Modal
hideFooter
size="xlarge"
visible={visible}
onCancel={onClose}
header="Help us improve."
>
<Modal.Content>
<div className="space-y-4">
<p className="text-sm text-foreground-light">
We always strive to improve Supabase as much as we can. Please let us know the reasons
you are canceling your subscription so that we can improve in the future.
</p>
<div className="space-y-8 mt-6">
<div className="flex flex-wrap gap-2" data-toggle="buttons">
{CANCELLATION_REASONS.map((option) => {
const active = selectedReasons.find((x) => x === option)
return (
<label
key={option}
className={`
flex cursor-pointer items-center space-x-2 rounded-md py-1
pl-2 pr-3 text-center text-sm
shadow-sm transition-all duration-100
${
active
? ` bg-foreground text-background opacity-100 hover:bg-opacity-75`
: ` bg-border-strong text-foreground opacity-25 hover:opacity-50`
}
`}
>
<input
type="checkbox"
name="options"
value={option}
className="hidden"
onClick={dispatchSelectedReasons}
/>
<div>{option}</div>
</label>
)
})}
</div>
<div className="text-area-text-sm">
<Input.TextArea
id="message"
name="message"
value={message}
onChange={(event: any) => setMessage(event.target.value)}
label="Anything else that we can improve on?"
/>
</div>
<Modal hideFooter size="xlarge" visible={visible} onCancel={onClose} header="Help us improve">
<Modal.Content>
<div className="space-y-4">
<p className="text-sm text-foreground-light">
Share with us why you're downgrading your plan.
</p>
<div className="space-y-8 mt-6">
<div className="flex flex-wrap gap-2" data-toggle="buttons">
{shuffledReasons.map((option) => {
const active = selectedReason[0] === option.value
return (
<label
key={option.value}
className={cn(
'flex cursor-pointer items-center space-x-2 rounded-md py-1',
'pl-2 pr-3 text-center text-sm',
'shadow-sm transition-all duration-100',
active
? `bg-foreground text-background opacity-100 hover:bg-opacity-75`
: `bg-border-strong text-foreground opacity-75 hover:opacity-100`
)}
>
<input
type="radio"
name="options"
value={option.value}
className="hidden"
checked={active}
onChange={() => onSelectCancellationReason(option.value)}
/>
<div>{option.value}</div>
</label>
)
})}
</div>
<div className="text-area-text-sm flex flex-col gap-y-2">
<label className="text-sm whitespace-pre-line break-words">{textareaLabel}</label>
<Input.TextArea
id="message"
name="message"
value={message}
onChange={(event: any) => setMessage(event.target.value)}
rows={3}
/>
</div>
{hasProjectsWithComputeDowngrade && (
<Alert
withIcon
variant="warning"
title={`${projectsWithComputeDowngrade.length} of your projects will be restarted upon clicking confirm,`}
>
This is due to changes in compute instances from the downgrade. Affected projects
include {projectsWithComputeDowngrade.map((project) => project.name).join(', ')}.
</Alert>
)}
</div>
</Modal.Content>

<div className="flex items-center justify-between border-t px-4 py-4">
<p className="text-xs text-foreground-lighter">
The unused amount for the remaining time of your billing cycle will be refunded as
credits
</p>

<div className="flex items-center space-x-2">
<Button type="default" onClick={() => onClose()}>
Cancel
{hasProjectsWithComputeDowngrade && (
<Alert
withIcon
variant="warning"
title={`${projectsWithComputeDowngrade.length} of your projects will be restarted upon clicking confirm,`}
>
This is due to changes in compute instances from the downgrade. Affected projects
include {projectsWithComputeDowngrade.map((project) => project.name).join(', ')}.
</Alert>
)}
</div>
</Modal.Content>

<div className="flex items-center justify-between border-t px-4 py-4">
<p className="text-xs text-foreground-lighter">
The unused amount for the remaining time of your billing cycle will be refunded as credits
</p>

<div className="flex items-center space-x-2">
<Button type="default" onClick={() => onClose()}>
Cancel
</Button>
<ProjectUpdateDisabledTooltip projectUpdateDisabled={subscriptionUpdateDisabled}>
<Button
type="danger"
className="pointer-events-auto"
loading={isSubmitting}
disabled={subscriptionUpdateDisabled || isSubmitting}
onClick={onSubmit}
>
Confirm downgrade
</Button>
<ProjectUpdateDisabledTooltip projectUpdateDisabled={subscriptionUpdateDisabled}>
<Button
type="danger"
className="pointer-events-auto"
loading={isSubmitting}
disabled={subscriptionUpdateDisabled || isSubmitting}
onClick={onSubmit}
>
Confirm downgrade
</Button>
</ProjectUpdateDisabledTooltip>
</div>
</ProjectUpdateDisabledTooltip>
</div>
</Modal>
</>
</div>
</Modal>
)
}

export default ExitSurveyModal
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'

import { StudioPricingSidePanelOpenedEvent } from 'common/telemetry-constants'
import { getPlanChangeType } from 'components/interfaces/Billing/Subscription/Subscription.utils'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import PartnerManagedResource from 'components/ui/PartnerManagedResource'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
import { useOrganizationBillingSubscriptionPreview } from 'data/organizations/organization-billing-subscription-preview'
Expand All @@ -23,12 +25,10 @@ import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
import { Button, SidePanel, cn } from 'ui'
import DowngradeModal from './DowngradeModal'
import { EnterpriseCard } from './EnterpriseCard'
import ExitSurveyModal from './ExitSurveyModal'
import { ExitSurveyModal } from './ExitSurveyModal'
import MembersExceedLimitModal from './MembersExceedLimitModal'
import { SubscriptionPlanUpdateDialog } from './SubscriptionPlanUpdateDialog'
import UpgradeSurveyModal from './UpgradeModal'
import PartnerManagedResource from 'components/ui/PartnerManagedResource'
import { getPlanChangeType } from 'components/interfaces/Billing/Subscription/Subscription.utils'

const PlanUpdateSidePanel = () => {
const router = useRouter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useState } from 'react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import DeleteProjectModal from './DeleteProjectModal'
import { DeleteProjectModal } from './DeleteProjectModal'

export interface DeleteProjectButtonProps {
type?: 'danger' | 'default'
Expand Down
Loading
Loading