From 11914a55defb553b71872bbaadbcdca359b9b41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 1 Nov 2023 10:52:52 +0100 Subject: [PATCH] chore: remove old billing code (#18610) --- apps/www/pages/pricing/index.tsx | 2 +- packages/shared-data/plans.ts | 14 +- .../subscriptions/org-subscription-query.js | 7 + .../project-subscription-v2-query.js | 7 - .../Billing.constants.ts | 0 .../InvoiceStatusBadge.tsx | 0 .../{BillingV2 => Billing}/Invoices.tsx | 0 .../{BillingV2 => Billing}/Invoices.types.ts | 0 .../NoProjectsOnPaidOrgInfo.tsx | 0 .../Payment}/AddNewPaymentMethodModal.tsx | 0 .../Payment}/AddPaymentMethodForm.tsx | 0 .../{BillingV2 => Billing}/SpendCapModal.tsx | 0 .../Subscription/Subscription.constants.ts | 0 .../Subscription/Subscription.utils.ts | 5 +- .../Usage/UsageWarningAlerts/CPUWarnings.tsx | 0 .../DiskIOBandwidthWarnings.tsx | 0 .../Usage/UsageWarningAlerts/RAMWarnings.tsx | 0 .../Usage/UsageWarningAlerts/index.ts | 0 .../BillingV2/Subscription/AddOns/AddOns.tsx | 10 +- .../Subscription/BillingBreakdown.tsx | 456 ---------------- .../Subscription/CostControl/CostControl.tsx | 159 ------ .../CostControl/SpendCapSidePanel.tsx | 310 ----------- .../BillingV2/Subscription/Subscription.tsx | 120 ----- .../Subscription/Tier/ExitSurveyModal.tsx | 227 -------- .../Tier/MembersExceedLimitModal.tsx | 57 -- .../Tier/PaymentMethodSelection.tsx | 181 ------- .../Tier/SubscriptionPaymentMethod.tsx | 226 -------- .../Subscription/Tier/SubscriptionTier.tsx | 181 ------- .../Subscription/Tier/TierUpdateSidePanel.tsx | 420 --------------- .../interfaces/BillingV2/Usage/Activity.tsx | 126 ----- .../interfaces/BillingV2/Usage/Bandwidth.tsx | 65 --- .../BillingV2/Usage/Infrastructure.tsx | 281 ---------- .../BillingV2/Usage/SectionContent.tsx | 63 --- .../BillingV2/Usage/SectionHeader.tsx | 19 - .../BillingV2/Usage/SizeAndCounts.tsx | 79 --- .../BillingV2/Usage/Usage.constants.tsx | 365 ------------- .../interfaces/BillingV2/Usage/Usage.tsx | 327 ----------- .../interfaces/BillingV2/Usage/Usage.utils.ts | 111 ---- .../BillingV2/Usage/UsageBarChart.tsx | 96 ---- .../BillingV2/Usage/UsageSection.tsx | 286 ---------- .../components/interfaces/BillingV2/index.ts | 6 - .../Database/Backups/BackupsList.tsx | 4 +- .../Home/ProjectList/ProjectList.tsx | 65 +-- .../interfaces/Home/ProjectUsageSection.tsx | 3 +- studio/components/interfaces/Home/index.ts | 3 +- .../BillingAddress/BillingAddress.tsx | 329 ++++++------ .../BillingBreakdown.constants.ts | 0 .../BillingBreakdown/BillingBreakdown.tsx | 0 .../BillingBreakdown/BillingMetric.tsx | 2 +- .../BillingBreakdown/UpcomingInvoice.tsx | 0 .../BillingSettings/BillingEmail.tsx | 156 +++--- .../BillingSettings.constants.ts | 0 .../BillingSettings/BillingSettings.tsx | 103 ++-- .../CostControl/CostControl.tsx | 2 +- .../CostControl/SpendCapSidePanel.tsx | 0 .../BillingSettings/CreditBalance.tsx | 78 ++- .../BillingSettings/PaymentMethods.tsx | 2 +- .../ChangePaymentMethodModal.tsx | 0 .../DeletePaymentMethodModal.tsx | 0 .../PaymentMethods/PaymentMethods.tsx | 2 +- .../BillingSettings/ProjectsSummary.tsx | 105 ---- .../Subscription/DowngradeModal.tsx | 0 .../Subscription}/EnterpriseCard.tsx | 0 .../Subscription/ExitSurveyModal.tsx | 2 +- .../Subscription/MembersExceedLimitModal.tsx | 0 .../Subscription/PaymentMethodSelection.tsx | 2 +- .../Subscription/PlanUpdateSidePanel.tsx | 2 +- .../Subscription/Subscription.tsx | 2 +- .../BillingSettings/TaxID/TaxID.constants.ts | 8 - .../BillingSettings/TaxID/TaxID.tsx | 312 +++++------ .../BillingSettings/TaxID/TaxID.utils.ts | 22 - .../TaxID/TaxID.utilts.ts | 0 .../BillingAddress.constants.ts | 248 --------- .../BillingAddress/BillingAddress.tsx | 211 -------- .../BillingSettingsV2/BillingEmail.tsx | 119 ---- .../BillingSettingsV2/BillingSettings.tsx | 82 --- .../BillingSettingsV2/CreditBalance.tsx | 66 --- .../Subscription/EnterpriseCard.tsx | 65 --- .../TaxID/TaxID.constants.ts | 459 ---------------- .../BillingSettingsV2/TaxID/TaxID.tsx | 262 --------- .../GeneralSettings/GeneralSettings.tsx | 2 +- .../MigrateOrganizationBillingButton.tsx | 508 ------------------ .../OrganizationBillingMigrationPanel.tsx | 48 -- .../InvoicesSettings/InvoicesSettings.tsx | 4 +- .../Organization/NewOrg/NewOrgForm.tsx | 2 +- .../NewProject/EmptyPaymentMethodWarning.tsx | 2 +- .../Organization/Usage/Usage.constants.tsx | 2 +- .../Organization/Usage/Usage.utils.ts | 25 +- .../Usage/UsageSection/AttributeUsage.tsx | 1 - .../interfaces/Organization/index.ts | 2 - .../interfaces/SQLEditor/RenameQueryModal.tsx | 10 +- .../interfaces/SQLEditor/SQLEditor.tsx | 7 +- .../UtilityPanel/UtilityTabResults.tsx | 7 +- .../interfaces/Settings/Addons/Addons.tsx | 6 +- .../Addons/ComputeInstanceSidePanel.tsx | 27 +- .../Settings/Addons/CustomDomainSidePanel.tsx | 19 +- .../Settings/Addons/PITRSidePanel.tsx | 19 +- .../interfaces/Settings/Addons/index.ts | 8 - .../DatabaseSettings/DatabaseSettings.tsx | 18 +- .../Database/DiskSizeConfiguration.tsx | 7 +- .../CustomDomainConfig/CustomDomainConfig.tsx | 7 +- .../DeleteProjectButton.tsx | 9 +- .../interfaces/Settings/General/General.tsx | 5 +- .../General/Infrastructure/Infrastructure.tsx | 8 +- .../Infrastructure.constants.ts | 5 - .../Infrastructure/InfrastructureActivity.tsx | 7 +- .../interfaces/Settings/Logs/Logs.utils.ts | 2 +- .../Settings/Logs/LogsPreviewer.tsx | 6 +- .../interfaces/Support/SupportForm.tsx | 9 +- .../EnableBranchingModal.tsx | 201 +++---- .../AppLayout/OrganizationDropdown.tsx | 6 +- .../layouts/AppLayout/ProjectDropdown.tsx | 27 +- .../components/layouts/OrganizationLayout.tsx | 8 +- .../LayoutHeader/LayoutHeader.tsx | 37 +- .../ProjectLayout/ProjectPausedState.tsx | 5 +- .../SettingsMenu.utils.ts | 314 ++++------- .../ProjectSettingsMenuItem.tsx | 20 +- .../StorageSettings/StorageSettings.tsx | 8 +- .../ui/OveragesBanner/OveragesBanner.tsx | 104 ---- .../ui/OveragesBanner/OveragesBanner.utils.ts | 30 +- .../ResourceExhaustionWarningBanner.tsx | 4 +- studio/components/ui/UpgradeToPro.tsx | 16 +- .../free-project-limit-check-query.ts | 12 - .../organization-customer-profile-query.ts | 15 - .../organization-payment-methods-query.ts | 14 - .../organization-tax-ids-query.ts | 13 - studio/data/subscriptions/keys.ts | 5 - studio/data/subscriptions/org-plans-query.ts | 15 +- .../subscriptions/org-subscription-query.ts | 16 +- .../project-addon-update-mutation.ts | 5 +- .../subscriptions/project-addons-query.ts | 15 +- .../data/subscriptions/project-plans-query.ts | 55 -- .../project-subscription-update-mutation.ts | 84 --- .../project-subscription-v2-query.ts | 98 ---- studio/data/usage/org-usage-query.ts | 13 - studio/data/usage/project-usage-query.ts | 12 - studio/hooks/misc/useUpgradePrompt.tsx | 8 +- studio/pages/new/[slug].tsx | 2 +- studio/pages/old-org.tsx | 170 ------ studio/pages/org/[slug]/billing.tsx | 13 +- studio/pages/org/[slug]/index.tsx | 2 +- .../project/[ref]/database/backups/pitr.tsx | 11 +- studio/pages/project/[ref]/index.tsx | 2 +- .../project/[ref]/logs/explorer/index.tsx | 7 +- .../project/[ref]/reports/api-overview.tsx | 7 +- .../pages/project/[ref]/reports/storage.tsx | 6 +- .../pages/project/[ref]/settings/addons.tsx | 28 +- .../[ref]/settings/billing/invoices.tsx | 110 ---- .../[ref]/settings/billing/subscription.tsx | 51 -- .../project/[ref]/settings/billing/usage.tsx | 41 -- .../pages/project/[ref]/settings/general.tsx | 6 +- .../project/[ref]/settings/infrastructure.tsx | 26 - .../pages/project/[ref]/settings/storage.tsx | 4 +- studio/styles/users-table.scss | 1 - .../components/Home/ProjectUsage.test.js | 109 ---- .../pages/projects/LogsPreviewer.test.js | 6 +- .../projects/billing/Infrastructure.test.tsx | 24 - .../tests/pages/projects/logs-query.test.js | 4 +- 158 files changed, 894 insertions(+), 8600 deletions(-) create mode 100644 studio/__mocks__/data/subscriptions/org-subscription-query.js delete mode 100644 studio/__mocks__/data/subscriptions/project-subscription-v2-query.js rename studio/components/interfaces/{BillingV2 => Billing}/Billing.constants.ts (100%) rename studio/components/interfaces/{BillingV2 => Billing}/InvoiceStatusBadge.tsx (100%) rename studio/components/interfaces/{BillingV2 => Billing}/Invoices.tsx (100%) rename studio/components/interfaces/{BillingV2 => Billing}/Invoices.types.ts (100%) rename studio/components/interfaces/{BillingV2 => Billing}/NoProjectsOnPaidOrgInfo.tsx (100%) rename studio/components/interfaces/{BillingV2/AddNewPaymentMethodModal => Billing/Payment}/AddNewPaymentMethodModal.tsx (100%) rename studio/components/interfaces/{BillingV2/AddNewPaymentMethodModal => Billing/Payment}/AddPaymentMethodForm.tsx (100%) rename studio/components/interfaces/{BillingV2 => Billing}/SpendCapModal.tsx (100%) rename studio/components/interfaces/{BillingV2 => Billing}/Subscription/Subscription.constants.ts (100%) rename studio/components/interfaces/{BillingV2 => Billing}/Subscription/Subscription.utils.ts (74%) rename studio/components/interfaces/{BillingV2 => Billing}/Usage/UsageWarningAlerts/CPUWarnings.tsx (100%) rename studio/components/interfaces/{BillingV2 => Billing}/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx (100%) rename studio/components/interfaces/{BillingV2 => Billing}/Usage/UsageWarningAlerts/RAMWarnings.tsx (100%) rename studio/components/interfaces/{BillingV2 => Billing}/Usage/UsageWarningAlerts/index.ts (100%) delete mode 100644 studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/CostControl/CostControl.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/CostControl/SpendCapSidePanel.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Subscription.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Tier/ExitSurveyModal.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Tier/MembersExceedLimitModal.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Tier/PaymentMethodSelection.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionPaymentMethod.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionTier.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Tier/TierUpdateSidePanel.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/Activity.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/Bandwidth.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/SectionContent.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/SectionHeader.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/SizeAndCounts.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/Usage.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/Usage.utils.ts delete mode 100644 studio/components/interfaces/BillingV2/Usage/UsageBarChart.tsx delete mode 100644 studio/components/interfaces/BillingV2/Usage/UsageSection.tsx delete mode 100644 studio/components/interfaces/BillingV2/index.ts rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/BillingBreakdown/BillingBreakdown.constants.ts (100%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/BillingBreakdown/BillingBreakdown.tsx (100%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/BillingBreakdown/BillingMetric.tsx (99%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/BillingBreakdown/UpcomingInvoice.tsx (100%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/BillingSettings.constants.ts (100%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/CostControl/CostControl.tsx (98%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/CostControl/SpendCapSidePanel.tsx (100%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/PaymentMethods/ChangePaymentMethodModal.tsx (100%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/PaymentMethods/DeletePaymentMethodModal.tsx (100%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/PaymentMethods/PaymentMethods.tsx (99%) delete mode 100644 studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/Subscription/DowngradeModal.tsx (100%) rename studio/components/interfaces/{BillingV2/Subscription/Tier => Organization/BillingSettings/Subscription}/EnterpriseCard.tsx (100%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/Subscription/ExitSurveyModal.tsx (98%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/Subscription/MembersExceedLimitModal.tsx (100%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/Subscription/PaymentMethodSelection.tsx (98%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/Subscription/PlanUpdateSidePanel.tsx (99%) rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/Subscription/Subscription.tsx (99%) delete mode 100644 studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts rename studio/components/interfaces/Organization/{BillingSettingsV2 => BillingSettings}/TaxID/TaxID.utilts.ts (100%) delete mode 100644 studio/components/interfaces/Organization/BillingSettingsV2/BillingAddress/BillingAddress.constants.ts delete mode 100644 studio/components/interfaces/Organization/BillingSettingsV2/BillingAddress/BillingAddress.tsx delete mode 100644 studio/components/interfaces/Organization/BillingSettingsV2/BillingEmail.tsx delete mode 100644 studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.tsx delete mode 100644 studio/components/interfaces/Organization/BillingSettingsV2/CreditBalance.tsx delete mode 100644 studio/components/interfaces/Organization/BillingSettingsV2/Subscription/EnterpriseCard.tsx delete mode 100644 studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.constants.ts delete mode 100644 studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.tsx delete mode 100644 studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx delete mode 100644 studio/components/interfaces/Organization/GeneralSettings/OrganizationBillingMigrationPanel.tsx delete mode 100644 studio/components/interfaces/Settings/Addons/index.ts delete mode 100644 studio/components/ui/OveragesBanner/OveragesBanner.tsx delete mode 100644 studio/data/subscriptions/project-plans-query.ts delete mode 100644 studio/data/subscriptions/project-subscription-update-mutation.ts delete mode 100644 studio/data/subscriptions/project-subscription-v2-query.ts delete mode 100644 studio/pages/old-org.tsx delete mode 100644 studio/pages/project/[ref]/settings/billing/invoices.tsx delete mode 100644 studio/pages/project/[ref]/settings/billing/subscription.tsx delete mode 100644 studio/pages/project/[ref]/settings/billing/usage.tsx delete mode 100644 studio/styles/users-table.scss delete mode 100644 studio/tests/components/Home/ProjectUsage.test.js delete mode 100644 studio/tests/pages/projects/billing/Infrastructure.test.tsx diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index d47c4e3789d..49128542990 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -253,7 +253,7 @@ export default function IndexPage() { {plan.priceMonthly}

- {plan.costUnitOrg} + {plan.costUnit}

diff --git a/packages/shared-data/plans.ts b/packages/shared-data/plans.ts index d13c90cb463..6d391e1e24b 100644 --- a/packages/shared-data/plans.ts +++ b/packages/shared-data/plans.ts @@ -3,12 +3,10 @@ export interface PricingInformation { name: string nameBadge?: string costUnit?: string - costUnitOrg?: string href: string priceLabel?: string priceMonthly: number | string warning?: string - warningLegacy?: string description: string preface: string features: string[] @@ -21,13 +19,11 @@ export const plans: PricingInformation[] = [ id: 'tier_free', name: 'Free', nameBadge: '', - costUnit: '/ month / project', - costUnitOrg: '/ month / org', + costUnit: '/ month / org', href: 'https://supabase.com/dashboard/new?plan=free', priceLabel: '', priceMonthly: 0, warning: 'Limit of 2 free organizations', - warningLegacy: 'Limit of 2 free projects', description: 'Perfect for passion projects & simple websites.', preface: 'Get started with:', features: [ @@ -51,12 +47,10 @@ export const plans: PricingInformation[] = [ id: 'tier_pro', name: 'Pro', nameBadge: '', - costUnit: '/ month / project', - costUnitOrg: '/ month / org', + costUnit: '/ month / org', href: 'https://supabase.com/dashboard/new?plan=pro', priceLabel: 'From', warning: 'Usage-based plan', - warningLegacy: 'Usage-based plan', priceMonthly: 25, description: 'For production applications with the option to scale.', features: [ @@ -81,12 +75,10 @@ export const plans: PricingInformation[] = [ id: 'tier_team', name: 'Team', nameBadge: 'New', - costUnit: '/ month / project', - costUnitOrg: '/ month / org', + costUnit: '/ month / org', href: 'https://supabase.com/dashboard/new?plan=team', priceLabel: 'From', warning: 'Usage-based plan', - warningLegacy: 'Usage-based plan', priceMonthly: 599, description: 'Collaborate with different permissions and access patterns.', features: [ diff --git a/studio/__mocks__/data/subscriptions/org-subscription-query.js b/studio/__mocks__/data/subscriptions/org-subscription-query.js new file mode 100644 index 00000000000..642cebdd0aa --- /dev/null +++ b/studio/__mocks__/data/subscriptions/org-subscription-query.js @@ -0,0 +1,7 @@ +export const useOrgSubscriptionQuery = jest.fn().mockReturnValue({ + data: { + plan: { + id: 'free', + }, + }, +}) diff --git a/studio/__mocks__/data/subscriptions/project-subscription-v2-query.js b/studio/__mocks__/data/subscriptions/project-subscription-v2-query.js deleted file mode 100644 index 8335f3ea6e9..00000000000 --- a/studio/__mocks__/data/subscriptions/project-subscription-v2-query.js +++ /dev/null @@ -1,7 +0,0 @@ -export const useProjectSubscriptionV2Query = jest.fn().mockReturnValue({ - data: { - plan: { - id: 'free', - }, - }, -}) diff --git a/studio/components/interfaces/BillingV2/Billing.constants.ts b/studio/components/interfaces/Billing/Billing.constants.ts similarity index 100% rename from studio/components/interfaces/BillingV2/Billing.constants.ts rename to studio/components/interfaces/Billing/Billing.constants.ts diff --git a/studio/components/interfaces/BillingV2/InvoiceStatusBadge.tsx b/studio/components/interfaces/Billing/InvoiceStatusBadge.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/InvoiceStatusBadge.tsx rename to studio/components/interfaces/Billing/InvoiceStatusBadge.tsx diff --git a/studio/components/interfaces/BillingV2/Invoices.tsx b/studio/components/interfaces/Billing/Invoices.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/Invoices.tsx rename to studio/components/interfaces/Billing/Invoices.tsx diff --git a/studio/components/interfaces/BillingV2/Invoices.types.ts b/studio/components/interfaces/Billing/Invoices.types.ts similarity index 100% rename from studio/components/interfaces/BillingV2/Invoices.types.ts rename to studio/components/interfaces/Billing/Invoices.types.ts diff --git a/studio/components/interfaces/BillingV2/NoProjectsOnPaidOrgInfo.tsx b/studio/components/interfaces/Billing/NoProjectsOnPaidOrgInfo.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/NoProjectsOnPaidOrgInfo.tsx rename to studio/components/interfaces/Billing/NoProjectsOnPaidOrgInfo.tsx diff --git a/studio/components/interfaces/BillingV2/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx b/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx rename to studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx diff --git a/studio/components/interfaces/BillingV2/AddNewPaymentMethodModal/AddPaymentMethodForm.tsx b/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/AddNewPaymentMethodModal/AddPaymentMethodForm.tsx rename to studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx diff --git a/studio/components/interfaces/BillingV2/SpendCapModal.tsx b/studio/components/interfaces/Billing/SpendCapModal.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/SpendCapModal.tsx rename to studio/components/interfaces/Billing/SpendCapModal.tsx diff --git a/studio/components/interfaces/BillingV2/Subscription/Subscription.constants.ts b/studio/components/interfaces/Billing/Subscription/Subscription.constants.ts similarity index 100% rename from studio/components/interfaces/BillingV2/Subscription/Subscription.constants.ts rename to studio/components/interfaces/Billing/Subscription/Subscription.constants.ts diff --git a/studio/components/interfaces/BillingV2/Subscription/Subscription.utils.ts b/studio/components/interfaces/Billing/Subscription/Subscription.utils.ts similarity index 74% rename from studio/components/interfaces/BillingV2/Subscription/Subscription.utils.ts rename to studio/components/interfaces/Billing/Subscription/Subscription.utils.ts index 1b288cee149..85171c1734d 100644 --- a/studio/components/interfaces/BillingV2/Subscription/Subscription.utils.ts +++ b/studio/components/interfaces/Billing/Subscription/Subscription.utils.ts @@ -1,6 +1,5 @@ import { OrgSubscription } from 'data/subscriptions/org-subscription-query' import { ProjectSelectedAddon } from 'data/subscriptions/project-addons-query' -import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query' export const getAddons = (selectedAddons: ProjectSelectedAddon[]) => { const computeInstance = selectedAddons.find((addon) => addon.type === 'compute_instance') @@ -10,9 +9,7 @@ export const getAddons = (selectedAddons: ProjectSelectedAddon[]) => { return { computeInstance, pitr, customDomain } } -export const subscriptionHasHipaaAddon = ( - subscription: OrgSubscription | ProjectSubscriptionResponse | undefined -): boolean => { +export const subscriptionHasHipaaAddon = (subscription: OrgSubscription | undefined): boolean => { return ( subscription !== undefined && subscription.addons.some((addon) => addon.supabase_prod_id === 'addon_security_hipaa') diff --git a/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/CPUWarnings.tsx b/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/CPUWarnings.tsx rename to studio/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings.tsx diff --git a/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx b/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx rename to studio/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx diff --git a/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/RAMWarnings.tsx b/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/RAMWarnings.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/RAMWarnings.tsx rename to studio/components/interfaces/Billing/Usage/UsageWarningAlerts/RAMWarnings.tsx diff --git a/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/index.ts b/studio/components/interfaces/Billing/Usage/UsageWarningAlerts/index.ts similarity index 100% rename from studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/index.ts rename to studio/components/interfaces/Billing/Usage/UsageWarningAlerts/index.ts diff --git a/studio/components/interfaces/BillingV2/Subscription/AddOns/AddOns.tsx b/studio/components/interfaces/BillingV2/Subscription/AddOns/AddOns.tsx index 257719b8c77..f9278b26338 100644 --- a/studio/components/interfaces/BillingV2/Subscription/AddOns/AddOns.tsx +++ b/studio/components/interfaces/BillingV2/Subscription/AddOns/AddOns.tsx @@ -14,11 +14,6 @@ import { } from 'ui' import ProjectUpdateDisabledTooltip from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip' -import { - ComputeInstanceSidePanel, - CustomDomainSidePanel, - PITRSidePanel, -} from 'components/interfaces/Settings/Addons' import { useIsProjectActive, useProjectContext, @@ -31,8 +26,11 @@ import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils' import { BASE_PATH } from 'lib/constants' import { getSemanticVersion } from 'lib/helpers' import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' -import { getAddons } from '../Subscription.utils' import Image from 'next/image' +import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils' +import ComputeInstanceSidePanel from 'components/interfaces/Settings/Addons/ComputeInstanceSidePanel' +import PITRSidePanel from 'components/interfaces/Settings/Addons/PITRSidePanel' +import CustomDomainSidePanel from 'components/interfaces/Settings/Addons/CustomDomainSidePanel' const AddOns = () => { const { resolvedTheme } = useTheme() diff --git a/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx b/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx deleted file mode 100644 index dbb2bef1327..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx +++ /dev/null @@ -1,456 +0,0 @@ -import * as Tooltip from '@radix-ui/react-tooltip' -import clsx from 'clsx' -import { useParams } from 'common' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import SparkBar from 'components/ui/SparkBar' -import { useUpcomingInvoiceQuery } from 'data/invoices/invoice-upcoming-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useProjectUsageQuery } from 'data/usage/project-usage-query' -import dayjs from 'dayjs' -import { USAGE_APPROACHING_THRESHOLD } from 'lib/constants' -import { formatBytes } from 'lib/helpers' -import { partition } from 'lodash' -import Link from 'next/link' -import { useState } from 'react' -import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' -import { Alert, Button, Collapsible, IconAlertTriangle, IconChevronRight, IconInfo } from 'ui' -import { BILLING_BREAKDOWN_METRICS } from './Subscription.constants' - -export interface BillingBreakdownProps {} - -const BillingBreakdown = ({}: BillingBreakdownProps) => { - const { ref: projectRef } = useParams() - const snap = useSubscriptionPageStateSnapshot() - const [showUsageFees, setShowUsageFees] = useState(false) - const { data: usage, isLoading: isLoadingUsage } = useProjectUsageQuery({ projectRef }) - const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionV2Query({ - projectRef, - }) - const { - data: upcomingInvoice, - error: upcomingInvoiceError, - isLoading: isLoadingUpcomingInvoice, - } = useUpcomingInvoiceQuery({ - projectRef, - }) - const billingCycleStart = dayjs.unix(subscription?.current_period_start ?? 0).utc() - const billingCycleEnd = dayjs.unix(subscription?.current_period_end ?? 0).utc() - - const currentPlan = subscription?.plan - const isUsageBillingEnabled = subscription?.usage_billing_enabled - const [usageFees, fixedFees] = partition(upcomingInvoice?.lines ?? [], (item) => item.usage_based) - const totalUsageFees = Number(usageFees.reduce((a, b) => a + b.amount, 0).toFixed(2)) - - const largeNumberFormatter = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }) - - const hasExceededAnyLimits = - isUsageBillingEnabled === false && - Object.values(usage ?? {}) - .map((metric) => { - if (typeof metric !== 'object') return false - if (metric.limit <= 0) return false - if (metric.usage > metric.limit) return true - }) - .includes(true) - - return ( -
-
-
-

Billing breakdown

-

- Current billing cycle: {billingCycleStart.format('MMM DD')} -{' '} - {billingCycleEnd.format('MMM DD')} -

-
-
- {isLoadingSubscription ? ( -
- - - -
- ) : ( -
-

Included usage summary

- {isUsageBillingEnabled ? ( -

- Your plan includes a limited amount of usage. If the usage on your project exceeds - these quotas, your subscription will be charged for the overages. It may take up to 24 - hours for usage stats to update. -

- ) : ( -

- Your plan includes a limited amount of usage. If the usage on your project exceeds - these quotas, you may experience restrictions, as you are currently not billed for - overages. It may take up to 24 hours for usage stats to update. -

- )} - - {hasExceededAnyLimits && ( - - snap.setPanelKey( - currentPlan?.id === 'free' ? 'subscriptionPlan' : 'costControl' - ) - } - > - {currentPlan?.id === 'free' ? 'Upgrade plan' : 'Change spend cap'} - , - ]} - > - Your project can become unresponsive or enter read only mode.{' '} - {currentPlan?.id === 'free' - ? 'Please upgrade to the Pro plan to ensure that your project remains available.' - : 'Please disable spend cap to ensure that your project remains available.'} - - )} - - {isLoadingUsage ? ( -
- - - -
- ) : ( -
- {BILLING_BREAKDOWN_METRICS.map((metric, i) => { - const usageMeta = usage?.[metric.key] - const usageRatio = (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0) - - const hasLimit = (usageMeta?.limit ?? 0) > 0 - const usageCurrentLabel = - metric.units === 'bytes' - ? formatBytes(usageMeta?.usage) - : usageMeta?.usage?.toLocaleString() - const usageLimitLabel = - metric.units === 'bytes' - ? formatBytes(usageMeta?.limit) - : usageMeta?.limit.toLocaleString() - const usageLabel = `${usageCurrentLabel} ${hasLimit ? `of ${usageLimitLabel}` : ''}` - const percentageLabel = `${(usageRatio * 100).toFixed(2)}%` - - const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD - const isExceededLimit = hasLimit && usageRatio >= 1 - - const usageFee = subscription?.usage_fees?.find( - (item) => item.metric === metric.metric - ) - - return ( -
-
- -
-

- {metric.name} -

- -
- - {isUsageBillingEnabled && hasLimit && usageFee && ( - - -
- -
-
- - - -
-
- {usageFee.pricingStrategy === 'UNIT' ? ( -
-

- {largeNumberFormatter.format( - usageFee.pricingOptions.freeUnits - )}{' '} - {metric.unitName} included -

-

- then ${usageFee.pricingOptions.perUnitPrice} per{' '} - {metric.unitName} -

-
- ) : usageFee.pricingStrategy === 'PACKAGE' ? ( -
-

- {largeNumberFormatter.format( - usageFee.pricingOptions.freeUnits - )}{' '} - included -

- -

- then ${usageFee.pricingOptions.packagePrice} per{' '} - {largeNumberFormatter.format( - usageFee.pricingOptions.packageSize! - )} -

-
- ) : null} -
-
-
-
-
- )} - {isUsageBillingEnabled === false && - usageRatio >= USAGE_APPROACHING_THRESHOLD && ( - - - {isExceededLimit && !isUsageBillingEnabled ? ( -
- -

Exceeded limit

-
- ) : isApproachingLimit && !isUsageBillingEnabled ? ( -
- -

Approaching limit

-
- ) : null} -
- - - - -
-

- Exceeding your plans included usage will lead to restrictions to - your project. -

-

- Upgrade to a usage-based plan or disable the spend cap to avoid - restrictions. -

-
-
-
-
- )} -
- {usageMeta ? ( - usageMeta.available_in_plan ? ( - 0 - ? 'bg-scale-1100' - : isExceededLimit && !isUsageBillingEnabled - ? 'bg-red-900' - : isApproachingLimit && !isUsageBillingEnabled - ? 'bg-amber-900' - : 'bg-scale-1100' - } - bgClass="bg-gray-300 dark:bg-gray-600" - labelBottom={usageLabel} - labelBottomClass="!text-foreground-light" - labelTop={hasLimit ? percentageLabel : undefined} - labelTopClass={ - !hasLimit - ? '' - : isExceededLimit && !isUsageBillingEnabled - ? '!text-red-900' - : isApproachingLimit && !isUsageBillingEnabled - ? '!text-amber-900' - : '' - } - /> - ) : ( - // [Joshen] Needs a better CTA here -
-

Unavailable in your plan

- -
- ) - ) : ( -
-

- The usage for this metric is missing. -

-
- )} -
- ) - })} -
- )} - -

Upcoming cost for next invoice

-

- The following table shows your upcoming costs. Depending on your usage, the final amount - may vary. Next invoice on{' '} - {billingCycleEnd.format('MMM DD, YYYY')}. -

- - {isLoadingUpcomingInvoice ? ( -
- - - -
- ) : upcomingInvoiceError !== null ? ( - - - , - ]} - > - Try refreshing your browser, but if the issue persists, please reach out to us via - support. - - ) : ( - - - - - - - - - - - {fixedFees.map((item) => ( - - - - - - - ))} - - - {usageFees.length > 0 && ( - - - - - - - - - - <> - {usageFees.map((fee) => ( - - - - - - - ))} - - - - - )} - - - - - - - -
- Item - - Count - - Unit price - - Price -
{item.description ?? 'Unknown'}{item.quantity} - {item.unit_price === 0 ? 'FREE' : `$${item.unit_price}`} - ${item.amount}
- - - - {!showUsageFees ? `$${totalUsageFees}` : null} -
{fee.description} - {fee.unit_price ? `$${fee.unit_price}` : null} - ${fee.amount ?? 0}
Total - - ${upcomingInvoice?.amount_total ?? 0}
- )} -
- )} -
- ) -} - -export default BillingBreakdown diff --git a/studio/components/interfaces/BillingV2/Subscription/CostControl/CostControl.tsx b/studio/components/interfaces/BillingV2/Subscription/CostControl/CostControl.tsx deleted file mode 100644 index ea07062c41d..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/CostControl/CostControl.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { useParams } from 'common' -import { useTheme } from 'next-themes' -import ProjectUpdateDisabledTooltip from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useFlag } from 'hooks' -import { BASE_PATH } from 'lib/constants' -import Link from 'next/link' -import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' -import { Alert, Button, IconExternalLink } from 'ui' -import SpendCapSidePanel from './SpendCapSidePanel' -import Image from 'next/image' - -export interface CostControlProps {} - -const CostControl = ({}: CostControlProps) => { - const { ref: projectRef } = useParams() - const snap = useSubscriptionPageStateSnapshot() - const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') - const { resolvedTheme } = useTheme() - - const { data: subscription, isLoading } = useProjectSubscriptionV2Query({ projectRef }) - - const currentPlan = subscription?.plan - const isUsageBillingEnabled = subscription?.usage_billing_enabled ?? false - - const canChangeTier = - !projectUpdateDisabled && !['team', 'enterprise'].includes(currentPlan?.id || '') - - return ( - <> -
-
-
-
-
-

Cost Control

-

- Control whether to use beyond your plans included quota -

-
-
-

More information

-
- -
-

Spend cap

- -
- -
-
-
-
-
- {isLoading ? ( -
- - - -
- ) : ( -
- {['team', 'enterprise'].includes(currentPlan?.id || '') ? ( - - {currentPlan?.name || ''} plan requires you to have spend cap off at all times. Your - project will never become unresponsive. Only when your{' '} - - included usage - {' '} - is exceeded will you be charged for any additional usage. - - ) : ( -

- You can control whether your project is charged for additional usage beyond the{' '} - - included quota - {' '} - of your subscription plan. If you need to go beyond the included quota, simply - switch off your spend cap to pay for additional usage. -

- )} - -
-
-
- Spend Cap -
-
-
-

- Spend cap is {isUsageBillingEnabled ? 'disabled' : 'enabled'} -

-

- {isUsageBillingEnabled ? ( - You will be charged for any usage above the included quota. - ) : ( - - You won't be charged any extra for usage. However, your project could become - unresponsive or enter read only mode if you exceed the included quota. - - )} -

- {isUsageBillingEnabled && ( -

- Your project will never become unresponsive. Only when your usage reaches the - quota limit will you be charged for any excess usage. -

- )} - - - -
-
-
- )} -
- - - ) -} - -export default CostControl diff --git a/studio/components/interfaces/BillingV2/Subscription/CostControl/SpendCapSidePanel.tsx b/studio/components/interfaces/BillingV2/Subscription/CostControl/SpendCapSidePanel.tsx deleted file mode 100644 index 66f1559140c..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/CostControl/SpendCapSidePanel.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import clsx from 'clsx' -import { useParams } from 'common' -import { useTheme } from 'next-themes' -import Table from 'components/to-be-cleaned/Table' -import { useProjectSubscriptionUpdateMutation } from 'data/subscriptions/project-subscription-update-mutation' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useCheckPermissions, useStore } from 'hooks' -import { BASE_PATH, PRICING_TIER_PRODUCT_IDS } from 'lib/constants' -import Telemetry from 'lib/telemetry' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import { pricing } from 'shared-data/pricing' -import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' -import { Alert, Button, Collapsible, IconChevronRight, IconExternalLink, SidePanel } from 'ui' -import Image from 'next/image' - -const SPEND_CAP_OPTIONS: { - name: string - value: 'on' | 'off' - imageUrl: string - imageUrlLight: string -}[] = [ - { - name: 'Spend cap enabled', - value: 'on', - imageUrl: `${BASE_PATH}/img/spend-cap-on.png`, - imageUrlLight: `${BASE_PATH}/img/spend-cap-on--light.png`, - }, - { - name: 'Spend cap disabled', - value: 'off', - imageUrl: `${BASE_PATH}/img/spend-cap-off.png`, - imageUrlLight: `${BASE_PATH}/img/spend-cap-off--light.png`, - }, -] - -const SpendCapSidePanel = () => { - const { ui } = useStore() - const router = useRouter() - const { ref: projectRef } = useParams() - const { resolvedTheme } = useTheme() - - const [showUsageCosts, setShowUsageCosts] = useState(false) - const [selectedOption, setSelectedOption] = useState<'on' | 'off'>() - - const canUpdateSpendCap = useCheckPermissions( - PermissionAction.BILLING_WRITE, - 'stripe.subscriptions' - ) - - const snap = useSubscriptionPageStateSnapshot() - const visible = snap.panelKey === 'costControl' - const onClose = () => snap.setPanelKey(undefined) - - const { data: subscription, isLoading } = useProjectSubscriptionV2Query({ projectRef }) - const isFreePlan = subscription?.plan?.id === 'free' - const isSpendCapOn = !subscription?.usage_billing_enabled - const isTurningOnCap = !isSpendCapOn && selectedOption === 'on' - const hasChanges = selectedOption !== (isSpendCapOn ? 'on' : 'off') - - const { mutate: updateSubscriptionTier, isLoading: isUpdating } = - useProjectSubscriptionUpdateMutation({ - onSuccess: () => { - ui.setNotification({ - category: 'success', - message: `Successfully ${isTurningOnCap ? 'enabled' : 'disabled'} spend cap`, - }) - onClose() - }, - onError: (error) => { - ui.setNotification({ - error, - category: 'error', - message: `Unable to toggle spend cap: ${error.message}`, - }) - }, - }) - - useEffect(() => { - if (visible && subscription !== undefined) { - setSelectedOption(isSpendCapOn ? 'on' : 'off') - Telemetry.sendActivity( - { - activity: 'Side Panel Viewed', - source: 'Dashboard', - data: { - title: 'Spend cap', - section: 'Cost Control', - }, - projectRef, - }, - router - ) - } - }, [visible, isLoading, subscription, isSpendCapOn]) - - const onConfirm = async () => { - if (!projectRef) return console.error('Project ref is required') - - const tier = ( - selectedOption === 'on' ? PRICING_TIER_PRODUCT_IDS.PRO : PRICING_TIER_PRODUCT_IDS.PAYG - ) as 'tier_pro' | 'tier_payg' - updateSubscriptionTier({ projectRef, tier }) - } - - const billingMetricCategories: (keyof typeof pricing)[] = [ - 'database', - 'auth', - 'storage', - 'realtime', - 'edge_functions', - ] - - return ( - -

Spend cap

- - - } - tooltip={!canUpdateSpendCap ? 'You do not have permission to update spend cap' : undefined} - > - -
-

- Use the spend cap to manage project usage and costs, and control whether the project can - exceed the included quota allowance of any billed line item in a billing cycle -

- - - -
- -

- How are each resource charged after exceeding the included quota? -

-
-
- - - -

Item

-
- -

Rate

-
- - } - body={billingMetricCategories.map((categoryId) => { - const category = pricing[categoryId] - const usageItems = category.features.filter((it: any) => it.usage_based) - - return ( - <> - - -

{category.title}

-
- {null} -
- {usageItems.map((item: any) => { - return ( - - -

{item.title}

-
- -

{item.plans['pro']}

-
-
- ) - })} - - ) - })} - /> - - - - {isFreePlan && ( - snap.setPanelKey('subscriptionPlan')}> - View available plans - - } - > - Upgrade your plan to disable the spend cap - - )} - -
-
- {SPEND_CAP_OPTIONS.map((option) => { - const isSelected = selectedOption === option.value - - return ( -
{ - !isFreePlan && setSelectedOption(option.value) - Telemetry.sendActivity( - { - activity: 'Option Selected', - source: 'Dashboard', - data: { - title: 'Spend cap', - section: 'Cost Control', - option: option.name, - }, - projectRef, - }, - router - ) - }} - > - - -

- {option.name} -

-
- ) - })} -
-
- - {selectedOption === 'on' ? ( - - Exceeding the included quota allowance with spend cap enabled can cause your project - to become unresponsive or enter read only mode. - - ) : ( - - Your project will always remain responsive and active, and charges only apply when - exceeding the included quota limit. - - )} - - {hasChanges && ( - <> -

- {selectedOption === 'on' - ? 'Upon clicking confirm, spend cap will be enabled for your project and you will no longer be charged any extra for usage.' - : 'Upon clicking confirm, spend cap will be disabled for your project and you will be charged for any usage beyong the included quota.'} -

- - )} - - - - ) -} - -export default SpendCapSidePanel diff --git a/studio/components/interfaces/BillingV2/Subscription/Subscription.tsx b/studio/components/interfaces/BillingV2/Subscription/Subscription.tsx deleted file mode 100644 index db00e613126..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Subscription.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useRouter } from 'next/router' -import AddOns from './AddOns/AddOns' -import BillingBreakdown from './BillingBreakdown' -import CostControl from './CostControl/CostControl' -import SubscriptionTier from './Tier/SubscriptionTier' -import { SUBSCRIPTION_PANEL_KEYS, useSubscriptionPageStateSnapshot } from 'state/subscription-page' -import { useSelectedOrganization } from 'hooks' -import { useEffect } from 'react' -import InformationBox from 'components/ui/InformationBox' -import { Button, IconExternalLink, IconInfo } from 'ui' -import Link from 'next/link' - -export interface SubscriptionProps {} - -const Subscription = ({}: SubscriptionProps) => { - const router = useRouter() - const organization = useSelectedOrganization() - const snap = useSubscriptionPageStateSnapshot() - const allowedValues = [ - 'subscriptionPlan', - 'costControl', - 'computeInstance', - 'pitr', - 'customDomain', - ] - - const panel = router.query.panel - const isOrgBilling = !!organization?.subscription_id - - useEffect(() => { - if (panel && typeof panel === 'string' && allowedValues.includes(panel)) { - snap.setPanelKey(panel as SUBSCRIPTION_PANEL_KEYS) - } - }, [panel]) - - // No need to bother rendering, we'll redirect anyway - if (isOrgBilling) return null - - return ( - <> -
-
-

Subscription

-
-
- -
- } - defaultVisibility={true} - hideCollapse - title="We're upgrading our billing system" - description={ -
-

- This organization uses the legacy project-based billing. We’ve recently made some - big improvements to our billing system. To migrate to the new organization-based - billing, head over to your{' '} - - organization billing settings - - . -

- -
- - -
-
- } - /> -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- - ) -} - -export default Subscription diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/ExitSurveyModal.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/ExitSurveyModal.tsx deleted file mode 100644 index 047e9973a20..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/ExitSurveyModal.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import HCaptcha from '@hcaptcha/react-hcaptcha' -import { useQueryClient } from '@tanstack/react-query' -import { includes, without } from 'lodash' -import { useRouter } from 'next/router' -import { useReducer, useRef, useState } from 'react' - -import { useParams } from 'common' -import ProjectUpdateDisabledTooltip from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip' -import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send' -import { setProjectStatus } from 'data/projects/projects-query' -import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useProjectSubscriptionUpdateMutation } from 'data/subscriptions/project-subscription-update-mutation' -import { useFlag, useStore } from 'hooks' -import { PROJECT_STATUS } from 'lib/constants' -import { Button, Input, Modal } from 'ui' -import { CANCELLATION_REASONS } from '../../Billing.constants' - -export interface ExitSurveyModalProps { - visible: boolean - onClose: (success?: boolean) => void -} - -// [Joshen] For context - Exit survey is only when going to free plan from a paid plan - -const ExitSurveyModal = ({ visible, onClose }: ExitSurveyModalProps) => { - const queryClient = useQueryClient() - const { ui } = useStore() - const { ref: projectRef } = useParams() - const captchaRef = useRef(null) - const router = useRouter() - - const [message, setMessage] = useState('') - const [captchaToken, setCaptchaToken] = useState(null) - const [selectedReasons, dispatchSelectedReasons] = useReducer(reducer, []) - - const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') - const { data: addons } = useProjectAddonsQuery({ projectRef }) - const { mutateAsync: sendExitSurvey, isLoading: isSubmittingFeedback } = - useSendDowngradeFeedbackMutation() - const { mutateAsync: updateSubscriptionTier, isLoading: isUpdatingSubscription } = - useProjectSubscriptionUpdateMutation({ - onError: (error) => { - return ui.setNotification({ - error, - category: 'error', - message: `Failed to cancel subscription: ${error.message}`, - }) - }, - }) - const isSubmitting = isUpdatingSubscription || isSubmittingFeedback - const subscriptionAddons = addons?.selected_addons ?? [] - const hasComputeInstance = subscriptionAddons.find((addon) => addon.type === 'compute_instance') - - 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 resetCaptcha = () => { - setCaptchaToken(null) - captchaRef.current?.resetCaptcha() - } - - const onSubmit = async () => { - if (selectedReasons.length === 0) { - return ui.setNotification({ - category: 'error', - message: 'Please select at least one reason for canceling your subscription', - }) - } - - let token = captchaToken - - if (!token) { - const captchaResponse = await captchaRef.current?.execute({ async: true }) - token = captchaResponse?.response ?? null - await downgradeProject() - } - } - - const downgradeProject = async (values?: any) => { - // Update the subscription first, followed by posting the exit survey if successful - // If compute instance is present within the existing subscription, then a restart will be triggered - if (!projectRef) return console.error('Project ref is required') - - try { - await updateSubscriptionTier({ projectRef, tier: 'tier_free' }) - resetCaptcha() - } catch (error) { - return - } - - try { - await sendExitSurvey({ - projectRef, - reasons: selectedReasons.reduce((a, b) => `${a}- ${b}\n`, ''), - message, - exitAction: 'downgrade', - }) - } finally { - } - - ui.setNotification({ - category: 'success', - duration: hasComputeInstance ? 8000 : 4000, - message: hasComputeInstance - ? 'Your project has been downgraded and is currently restarting to update its instance size' - : 'Successfully downgraded project to the free plan', - }) - if (hasComputeInstance) { - setProjectStatus(queryClient, projectRef, PROJECT_STATUS.RESTORING) - router.push(`/project/${projectRef}`) - } - onClose(true) - window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) - } - - if (!visible) return null - - return ( - <> -
- { - setCaptchaToken(token) - if (document !== undefined) document.body.classList.remove('!pointer-events-auto') - }} - onExpire={() => setCaptchaToken(null)} - onOpen={() => { - // [Joshen] This is to ensure that hCaptcha popup remains clickable - if (document !== undefined) document.body.classList.add('!pointer-events-auto') - }} - onClose={() => { - if (document !== undefined) document.body.classList.remove('!pointer-events-auto') - }} - /> -
- - - -
-

- 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. -

-
-
- {CANCELLATION_REASONS.map((option) => { - const active = selectedReasons.find((x) => x === option) - return ( - - ) - })} -
-
- setMessage(event.target.value)} - label="Anything else that we can improve on?" - /> -
-
-
-
- -
-

- The unused amount for the remaining of your billing cycle will be refunded as credits -

-
- - - - -
-
-
- - ) -} - -export default ExitSurveyModal diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/MembersExceedLimitModal.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/MembersExceedLimitModal.tsx deleted file mode 100644 index 585a5c9813c..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/MembersExceedLimitModal.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' -import { useSelectedOrganization } from 'hooks' -import { Button, Modal } from 'ui' - -export interface MembersExceedLimitModalProps { - visible: boolean - onClose: () => void -} - -const MembersExceedLimitModal = ({ visible, onClose }: MembersExceedLimitModalProps) => { - const selectedOrganization = useSelectedOrganization() - const slug = selectedOrganization?.slug - const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery({ slug }) - - return ( - -
- -
-

- The following members have reached their maximum limits for the number of active free - plan projects within organizations where they are an administrator or owner: -

-
    - {(membersExceededLimit || []).map((member, idx: number) => ( -
  • - {member.username || member.primary_email} (Limit: {member.free_project_limit} free - projects) -
  • - ))} -
-

- These members will need to either delete, pause, or upgrade one or more of these - projects before you're able to downgrade this project. -

-
-
- - -
- -
-
-
-
- ) -} - -export default MembersExceedLimitModal diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/PaymentMethodSelection.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/PaymentMethodSelection.tsx deleted file mode 100644 index e033ed80263..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/PaymentMethodSelection.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import * as Tooltip from '@radix-ui/react-tooltip' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useEffect, useMemo, useState } from 'react' - -import { useParams } from 'common' -import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2' -import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' -import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query' -import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' -import { BASE_PATH } from 'lib/constants' -import { getURL } from 'lib/helpers' -import { Button, IconAlertCircle, IconCreditCard, IconLoader, IconPlus, Listbox } from 'ui' - -export interface PaymentMethodSelectionProps { - selectedPaymentMethod?: string - onSelectPaymentMethod: (id: string) => void -} - -const PaymentMethodSelection = ({ - selectedPaymentMethod, - onSelectPaymentMethod, -}: PaymentMethodSelectionProps) => { - const { ui } = useStore() - const { ref: projectRef } = useParams() - const selectedOrganization = useSelectedOrganization() - const slug = selectedOrganization?.slug - const [showAddNewPaymentMethodModal, setShowAddNewPaymentMethodModal] = useState(false) - - const { - data, - isLoading, - isSuccess: loadedPaymentMethods, - refetch: refetchPaymentMethods, - } = useOrganizationPaymentMethodsQuery({ slug }) - const paymentMethods = useMemo(() => data ?? [], [data]) - - const { data: customerProfile, isSuccess: loadedCustomerProfile } = - useOrganizationCustomerProfileQuery({ slug }) - - const canUpdatePaymentMethods = useCheckPermissions( - PermissionAction.BILLING_WRITE, - 'stripe.payment_methods' - ) - - useEffect(() => { - if (loadedPaymentMethods && loadedCustomerProfile && paymentMethods.length > 0) { - const selectedPaymentMethodExists = paymentMethods.some( - (it) => it.id === selectedPaymentMethod - ) - - if (!selectedPaymentMethod || !selectedPaymentMethodExists) { - const defaultPaymentMethod = paymentMethods.find( - (method) => method.id === customerProfile.invoice_settings.default_payment_method - ) - if (defaultPaymentMethod !== undefined) { - onSelectPaymentMethod(defaultPaymentMethod.id) - } else { - onSelectPaymentMethod(paymentMethods[0].id) - } - } - } - }, [ - loadedPaymentMethods, - loadedCustomerProfile, - selectedPaymentMethod, - customerProfile, - paymentMethods, - onSelectPaymentMethod, - ]) - - return ( - <> -
-

Select payment method

- {isLoading ? ( -
- -

Retrieving payment methods

-
- ) : paymentMethods.length === 0 ? ( -
-
- -

No saved payment methods

-
- - - - - - {!canUpdatePaymentMethods && ( - - - -
- - You need additional permissions to add new payment methods to this - organization - -
-
-
- )} -
-
- ) : ( - - {paymentMethods.map((method: any) => { - const label = `•••• •••• •••• ${method.card.last4}` - return ( - { - return ( - Credit Card Brand - ) - }} - > -
{label}
-
- ) - })} -
setShowAddNewPaymentMethodModal(true)} - > - -

- Add new payment method -

-
-
- )} -
- - setShowAddNewPaymentMethodModal(false)} - onConfirm={async () => { - setShowAddNewPaymentMethodModal(false) - ui.setNotification({ - category: 'success', - message: 'Successfully added new payment method', - }) - const { data } = await refetchPaymentMethods() - if (data?.length) { - // Preselect the card that was just added - const mostRecentPaymentMethod = data.reduce( - (prev, current) => (prev.created > current.created ? prev : current), - data[0] - ) - onSelectPaymentMethod(mostRecentPaymentMethod.id) - } - }} - /> - - ) -} - -export default PaymentMethodSelection diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionPaymentMethod.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionPaymentMethod.tsx deleted file mode 100644 index e5809648d80..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionPaymentMethod.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import * as Tooltip from '@radix-ui/react-tooltip' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useState } from 'react' - -import { useParams } from 'common' -import Panel from 'components/ui/Panel' -import { - SubscriptionTier, - updateSubscriptionTier, -} from 'data/subscriptions/project-subscription-update-mutation' -import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query' -import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' -import { BASE_PATH } from 'lib/constants' -import Link from 'next/link' -import { Button, IconCreditCard, Modal } from 'ui' -import PaymentMethodSelection from './PaymentMethodSelection' - -export interface SubscriptionTierProps { - subscription: ProjectSubscriptionResponse - onSubscriptionUpdated: () => void -} - -const SubscriptionPaymentMethod = ({ - subscription, - onSubscriptionUpdated, -}: SubscriptionTierProps) => { - const { ref: projectRef } = useParams() - const { ui } = useStore() - - const selectedOrganization = useSelectedOrganization() - const currentOrgSlug = selectedOrganization?.slug - - const canUpdatePaymentMethod = useCheckPermissions( - PermissionAction.BILLING_WRITE, - 'stripe.subscriptions' - ) - - const [isSubmitting, setIsSubmitting] = useState(false) - const [showPaymentEditModal, setShowPaymentEditModal] = useState(false) - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState( - subscription.payment_method_id || '' - ) - - const onConfirmPaymentMethod = async () => { - setIsSubmitting(true) - - if (selectedPaymentMethod) { - if (!projectRef) return console.error('Project ref is required') - if (!selectedPaymentMethod) { - return ui.setNotification({ category: 'error', message: 'Please select a payment method' }) - } - - const selectedTier = - subscription.plan.id === 'pro' && subscription.usage_billing_enabled - ? 'tier_payg' - : `tier_${subscription.plan.id}` - - try { - setIsSubmitting(true) - await updateSubscriptionTier({ - projectRef, - tier: selectedTier as SubscriptionTier, - paymentMethod: selectedPaymentMethod, - }) - - setShowPaymentEditModal(false) - - ui.setNotification({ - category: 'success', - message: `Successfully updated payment method!`, - }) - - onSubscriptionUpdated() - } catch (error: any) { - ui.setNotification({ - error, - category: 'error', - message: `Unable to update subscription: ${error.message}`, - }) - } finally { - setIsSubmitting(false) - } - } - - setIsSubmitting(false) - } - - if (subscription.payment_method_type === 'none') return null - - return ( - <> - - - {subscription.payment_method_type === 'card' && ( - <> -
- {subscription.payment_method_card_details?.brand ? ( - Credit card brand - ) : ( - - )} - - **** **** ****{' '} - {subscription?.payment_method_card_details?.last_4_digits || '****'} - -
-
- - Expires {subscription?.payment_method_card_details?.expiry_month || '-'}/ - {subscription?.payment_method_card_details?.expiry_year?.toString()?.slice(-2) || - '-'} - -
- - )} - - {subscription?.payment_method_type === 'invoice' && ( - <> -
-
-

Payment via invoice

-

- You get a monthly invoice and payment link via email. -

-
-
- - )} - - - -
- -
-
- {subscription?.payment_method_type !== 'card' ? ( - - - -
- - Please email support@supbase.io to change your payment method. - -
-
-
- ) : !canUpdatePaymentMethod ? ( - - - -
- - You do not have permission to change the payment method - -
-
-
- ) : null} -
-
-
- - setShowPaymentEditModal(false)} - onConfirm={onConfirmPaymentMethod} - overlayClassName="pointer-events-none" - header={`Change payment method`} - > - -
-

- Upon clicking confirm, all future charges will be deducted from the selected payment - method. There are no immediate charges. Changing the payment method for this project - does not affect the payment method for other projects. -

-

- To remove unused or expired payment methods, head to your{' '} - - organization's billing settings - - . -

-
- -
-
-
-
- - ) -} - -export default SubscriptionPaymentMethod diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionTier.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionTier.tsx deleted file mode 100644 index 600a210a1ea..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/SubscriptionTier.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import dayjs from 'dayjs' -import Link from 'next/link' - -import { useParams } from 'common' -import ProjectUpdateDisabledTooltip from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import SparkBar from 'components/ui/SparkBar' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useFlag, useSelectedOrganization } from 'hooks' -import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' -import { Alert, Button, IconExternalLink } from 'ui' -import SubscriptionPaymentMethod from './SubscriptionPaymentMethod' -import TierUpdateSidePanel from './TierUpdateSidePanel' - -export interface SubscriptionTierProps {} - -const SubscriptionTier = ({}: SubscriptionTierProps) => { - const { ref: projectRef } = useParams() - const selectedOrganization = useSelectedOrganization() - const orgSlug = selectedOrganization?.slug ?? '' - const snap = useSubscriptionPageStateSnapshot() - const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') - const { data: subscription, isLoading, refetch } = useProjectSubscriptionV2Query({ projectRef }) - - const currentPlan = subscription?.plan - const tierName = currentPlan?.name || 'Unknown' - - const billingCycleStart = dayjs.unix(subscription?.current_period_start ?? 0).utc() - const billingCycleEnd = dayjs.unix(subscription?.current_period_end ?? 0).utc() - const daysToCycleEnd = billingCycleEnd.diff(dayjs(), 'days') - const daysWithinCycle = billingCycleEnd.diff(billingCycleStart, 'days') - - const canChangeTier = - !projectUpdateDisabled && !['team', 'enterprise'].includes(currentPlan?.id ?? '') - - return ( - <> -
-
-
-

Subscription plan

-
- To manage your billing address, emails or Tax ID, head to your{' '} - - organization settings - - . -
-
-

More information

-
- -
-

Pricing

- -
- -
-
-
-
- {isLoading ? ( -
- - - -
- ) : ( -
-
-

This project is currently on the plan:

-

{tierName}

-
-
- - - -
- {!canChangeTier && - (projectUpdateDisabled ? ( - - We have temporarily disabled project and subscription changes - our engineers are - working on a fix. - - ) : ( - - - - -
, - ]} - > - Please contact us if you'd like to change your plan. - - ))} - {!subscription?.usage_billing_enabled && ( - snap.setPanelKey('subscriptionPlan')}> - Upgrade Plan - - ) : ( - - ) - } - > -

- When this project exceeds its{' '} - - included usage quotas - - , it may become unresponsive.{' '} - {currentPlan?.id === 'free' ? ( -

- If you wish to exceed the included usage, you should upgrade to a paid plan. -

- ) : ( -

- You currently have Spend Cap enabled - when you exceed your plan's limit, you - will experience restrictions. To scale seamlessly and pay for over-usage, you - can adjust your Cost Control settings. -

- )} -

-
- )} - - - {subscription && ( - refetch()} - /> - )} -
- )} - - - - - ) -} - -export default SubscriptionTier diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/TierUpdateSidePanel.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/TierUpdateSidePanel.tsx deleted file mode 100644 index 4ca11eb72a4..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/TierUpdateSidePanel.tsx +++ /dev/null @@ -1,420 +0,0 @@ -import * as Tooltip from '@radix-ui/react-tooltip' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import clsx from 'clsx' -import Link from 'next/link' -import { useEffect, useState } from 'react' - -import { useParams } from 'common' -import InformationBox from 'components/ui/InformationBox' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' -import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useProjectPlansQuery } from 'data/subscriptions/project-plans-query' -import { useProjectSubscriptionUpdateMutation } from 'data/subscriptions/project-subscription-update-mutation' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' -import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' -import Telemetry from 'lib/telemetry' -import { useRouter } from 'next/router' -import { plans as subscriptionsPlans } from 'shared-data/plans' -import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' -import { Alert, Button, IconCheck, IconExternalLink, IconInfo, Modal, SidePanel } from 'ui' -import EnterpriseCard from './EnterpriseCard' -import ExitSurveyModal from './ExitSurveyModal' -import MembersExceedLimitModal from './MembersExceedLimitModal' -import PaymentMethodSelection from './PaymentMethodSelection' - -const TierUpdateSidePanel = () => { - const { ui } = useStore() - const router = useRouter() - const selectedOrganization = useSelectedOrganization() - const slug = selectedOrganization?.slug - const { ref: projectRef } = useParams() - - const [showExitSurvey, setShowExitSurvey] = useState(false) - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState() - const [showDowngradeError, setShowDowngradeError] = useState(false) - const [selectedTier, setSelectedTier] = useState<'tier_free' | 'tier_pro' | 'tier_team'>() - - const canUpdateSubscription = useCheckPermissions( - PermissionAction.BILLING_WRITE, - 'stripe.subscriptions' - ) - - const snap = useSubscriptionPageStateSnapshot() - const visible = snap.panelKey === 'subscriptionPlan' - const onClose = () => snap.setPanelKey(undefined) - - const { data: plans, isLoading: isLoadingPlans } = useProjectPlansQuery({ projectRef }) - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) - const { data: addons } = useProjectAddonsQuery({ projectRef }) - const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery({ slug }) - const { mutate: updateSubscriptionTier, isLoading: isUpdating } = - useProjectSubscriptionUpdateMutation({ - onSuccess: () => { - ui.setNotification({ - category: 'success', - message: `Successfully updated subscription to ${selectedTierMeta?.name}!`, - }) - setSelectedTier(undefined) - onClose() - window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) - }, - }) - - const availablePlans = plans ?? [] - const subscriptionAddons = addons?.selected_addons ?? [] - - const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 - const selectedTierMeta = subscriptionsPlans.find((tier) => tier.id === selectedTier) - const selectedPlanMeta = availablePlans.find( - (plan) => plan.id === selectedTier?.split('tier_')[1] - ) - - useEffect(() => { - if (visible) { - setSelectedTier(undefined) - Telemetry.sendActivity( - { - activity: 'Side Panel Viewed', - source: 'Dashboard', - data: { - title: 'Change Subscription Plan', - section: 'Subscription plan', - }, - projectRef, - }, - router - ) - } - }, [visible]) - - const onConfirmDowngrade = () => { - setSelectedTier(undefined) - if (hasMembersExceedingFreeTierLimit) { - setShowDowngradeError(true) - } else { - setShowExitSurvey(true) - } - } - - const onUpdateSubscription = async () => { - if (!projectRef) return console.error('Project ref is required') - if (!selectedTier) return console.error('Selected plan is required') - if (!selectedPaymentMethod) { - return ui.setNotification({ category: 'error', message: 'Please select a payment method' }) - } - - updateSubscriptionTier({ - projectRef, - tier: selectedTier, - paymentMethod: selectedPaymentMethod, - }) - } - - return ( - <> - onClose()} - header={ -
-

Change subscription plan

- -
- } - > - - } - defaultVisibility={true} - hideCollapse - title="We're upgrading our billing system" - className="mt-4" - description={ -
-

- This organization uses the legacy project-based billing. We’ve recently made some - big improvements to our billing system. To migrate to the new organization-based - billing, head over to your{' '} - - organization billing settings - - . -

- -
- - -
-
- } - /> - -
- {subscriptionsPlans.map((plan) => { - const planMeta = availablePlans.find((p) => p.id === plan.id.split('tier_')[1]) - const tierMeta = subscriptionsPlans.find((it) => it.id === plan.id) - const price = planMeta?.price ?? 0 - const isDowngradeOption = planMeta?.change_type === 'downgrade' - const isCurrentPlan = planMeta?.id === subscription?.plan?.id - - if (plan.id === 'tier_enterprise') { - return - } - - return ( -
-
-
-

{plan.name}

- {isCurrentPlan ? ( -
- Current plan -
- ) : plan.nameBadge ? ( -
- {plan.nameBadge} -
- ) : ( - <> - )} -
-
- {(price ?? 0) > 0 &&

From

} - {isLoadingPlans ? ( -
- -
- ) : ( -

${price}

- )} -

{tierMeta?.costUnit}

-
-
-
- {tierMeta?.warningLegacy} -
-
- {isCurrentPlan ? ( - - ) : plan.id !== PRICING_TIER_PRODUCT_IDS.TEAM ? ( - - -
- -
-
- {!canUpdateSubscription ? ( - - - -
- - You do not have permission to change the subscription plan. - -
-
-
- ) : null} -
- ) : ( - - )} - -
- -
    - {plan.features.map((feature) => ( -
  • -
    -
    -

    {feature}

    -
  • - ))} -
-
- - {plan.footer && ( -
-

{plan.footer}

-
- )} -
- ) - })} -
- - - - setSelectedTier(undefined)} - onConfirm={onConfirmDowngrade} - header={`Confirm to downgrade to ${selectedTierMeta?.name}`} - > - {/* [JOSHEN] We could make this better by only showing a danger warning if the project is already above the free plan limits */} - -
- -

- If you're already past the limits of the free plan, your project could become - unresponsive or enter read only mode. -

- {subscriptionAddons.length > 0 && ( - <> -

- Your project's add ons will also be removed, which includes: -

-
    - {subscriptionAddons.map((addon) => ( -
  • - {addon.variant.name}{' '} - {addon.type === 'compute_instance' - ? 'compute instance' - : addon.type === 'pitr' - ? 'PITR' - : ''} -
  • - ))} -
- - )} -
-
-
-
- - setSelectedTier(undefined)} - onConfirm={onUpdateSubscription} - overlayClassName="pointer-events-none" - header={`Confirm to upgrade to ${selectedTierMeta?.name}`} - > - -
-

- Upon clicking confirm, the amount of ${selectedPlanMeta?.price ?? 'Unknown'} will be - added to your monthly invoice and your credit card will be charged immediately. - Changing the plan resets your billing cycle and may result in a prorated charge for - previous usage. -

-

- You will also be able to change your project's add-ons after upgrading your project's - plan. -

-
- -
-
-
-
- - setShowDowngradeError(false)} - /> - - { - setShowExitSurvey(false) - if (success) onClose() - }} - /> - - ) -} - -export default TierUpdateSidePanel diff --git a/studio/components/interfaces/BillingV2/Usage/Activity.tsx b/studio/components/interfaces/BillingV2/Usage/Activity.tsx deleted file mode 100644 index 29fff89ff43..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/Activity.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useDailyStatsQuery } from 'data/analytics/daily-stats-query' -import { DataPoint } from 'data/analytics/constants' -import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query' -import UsageSection from './UsageSection' - -export interface ActivityProps { - projectRef: string - startDate: string | undefined - endDate: string | undefined - subscription: ProjectSubscriptionResponse | undefined - currentBillingCycleSelected: boolean -} - -const Activity = ({ - projectRef, - subscription, - startDate, - endDate, - currentBillingCycleSelected, -}: ActivityProps) => { - const { data: mauData, isLoading: isLoadingMauData } = useDailyStatsQuery({ - projectRef, - attribute: 'total_auth_billing_period_mau', - interval: '1d', - startDate, - endDate, - }) - - const { data: mauSSOData, isLoading: isLoadingMauSSOData } = useDailyStatsQuery({ - projectRef, - attribute: 'total_auth_billing_period_sso_mau', - interval: '1d', - startDate, - endDate, - }) - - const { data: assetTransformationsData, isLoading: isLoadingAssetTransformationsData } = - useDailyStatsQuery({ - projectRef, - attribute: 'total_storage_image_render_count', - interval: '1d', - startDate, - endDate, - }) - - const { data: funcInvocationsData, isLoading: isLoadingFuncInvocationsData } = useDailyStatsQuery( - { - projectRef, - attribute: 'total_func_invocations', - interval: '1d', - startDate, - endDate, - } - ) - - const { data: realtimeMessagesData, isLoading: isLoadingRealtimeMessagesData } = - useDailyStatsQuery({ - projectRef, - attribute: 'total_realtime_message_count', - interval: '1d', - startDate, - endDate, - }) - - const { data: realtimeConnectionsData, isLoading: isLoadingRealtimeConnectionsData } = - useDailyStatsQuery({ - projectRef, - attribute: 'total_realtime_peak_connection', - interval: '1d', - startDate, - endDate, - }) - - const chartMeta: { - [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean; hasNoData: boolean } - } = { - monthly_active_users: { - data: mauData?.data ?? [], - margin: 18, - isLoading: isLoadingMauData, - hasNoData: mauData?.hasNoData ?? false, - }, - monthly_active_sso_users: { - data: mauSSOData?.data ?? [], - margin: 20, - isLoading: isLoadingMauSSOData, - hasNoData: mauSSOData?.hasNoData ?? false, - }, - storage_image_render_count: { - data: assetTransformationsData?.data ?? [], - margin: 0, - isLoading: isLoadingMauSSOData, - hasNoData: assetTransformationsData?.hasNoData ?? false, - }, - func_invocations: { - data: funcInvocationsData?.data ?? [], - margin: 26, - isLoading: isLoadingFuncInvocationsData, - hasNoData: funcInvocationsData?.hasNoData ?? false, - }, - realtime_message_count: { - data: realtimeMessagesData?.data ?? [], - margin: 38, - isLoading: isLoadingRealtimeMessagesData, - hasNoData: realtimeMessagesData?.hasNoData ?? false, - }, - realtime_peak_connection: { - data: realtimeConnectionsData?.data ?? [], - margin: 0, - isLoading: isLoadingRealtimeConnectionsData, - hasNoData: realtimeConnectionsData?.hasNoData ?? false, - }, - } - - return ( - - ) -} - -export default Activity diff --git a/studio/components/interfaces/BillingV2/Usage/Bandwidth.tsx b/studio/components/interfaces/BillingV2/Usage/Bandwidth.tsx deleted file mode 100644 index 55391989118..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/Bandwidth.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { DataPoint } from 'data/analytics/constants' -import { useDailyStatsQuery } from 'data/analytics/daily-stats-query' -import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query' -import UsageSection from './UsageSection' - -export interface BandwidthProps { - projectRef: string - startDate: string | undefined - endDate: string | undefined - subscription: ProjectSubscriptionResponse | undefined - currentBillingCycleSelected: boolean -} - -const Bandwidth = ({ - projectRef, - subscription, - startDate, - endDate, - currentBillingCycleSelected, -}: BandwidthProps) => { - const { data: dbEgressData, isLoading: isLoadingDbEgressData } = useDailyStatsQuery({ - projectRef, - attribute: 'total_egress_modified', - interval: '1d', - startDate, - endDate, - }) - - const { data: storageEgressData, isLoading: isLoadingStorageEgressData } = useDailyStatsQuery({ - projectRef, - attribute: 'total_storage_egress', - interval: '1d', - startDate, - endDate, - }) - - const chartMeta: { - [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean; hasNoData: boolean } - } = { - db_egress: { - data: dbEgressData?.data ?? [], - margin: 16, - isLoading: isLoadingDbEgressData, - hasNoData: dbEgressData?.hasNoData ?? false, - }, - storage_egress: { - data: storageEgressData?.data ?? [], - margin: 14, - isLoading: isLoadingStorageEgressData, - hasNoData: storageEgressData?.hasNoData ?? false, - }, - } - - return ( - - ) -} - -export default Bandwidth diff --git a/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx b/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx deleted file mode 100644 index dd7c457c378..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import dayjs from 'dayjs' -import { IconBarChart2 } from 'ui' - -import Panel from 'components/ui/Panel' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { DataPoint } from 'data/analytics/constants' -import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query' -import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' -import { getAddons } from '../Subscription/Subscription.utils' -import SectionContent from './SectionContent' -import SectionHeader from './SectionHeader' -import { USAGE_CATEGORIES } from './Usage.constants' -import { getUpgradeUrl } from './Usage.utils' -import UsageBarChart from './UsageBarChart' -import { CPUWarnings, DiskIOBandwidthWarnings, RAMWarnings } from './UsageWarningAlerts' - -export interface InfrastructureProps { - projectRef: string - startDate?: string - endDate?: string - currentBillingCycleSelected: boolean -} - -const Infrastructure = ({ - projectRef, - startDate, - endDate, - currentBillingCycleSelected, -}: InfrastructureProps) => { - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) - const { data: resourceWarnings } = useResourceWarningsQuery() - const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef) - const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'infra') - - const upgradeUrl = getUpgradeUrl(projectRef, subscription) - const isFreePlan = subscription?.plan?.id === 'free' - - const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef }) - const selectedAddons = addons?.selected_addons ?? [] - - const { computeInstance } = getAddons(selectedAddons) - const currentComputeInstanceSpecs = computeInstance?.variant?.meta ?? { - baseline_disk_io_mbs: 87, - max_disk_io_mbs: 2085, - cpu_cores: 2, - cpu_dedicated: true, - memory_gb: 1, - } - - // Switch to hourly interval, if the timeframe is <48 hours - let interval: '1d' | '1h' = '1d' - let dateFormat = 'DD MMM' - if (startDate && endDate) { - const diffInHours = dayjs(endDate).diff(startDate, 'hours') - - if (diffInHours <= 48) { - interval = '1h' - dateFormat = 'h a' - } - } - - const { data: cpuUsageData, isLoading: isLoadingCpuUsageData } = useInfraMonitoringQuery({ - projectRef, - attribute: 'max_cpu_usage', - interval, - startDate, - endDate, - dateFormat, - }) - - const { data: memoryUsageData, isLoading: isLoadingMemoryUsageData } = useInfraMonitoringQuery({ - projectRef, - attribute: 'ram_usage', - interval, - startDate, - endDate, - dateFormat, - }) - - const { data: ioBudgetData, isLoading: isLoadingIoBudgetData } = useInfraMonitoringQuery({ - projectRef, - attribute: 'disk_io_consumption', - interval, - startDate, - endDate, - dateFormat, - }) - - const hasLatest = dayjs(endDate!).isAfter(dayjs().startOf('day')) - - const latestIoBudgetConsumption = - hasLatest && ioBudgetData?.data?.slice(-1)?.[0] - ? Number(ioBudgetData.data.slice(-1)[0].disk_io_consumption) - : 0 - - const highestIoBudgetConsumption = Math.max( - ...(ioBudgetData?.data || []).map((x) => Number(x.disk_io_consumption) ?? 0), - 0 - ) - - const chartMeta: { [key: string]: { data: DataPoint[]; isLoading: boolean } } = { - max_cpu_usage: { - isLoading: isLoadingCpuUsageData, - data: cpuUsageData?.data ?? [], - }, - ram_usage: { - isLoading: isLoadingMemoryUsageData, - data: memoryUsageData?.data ?? [], - }, - disk_io_consumption: { - isLoading: isLoadingIoBudgetData, - data: ioBudgetData?.data ?? [], - }, - } - - if (categoryMeta === undefined) return null - - return ( - <> - - {categoryMeta.attributes.map((attribute) => { - const chartData = chartMeta[attribute.key]?.data ?? [] - - return ( -
- - {attribute.key === 'disk_io_consumption' && ( - <> - -
-

Disk IO Bandwidth

- - {currentComputeInstanceSpecs.baseline_disk_io_mbs === - currentComputeInstanceSpecs.max_disk_io_mbs ? ( -

- Your current compute can has a baseline and maximum disk throughput of{' '} - {currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps. -

- ) : ( -

- Your current compute can burst up to{' '} - {currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps for 30 - minutes a day and reverts to the baseline performance of{' '} - {currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()} Mbps. -

- )} -
-
-

Overview

-
-

Current compute instance

-

{computeInstance?.variant?.name ?? 'Micro'}

-
-
-

- Maximum IO Bandwidth (burst limit) -

-

- {currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps -

-
-
-

Baseline IO Bandwidth

-

- {currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()} Mbps -

-
- {currentComputeInstanceSpecs.max_disk_io_mbs !== - currentComputeInstanceSpecs?.baseline_disk_io_mbs && ( -
-

Daily burst time limit

-

30 mins

-
- )} -
- - )} - {attribute.key === 'max_cpu_usage' && ( - - )} - {attribute.key === 'ram_usage' && ( - - )} - -
-
- {attribute.key === 'disk_io_consumption' ? ( -

Disk IO consumed per {interval === '1d' ? 'day' : 'hour'}

- ) : ( -

- Max{' '} - - {attribute.name} - {' '} - utilization per {interval === '1d' ? 'day' : 'hour'} -

- )} -
- - {attribute.key === 'ram_usage' && ( -
-

- Your compute instance has {currentComputeInstanceSpecs.memory_gb} GB of - memory. -

- {currentComputeInstanceSpecs.memory_gb === 1 && ( -

- As your project is running on the smallest compute instance, it is not - unusual for your project to have a base memory usage of ~50%. -

- )} -
- )} - - {attribute.key === 'max_cpu_usage' && ( -

- Your compute instance has {currentComputeInstanceSpecs.cpu_cores} CPU cores. -

- )} - - {attribute.chartDescription.split('\n').map((paragraph, idx) => ( -

- {paragraph} -

- ))} -
- {chartMeta[attribute.key].isLoading ? ( -
- - - -
- ) : chartData.length ? ( - `${Math.round(Number(value))}%`} - tooltipFormatter={(value) => `${value}%`} - yLimit={100} - /> - ) : ( - - -
- -

No data in period

-

- May take a few minutes to show -

-
-
-
- )} -
-
- ) - })} - - ) -} - -export default Infrastructure diff --git a/studio/components/interfaces/BillingV2/Usage/SectionContent.tsx b/studio/components/interfaces/BillingV2/Usage/SectionContent.tsx deleted file mode 100644 index 594210f0702..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/SectionContent.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import Link from 'next/link' -import { PropsWithChildren } from 'react' -import { Badge, IconExternalLink } from 'ui' -import { CategoryAttribute } from './Usage.constants' - -export interface SectionContent { - section: CategoryAttribute - includedInPlan?: boolean -} - -const SectionContent = ({ - section, - includedInPlan, - children, -}: PropsWithChildren) => { - const { name, description, links } = section - - return ( -
-
-
-
-
-
-
-

{name}

- {includedInPlan === false && Not included} -
-
- {description.split('\n').map((value, idx) => ( -

- {value} -

- ))} -
-
- {links && links.length && ( -
-

- More information -

- {links.map((link) => ( -
- -
-

{link.name}

- -
- -
- ))} -
- )} -
-
-
{children}
-
-
-
- ) -} - -export default SectionContent diff --git a/studio/components/interfaces/BillingV2/Usage/SectionHeader.tsx b/studio/components/interfaces/BillingV2/Usage/SectionHeader.tsx deleted file mode 100644 index f5df014695c..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/SectionHeader.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export interface SectionHeaderProps { - title: string - description: string -} - -const SectionHeader = ({ title, description }: SectionHeaderProps) => { - return ( -
-
-
-

{title}

-

{description}

-
-
-
- ) -} - -export default SectionHeader diff --git a/studio/components/interfaces/BillingV2/Usage/SizeAndCounts.tsx b/studio/components/interfaces/BillingV2/Usage/SizeAndCounts.tsx deleted file mode 100644 index 6527a636ee7..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/SizeAndCounts.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useDailyStatsQuery } from 'data/analytics/daily-stats-query' -import { DataPoint } from 'data/analytics/constants' -import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query' -import UsageSection from './UsageSection' - -export interface SizeAndCountsProps { - projectRef: string - startDate: string | undefined - endDate: string | undefined - subscription: ProjectSubscriptionResponse | undefined - currentBillingCycleSelected: boolean -} - -const SizeAndCounts = ({ - projectRef, - startDate, - endDate, - subscription, - currentBillingCycleSelected, -}: SizeAndCountsProps) => { - const { data: dbSizeData, isLoading: isLoadingDbSizeData } = useDailyStatsQuery({ - projectRef, - attribute: 'total_db_size_bytes', - interval: '1d', - startDate, - endDate, - }) - - const { data: storageSizeData, isLoading: isLoadingStorageSizeData } = useDailyStatsQuery({ - projectRef, - attribute: 'total_storage_size_bytes', - interval: '1d', - startDate, - endDate, - }) - - const { data: functionCountData, isLoading: isLoadingFunctionCountData } = useDailyStatsQuery({ - projectRef, - attribute: 'total_func_count', - interval: '1d', - startDate, - endDate, - }) - - const chartMeta: { - [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean; hasNoData: boolean } - } = { - db_size: { - isLoading: isLoadingDbSizeData, - margin: 14, - data: dbSizeData?.data ?? [], - hasNoData: dbSizeData?.hasNoData ?? false, - }, - storage_size: { - isLoading: isLoadingStorageSizeData, - margin: 14, - data: storageSizeData?.data ?? [], - hasNoData: storageSizeData?.hasNoData ?? false, - }, - func_count: { - isLoading: isLoadingFunctionCountData, - margin: 0, - data: functionCountData?.data ?? [], - hasNoData: functionCountData?.hasNoData ?? false, - }, - } - - return ( - - ) -} - -export default SizeAndCounts diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx b/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx deleted file mode 100644 index a97b8149470..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query' -import { ProjectUsageData } from 'data/usage/project-usage-query' -import Link from 'next/link' -import { Alert, Badge, Button, IconExternalLink } from 'ui' -import { USAGE_APPROACHING_THRESHOLD } from '../Billing.constants' - -export const Y_DOMAIN_CEILING_MULTIPLIER = 4 / 3 - -export const USAGE_STATUS = { - NORMAL: 'NORMAL', - APPROACHING: 'APPROACHING', - EXCEEDED: 'EXCEEDED', -} - -export interface CategoryAttribute { - anchor: string - key: string // Property from project usage - attribute: string // For querying against stats-daily / infra-monitoring - name: string - chartPrefix?: string - unit: 'bytes' | 'absolute' | 'percentage' - links?: { - name: string - url: string - }[] - description: string - chartDescription: string - additionalInfo?: ( - ref: string, - subscription?: ProjectSubscriptionResponse, - usage?: ProjectUsageData - ) => JSX.Element | null -} - -export type CategoryMetaKey = 'infra' | 'bandwidth' | 'sizeCount' | 'activity' - -export interface CategoryMeta { - key: 'infra' | 'bandwidth' | 'sizeCount' | 'activity' - name: string - description: string - attributes: CategoryAttribute[] -} - -export const USAGE_CATEGORIES: CategoryMeta[] = [ - { - key: 'infra', - name: 'Infrastructure', - description: 'Usage statistics related to your server instance', - attributes: [ - { - anchor: 'cpu', - key: 'max_cpu_usage', - attribute: 'max_cpu_usage', - name: 'CPU', - unit: 'percentage', - description: 'Max CPU usage of your server', - chartDescription: '', - links: [ - { - name: 'Compute Add-Ons', - url: 'https://supabase.com/docs/guides/platform/compute-add-ons', - }, - { name: 'High CPU Usage', url: 'https://supabase.com/docs/guides/platform/exhaust-cpu' }, - { - name: 'Metrics', - url: 'https://supabase.com/docs/guides/platform/metrics', - }, - ], - }, - { - anchor: 'ram', - key: 'ram_usage', - attribute: 'ram_usage', - name: 'Memory', - unit: 'percentage', - description: - 'Memory usage of your server.\nYou might observe elevated memory usage, even with little to no load. Besides Postgres, a wide range of services is running under the hood resulting in an elevated base memory usage.', - chartDescription: '', - links: [ - { - name: 'Compute Add-Ons', - url: 'https://supabase.com/docs/guides/platform/compute-add-ons', - }, - { name: 'High RAM Usage', url: 'https://supabase.com/docs/guides/platform/exhaust-ram' }, - { - name: 'Metrics', - url: 'https://supabase.com/docs/guides/platform/metrics', - }, - ], - }, - { - anchor: 'disk_io', - key: 'disk_io_consumption', - attribute: 'disk_io_consumption', - name: 'Disk IO Bandwidth', - unit: 'percentage', - links: [ - { - name: 'Disk Throughput and IOPS', - url: 'https://supabase.com/docs/guides/platform/compute-add-ons#disk-throughput-and-iops', - }, - { - name: 'Metrics', - url: 'https://supabase.com/docs/guides/platform/metrics', - }, - ], - description: - 'The disk performance of your workload is determined by the Disk IO bandwidth.\nSmaller compute instances (below 4XL) can burst up to their largest throughput and IOPS for 30 minutes in a day. Beyond that, the performance reverts to the baseline. Your disk budget gets replenished throughout the day.', - chartDescription: '', - }, - ], - }, - { - key: 'bandwidth', - name: 'Bandwidth', - description: 'Amount of data transmitted over all network connections', - attributes: [ - { - anchor: 'dbEgress', - key: 'db_egress', - attribute: 'total_egress_modified', - name: 'Database Egress', - unit: 'bytes', - description: - 'Contains any outgoing traffic (egress) from your database.\nBilling is based on the total sum of egress in GB throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - }, - { - anchor: 'storageEgress', - key: 'storage_egress', - attribute: 'total_storage_egress', - name: 'Storage Egress', - unit: 'bytes', - description: - 'All requests to view and download your storage items go through our CDN. We sum up all outgoing traffic (egress) for storage related requests through our CDN. We do not differentiate between cache and no cache hits.\nBilling is based on the total amount of egress in GB throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - }, - ], - }, - { - key: 'sizeCount', - name: 'Size & Counts', - description: 'Amount of resources your project is consuming', - attributes: [ - { - anchor: 'dbSize', - key: 'db_size', - attribute: 'total_db_size_bytes', - name: 'Database size', - chartPrefix: 'Average ', - unit: 'bytes', - description: - 'Database size refers to the monthly average storage usage, as reported by Postgres. Paid plans use auto-scaling disks.\nBilling is based on the average daily database size used in GB throughout the billing period. Billing is independent of the provisioned disk size.', - links: [ - { - name: 'Documentation', - url: 'https://supabase.com/docs/guides/platform/database-size', - }, - ], - chartDescription: 'The data refreshes every 24 hours.', - additionalInfo: ( - ref: string, - subscription?: ProjectSubscriptionResponse, - usage?: ProjectUsageData - ) => { - const usageMeta = usage?.['db_size'] ?? undefined - const usageRatio = - typeof usageMeta !== 'number' ? (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0) : 0 - const hasLimit = usageMeta && usageMeta.limit > 0 - - const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD - const isExceededLimit = hasLimit && usageRatio >= 1 - - return subscription?.plan?.id !== 'free' ? ( -
-
-
-

Disk size:

-

{usage?.disk_volume_size_gb} GB

- - Auto-scaling - -
- -
-

- If you reach 95% of the disk space, your project will enter read-only mode. Your - disk storage expands automatically when the database reaches 90% of the disk size. - The disk is expanded to be 50% larger. Auto-scaling can only take place once every 6 - hours. You can also{' '} - - preprovision - {' '} - disk for loading larger amounts of data. -

-
- ) : ( -
- {(isApproachingLimit || isExceededLimit) && ( - -
-
- When you reach your database size limit, your project can go into read-only - mode. Please upgrade your plan. -
-
-
- )} -
- ) - }, - }, - { - anchor: 'storageSize', - key: 'storage_size', - attribute: 'total_storage_size_bytes', - name: 'Storage Size', - chartPrefix: 'Average ', - unit: 'bytes', - description: - 'Sum of all objects in your storage buckets.\nBilling is based on the average daily size in GB throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Storage', - url: 'https://supabase.com/docs/guides/storage', - }, - ], - }, - { - anchor: 'funcCount', - key: 'func_count', - attribute: 'total_func_count', - name: 'Edge Function Count', - chartPrefix: 'Max ', - unit: 'absolute', - description: - 'Number of serverless functions in your project.\nBilling is based on the maximum amount of functions at any point in time throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - }, - ], - }, - { - key: 'activity', - name: 'Activity', - description: 'Usage statistics that reflect the activity of your project', - attributes: [ - { - anchor: 'mau', - key: 'monthly_active_users', - attribute: 'total_auth_billing_period_mau', - name: 'Monthly Active Users', - unit: 'absolute', - description: - 'Users who log in or refresh their token count towards MAU.\nBilling is based on the sum of distinct users requesting your API throughout the billing period. Resets every billing cycle.', - chartDescription: - 'The data is refreshed over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period.', - links: [ - { - name: 'Auth', - url: 'https://supabase.com/docs/guides/auth', - }, - ], - }, - { - anchor: 'mauSso', - key: 'monthly_active_sso_users', - attribute: 'total_auth_billing_period_sso_mau', - name: 'Monthly Active SSO Users', - unit: 'absolute', - description: - 'SSO users who log in or refresh their token count towards SSO MAU.\nBilling is based on the sum of distinct Single Sign-On users requesting your API throughout the billing period. Resets every billing cycle.', - chartDescription: - 'The data refreshes over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period.', - links: [ - { - name: 'SSO with SAML 2.0', - url: 'https://supabase.com/docs/guides/auth/sso/auth-sso-saml', - }, - ], - }, - { - anchor: 'storageImageTransformations', - key: 'storage_image_render_count', - attribute: 'total_storage_image_render_count', - name: 'Storage Image Transformations', - unit: 'absolute', - description: - 'We count all images that were transformed in the billing period, ignoring any transformations.\nUsage example: You transform one image with four different size transformations and another image with just a single transformation. It counts as two, as only two images were transformed.\nBilling is based on the count of (origin) images that used transformations throughout the billing period. Resets every billing cycle.', - chartDescription: - 'The data refreshes every 24 hours.\nThe data points are relative to the beginning of your billing period.', - links: [ - { - name: 'Documentation', - url: 'https://supabase.com/docs/guides/storage/serving/image-transformations', - }, - ], - }, - { - anchor: 'functionInvocations', - key: 'func_invocations', - attribute: 'total_func_invocations', - name: 'Edge Function Invocations', - unit: 'absolute', - description: - 'Every serverless function invocation independent of response status is counted.\nBilling is based on the sum of all invocations throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Edge Functions', - url: 'https://supabase.com/docs/guides/functions', - }, - ], - }, - { - anchor: 'realtimeMessageCount', - key: 'realtime_message_count', - attribute: 'total_realtime_message_count', - name: 'Realtime Message Count', - unit: 'absolute', - description: - "Count of messages going through Realtime.\nUsage example: If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period.", - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Realtime Quotas', - url: 'https://supabase.com/docs/guides/realtime/quotas', - }, - ], - }, - { - anchor: 'realtimePeakConnection', - key: 'realtime_peak_connection', - attribute: 'total_realtime_peak_connection', - name: 'Realtime Peak Connections', - chartPrefix: 'Max ', - unit: 'absolute', - description: - 'Total number of successful connections. Connections attempts are not counted towards usage.\nBilling is based on the maximum amount of concurrent peak connections throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Realtime Quotas', - url: 'https://supabase.com/docs/guides/realtime/quotas', - }, - ], - }, - ], - }, -] diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.tsx b/studio/components/interfaces/BillingV2/Usage/Usage.tsx deleted file mode 100644 index f50f4ba277d..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/Usage.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import clsx from 'clsx' -import { useParams } from 'common' -import dayjs from 'dayjs' -import { useMemo, useRef, useState } from 'react' -import { InView } from 'react-intersection-observer' -import { Button, cn, IconAlertCircle, IconExternalLink, IconInfo, IconLoader } from 'ui' - -import DateRangePicker from 'components/to-be-cleaned/DateRangePicker' -import InformationBox from 'components/ui/InformationBox' -import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useProjectUsageQuery } from 'data/usage/project-usage-query' -import { useSelectedOrganization } from 'hooks' -import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants' -import Link from 'next/link' -import Activity from './Activity' -import Bandwidth from './Bandwidth' -import Infrastructure from './Infrastructure' -import SizeAndCounts from './SizeAndCounts' -import { USAGE_CATEGORIES, USAGE_STATUS } from './Usage.constants' -import { getUsageStatus } from './Usage.utils' - -type UsageSectionIds = 'infra' | 'bandwidth' | 'sizeCount' | 'activity' - -const Usage = () => { - const { ref } = useParams() - const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id - - // [Joshen] Using a state here for now as in the future, we'd have this usage page support showing - // stats for different projects. So we'll pass the selected ref as a prop into the individual components - const [selectedProjectRef] = useState(ref as string) - const [activeTab, setActiveTab] = useState('infra') - const [dateRange, setDateRange] = useState() - - const infrastructureRef = useRef(null) - const bandwidthRef = useRef(null) - const sizeAndCountsRef = useRef(null) - const activityRef = useRef(null) - - const { data: usage } = useProjectUsageQuery({ projectRef: ref }) - const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionV2Query({ - projectRef: selectedProjectRef, - }) - const usageBillingEnabled = subscription?.usage_billing_enabled - const billingCycleStart = dayjs.unix(subscription?.current_period_start ?? 0).utc() - const billingCycleEnd = dayjs.unix(subscription?.current_period_end ?? 0).utc() - - const currentBillingCycleSelected = useMemo(() => { - // Selected by default - if (!dateRange?.period_start || !dateRange?.period_end || !subscription) return true - - const { current_period_start, current_period_end } = subscription - - return ( - dayjs(dateRange.period_start.date).isSame(new Date(current_period_start * 1000)) && - dayjs(dateRange.period_end.date).isSame(new Date(current_period_end * 1000)) - ) - }, [dateRange, subscription]) - - const startDate = useMemo(() => { - // If end date is in future, set end date to now - if (!dateRange?.period_start?.date) { - return undefined - } else { - // LF seems to have an issue with the milliseconds, causes infinite loading sometimes - return new Date(dateRange?.period_start?.date).toISOString().slice(0, -5) + 'Z' - } - }, [dateRange, subscription]) - - const endDate = useMemo(() => { - // If end date is in future, set end date to end of current day - if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) { - // LF seems to have an issue with the milliseconds, causes infinite loading sometimes - // In order to have full days from Prometheus metrics when using 1d interval, - // the time needs to be greater or equal than the time of the start date - return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z' - } else if (dateRange?.period_end?.date) { - // LF seems to have an issue with the milliseconds, causes infinite loading sometimes - return new Date(dateRange.period_end.date).toISOString().slice(0, -5) + 'Z' - } - }, [dateRange, subscription]) - - const { data: ioBudgetData } = useInfraMonitoringQuery({ - projectRef: selectedProjectRef, - attribute: 'disk_io_budget', - interval: '1d', - startDate: dateRange?.period_start?.date, - endDate: dateRange?.period_end?.date, - }) - const currentDayIoBudget = Number( - ioBudgetData?.data.find((x) => x.periodStartFormatted === dayjs().format('DD MMM'))?.[ - 'disk_io_budget' - ] ?? 100 - ) - - const scrollTo = (id: UsageSectionIds) => { - switch (id) { - case 'infra': - if (infrastructureRef.current) { - infrastructureRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'start', // or 'end' to scroll to the bottom - inline: 'start', // - }) - } - break - case 'bandwidth': - if (bandwidthRef.current) { - bandwidthRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'start', // or 'end' to scroll to the bottom - inline: 'start', // - }) - } - break - case 'sizeCount': - if (sizeAndCountsRef.current) { - sizeAndCountsRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'start', // or 'end' to scroll to the bottom - inline: 'start', // - }) - } - break - case 'activity': - if (activityRef.current) { - activityRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'start', // or 'end' to scroll to the bottom - inline: 'start', // - }) - } - break - } - } - - return ( - <> -
-
-

{isOrgBilling ? 'Project ' : ''}Usage

-
-
-
-
-
-
-
- {!isLoadingSubscription && ( - - )} - - {isLoadingSubscription ? ( - - ) : subscription !== undefined ? ( -
-

- {isOrgBilling ? 'Organization' : 'Project'} is on the {subscription.plan.name}{' '} - plan -

-

- {billingCycleStart.format('DD MMM YYYY')} -{' '} - {billingCycleEnd.format('DD MMM YYYY')} -

-
- ) : null} -
-
-
- {USAGE_CATEGORIES.map((category) => { - const infraStatus = - currentDayIoBudget <= 0 - ? USAGE_STATUS.EXCEEDED - : currentDayIoBudget <= 20 - ? USAGE_STATUS.APPROACHING - : USAGE_STATUS.NORMAL - const status = - category.key === 'infra' - ? infraStatus - : getUsageStatus(category.attributes, usage) - - return ( - - ) - })} -
-
-
- -
- } - defaultVisibility={true} - hideCollapse - title="We're upgrading our billing system" - description={ -
-

- This organization uses the legacy project-based billing. We’ve recently made some - big improvements to our billing system. To migrate to the new organization-based - billing, head over to your{' '} - - organization billing settings - - . -

- -
- - -
-
- } - /> -
- - inView && setActiveTab('infra')}> -
- -
-
- inView && setActiveTab('bandwidth')} - > -
- -
-
- inView && setActiveTab('sizeCount')} - > -
- -
-
- inView && setActiveTab('activity')} - > -
- -
-
-
- - ) -} - -export default Usage diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts b/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts deleted file mode 100644 index 62131d1ed54..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { DataPoint } from 'data/analytics/constants' -import { ProjectUsageResponse } from 'data/usage/project-usage-query' -import { USAGE_APPROACHING_THRESHOLD } from '../Billing.constants' -import { CategoryAttribute, USAGE_STATUS } from './Usage.constants' -import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query' - -// [Joshen] This is just for development to generate some test data for chart rendering -export const generateUsageData = (attribute: string, days: number): DataPoint[] => { - const tempArray = new Array(days).fill(0) - return tempArray.map((x, idx) => { - return { - loopId: (idx + 1).toString(), - period_start: `${idx + 1}`, - [attribute]: Math.floor(Math.random() * 100).toString(), - } - }) -} - -export const getUsageStatus = (attributes: CategoryAttribute[], usage?: ProjectUsageResponse) => { - if (!usage) return USAGE_STATUS.NORMAL - - const attributeStatuses = attributes.map((attribute) => { - const usageMeta = usage?.[attribute.key as keyof ProjectUsageResponse] - const usageRatio = - typeof usageMeta !== 'number' ? (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0) : 0 - if (usageRatio >= 1) return USAGE_STATUS.EXCEEDED - else if (usageRatio >= USAGE_APPROACHING_THRESHOLD) return USAGE_STATUS.APPROACHING - else return USAGE_STATUS.NORMAL - }) - - if (attributeStatuses.find((x) => x === USAGE_STATUS.EXCEEDED)) return USAGE_STATUS.EXCEEDED - else if (attributeStatuses.find((x) => x === USAGE_STATUS.APPROACHING)) - return USAGE_STATUS.APPROACHING - else return USAGE_STATUS.NORMAL -} - -export const getUpgradeUrl = (projectRef: string, subscription?: ProjectSubscriptionResponse) => { - if (!subscription) { - return `/project/${projectRef}/settings/billing/subscription` - } - - return subscription?.plan?.id === 'pro' && subscription?.usage_billing_enabled === false - ? `/project/${projectRef}/settings/billing/subscription#cost-control` - : `/project/${projectRef}/settings/billing/subscription?panel=subscriptionPlan` -} - -const compactNumberFormatter = new Intl.NumberFormat('en-US', { - notation: 'compact', - compactDisplay: 'short', -}) - -/** - * For the y-axis, we don't need to be as precise, to avoid showing 58.597MB. - */ -export const ChartYFormatterCompactNumber = (number: number | string, unit: string) => { - if (typeof number === 'string') return number - - if (unit === 'bytes') { - const formattedBytes = formatBytesCompact(number).replace(/\s/g, '') - - return formattedBytes === '0bytes' ? '0' : formattedBytes - } else { - return compactNumberFormatter.format(number) - } -} - -/** - * For the chart tooltip, we want to be more precise and show more decimals. - */ -export const ChartTooltipValueFormatter = (number: number | string, unit: string) => { - if (typeof number === 'string') return number - - if (unit === 'bytes') { - const formattedBytes = formatBytesPrecision(number).replace(/\s/g, '') - - return formattedBytes === '0bytes' ? '0' : formattedBytes - } else { - return compactNumberFormatter.format(number) - } -} - -const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] - -export const formatBytesCompact = (bytes: number) => { - if (bytes === 0 || bytes === undefined) return '0 bytes' - - const k = 1024 - const i = Math.floor(Math.log(bytes) / Math.log(k)) - - const unit = sizes[i] - - let dm = 2 - if (['bytes', 'KB', 'MB'].includes(unit)) { - dm = 0 - } else if (['GB'].includes(unit)) { - dm = 1 - } - - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + unit -} - -export const formatBytesPrecision = (bytes: any) => { - if (bytes === 0 || bytes === undefined) return '0 bytes' - - const k = 1024 - const i = Math.floor(Math.log(bytes) / Math.log(k)) - - const unit = sizes[i] - - return parseFloat((bytes / Math.pow(k, i)).toFixed(3)) + ' ' + unit -} diff --git a/studio/components/interfaces/BillingV2/Usage/UsageBarChart.tsx b/studio/components/interfaces/BillingV2/Usage/UsageBarChart.tsx deleted file mode 100644 index 45e3d998803..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/UsageBarChart.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { DataPoint } from 'data/analytics/constants' -import dayjs from 'dayjs' -import { - Bar, - CartesianGrid, - Cell, - ComposedChart, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from 'recharts' - -// [Joshen] This BarChart is specifically for usage, hence not a reusable component, and not -// replacing the existing BarChart in ui/Charts - -export interface UsageBarChartProps { - data: DataPoint[] - name: string // Used within the tooltip - attribute: string - unit: 'bytes' | 'absolute' | 'percentage' - yLimit?: number - yLeftMargin?: number - yFormatter?: (value: number | string) => string - tooltipFormatter?: (value: number | string) => string -} - -const UsageBarChart = ({ - data, - name, - attribute, - unit, - yLimit, - yLeftMargin = 10, - yFormatter, - tooltipFormatter, -}: UsageBarChartProps) => { - const yMin = 0 // We can consider passing this as a prop if there's a use case in the future - - const yDomain = [yMin, yLimit ?? 0] - - return ( -
- - - - - - { - const { active, payload } = props - if (active && payload && payload.length) { - const dataPeriod = dayjs(payload[0].payload.period_start) - const value = - unit === 'percentage' - ? Number(payload[0].value).toFixed(2) - : Number(payload[0].value) - - return ( -
-

- {attribute === 'disk_io_budget' ? `Remaining IO budget:` : `${name}:`} -

- {dataPeriod.startOf('day').isAfter(dayjs().startOf('day')) ? ( -

No data yet

- ) : ( -

- {tooltipFormatter !== undefined ? tooltipFormatter(value) : value} -

- )} -

- {dataPeriod.format('DD MMM YYYY')} -

-
- ) - } else return null - }} - /> - - {data.map((entry) => { - return - })} - -
-
-
- ) -} - -export default UsageBarChart diff --git a/studio/components/interfaces/BillingV2/Usage/UsageSection.tsx b/studio/components/interfaces/BillingV2/Usage/UsageSection.tsx deleted file mode 100644 index 584b310eb30..00000000000 --- a/studio/components/interfaces/BillingV2/Usage/UsageSection.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import * as Tooltip from '@radix-ui/react-tooltip' -import clsx from 'clsx' -import Link from 'next/link' - -import Panel from 'components/ui/Panel' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import SparkBar from 'components/ui/SparkBar' -import { DataPoint } from 'data/analytics/constants' -import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query' -import { - ProjectUsageResponse, - UsageMetric, - useProjectUsageQuery, -} from 'data/usage/project-usage-query' -import { useIsFeatureEnabled } from 'hooks' -import { formatBytes } from 'lib/helpers' -import { Button, IconAlertTriangle, IconBarChart2 } from 'ui' -import { USAGE_APPROACHING_THRESHOLD } from '../Billing.constants' -import SectionContent from './SectionContent' -import SectionHeader from './SectionHeader' -import { CategoryMetaKey, USAGE_CATEGORIES } from './Usage.constants' -import { - ChartTooltipValueFormatter, - ChartYFormatterCompactNumber, - getUpgradeUrl, -} from './Usage.utils' -import UsageBarChart from './UsageBarChart' - -interface UsageSectionProps { - projectRef: string - categoryKey: CategoryMetaKey - subscription: ProjectSubscriptionResponse | undefined - chartMeta: { - [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean; hasNoData: boolean } - } - currentBillingCycleSelected: boolean -} - -const UsageSection = ({ - projectRef, - categoryKey, - chartMeta, - subscription, - currentBillingCycleSelected, -}: UsageSectionProps) => { - const { data: usage } = useProjectUsageQuery({ projectRef }) - const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === categoryKey) - - if (!categoryMeta) return null - - const usageBasedBilling = subscription?.usage_billing_enabled - const exceededLimitStyle = !usageBasedBilling ? 'text-red-900' : 'text-amber-900' - - const upgradeUrl = getUpgradeUrl(projectRef, subscription) - - return ( - <> - - - {categoryMeta.attributes.map((attribute) => { - const usageMeta = usage?.[attribute.key as keyof ProjectUsageResponse] as UsageMetric - const usageRatio = - typeof usageMeta !== 'number' ? (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0) : 0 - const usageExcess = (usageMeta?.usage ?? 0) - (usageMeta?.limit ?? 0) - - const chartData = chartMeta[attribute.key]?.data ?? [] - - const notAllValuesZero = chartData.some( - (dataPoint) => Number(dataPoint[attribute.attribute]) !== 0 - ) - - return ( -
- - {usageMeta?.available_in_plan ? ( - <> -
-
-
-

{attribute.name} usage

- {currentBillingCycleSelected && - usageBasedBilling === false && - usageRatio >= USAGE_APPROACHING_THRESHOLD && ( - - - {!usageBasedBilling && usageRatio >= 1 ? ( -
- -

- Exceeded limit -

-
- ) : ( - !usageBasedBilling && - usageRatio >= USAGE_APPROACHING_THRESHOLD && ( -
- -

Approaching limit

-
- ) - )} -
- - - -
-

- Exceeding your plans included usage will lead to restrictions - to your project. -

-

- Upgrade to a usage-based plan or disable the spend cap to - avoid restrictions. -

-
-
-
-
- )} -
- - {currentBillingCycleSelected && - !usageBasedBilling && - usageRatio >= USAGE_APPROACHING_THRESHOLD && ( - - )} -
- {currentBillingCycleSelected && usageMeta?.limit > 0 && ( - = 1 - ? usageBasedBilling - ? 'bg-scale-1100' - : 'bg-red-900' - : usageBasedBilling === false && - usageRatio >= USAGE_APPROACHING_THRESHOLD - ? 'bg-amber-900' - : 'bg-scale-1100' - )} - bgClass="bg-gray-300 dark:bg-gray-600" - value={usageMeta?.usage ?? 0} - max={usageMeta?.limit || 1} - /> - )} -
-
-

- Included in {subscription?.plan?.name.toLowerCase()} plan -

- {usageMeta?.limit === -1 ? ( -

None

- ) : usageMeta?.limit === 0 ? ( -

Unlimited

- ) : ( -

- {attribute.unit === 'bytes' - ? formatBytes(usageMeta?.limit ?? 0) - : (usageMeta?.limit ?? 0).toLocaleString()} -

- )} -
- {currentBillingCycleSelected && ( -
-

- {attribute.chartPrefix || 'Used '}in period -

-

- {attribute.unit === 'bytes' - ? formatBytes(usageMeta?.usage ?? 0) - : (usageMeta?.usage ?? 0).toLocaleString()} -

-
- )} - {currentBillingCycleSelected && usageMeta?.limit > 0 && ( -
-

Overage in period

-

- {(usageMeta?.limit ?? 0) === -1 || usageExcess < 0 - ? 0 - : attribute.unit === 'bytes' - ? formatBytes(usageExcess) - : usageExcess.toLocaleString()} -

-
- )} -
-
- - {attribute.additionalInfo?.(projectRef, subscription, usage)} - -
-

- {attribute.chartPrefix || ''} - {attribute.name} per day -

- {attribute.chartDescription.split('\n').map((paragraph, idx) => ( -

- {paragraph} -

- ))} -
- {chartMeta[attribute.key].isLoading ? ( -
- - - -
- ) : chartData.length > 0 && notAllValuesZero ? ( - ChartYFormatterCompactNumber(value, attribute.unit)} - tooltipFormatter={(value) => - ChartTooltipValueFormatter(value, attribute.unit) - } - /> - ) : ( - - -
- -

No data in period

-

- May take up to 24 hours to show -

-
-
-
- )} - - ) : ( - - -
-
-

Not included in plan

-
-

- You need to be on a higher plan in order to use this feature. -

-
-
- - -
-
-
- )} -
-
- ) - })} - - ) -} - -export default UsageSection diff --git a/studio/components/interfaces/BillingV2/index.ts b/studio/components/interfaces/BillingV2/index.ts deleted file mode 100644 index 061f3541e13..00000000000 --- a/studio/components/interfaces/BillingV2/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import AddNewPaymentMethodModal from './AddNewPaymentMethodModal/AddNewPaymentMethodModal' -import Invoices from './Invoices' -import SpendCapModal from './SpendCapModal' -import InvoiceStatusBadge from './InvoiceStatusBadge' - -export { AddNewPaymentMethodModal, Invoices, SpendCapModal, InvoiceStatusBadge } diff --git a/studio/components/interfaces/Database/Backups/BackupsList.tsx b/studio/components/interfaces/Database/Backups/BackupsList.tsx index 1b2be25cd1b..8f2991d3a54 100644 --- a/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -14,7 +14,7 @@ import UpgradeToPro from 'components/ui/UpgradeToPro' import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation' import { DatabaseBackup, useBackupsQuery } from 'data/database/backups-query' import { setProjectStatus } from 'data/projects/projects-query' -import { useCheckPermissions, useStore } from 'hooks' +import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { PROJECT_STATUS } from 'lib/constants' import BackupItem from './BackupItem' import BackupsEmpty from './BackupsEmpty' @@ -23,6 +23,7 @@ const BackupsList = () => { const { ui } = useStore() const router = useRouter() const queryClient = useQueryClient() + const organization = useSelectedOrganization() const { project: selectedProject } = useProjectContext() const projectRef = selectedProject?.ref || 'default' @@ -65,6 +66,7 @@ const BackupsList = () => { icon={} primaryText="Free Plan does not include project backups." projectRef={projectRef} + organizationSlug={organization!.slug} secondaryText="Upgrade to the Pro plan for up to 7 days of scheduled backups." addon="pitr" /> diff --git a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx index 0de9baca7f2..5ce9e3ba650 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx @@ -1,7 +1,7 @@ import { groupBy } from 'lodash' import { observer } from 'mobx-react-lite' import Link from 'next/link' -import { Button, IconExternalLink, IconPlus, Modal } from 'ui' +import { Button, IconPlus } from 'ui' import AlertError from 'components/ui/AlertError' import { @@ -14,7 +14,6 @@ import { useProjectsQuery } from 'data/projects/projects-query' import { ResourceWarning, useResourceWarningsQuery } from 'data/usage/resource-warnings-query' import { IS_PLATFORM } from 'lib/constants' import { makeRandomString } from 'lib/helpers' -import { useState } from 'react' import { Organization, Project, ResponseError } from 'types' import ProjectCard from './ProjectCard' import ShimmeringCard from './ShimmeringCard' @@ -111,8 +110,6 @@ const OrganizationProjects = ({ ?.filter((integration) => integration.integration.name === 'Vercel') .flatMap((integration) => integration.connections) - const [orgBillingMigrationModalVisible, setOrgBillingMigrationModalVisible] = useState(false) - return (
@@ -125,14 +122,6 @@ const OrganizationProjects = ({
)} - - {!subscription_id && ( -
- -
- )}
{isLoadingPermissions || isLoadingProjects ? ( @@ -178,58 +167,6 @@ const OrganizationProjects = ({ )} )} - - setOrgBillingMigrationModalVisible(false)} - header="We're upgrading our billing system" - > - -
-

- The organization "{name}" still uses the legacy project-based billing. We've recently - made some big improvements to our billing system and require your action. To migrate - to the new organization-based billing, head over to your{' '} - - organization billing settings - - . -

- -

- Please do this until the 18th of October, as - remaining organizations will be migrated. -

- -
- - -
-
-
-
) } diff --git a/studio/components/interfaces/Home/ProjectUsageSection.tsx b/studio/components/interfaces/Home/ProjectUsageSection.tsx index ee89b6c0a30..493de068e3f 100644 --- a/studio/components/interfaces/Home/ProjectUsageSection.tsx +++ b/studio/components/interfaces/Home/ProjectUsageSection.tsx @@ -2,9 +2,10 @@ import { useParams } from 'common' import { observer } from 'mobx-react-lite' import { IconAlertCircle, IconLoader } from 'ui' -import { NewProjectPanel, ProjectUsage } from 'components/interfaces/Home' +import { NewProjectPanel } from 'components/interfaces/Home' import InformationBox from 'components/ui/InformationBox' import { ProjectUsageResponseUsageKeys, useProjectUsageQuery } from 'data/usage/project-usage-query' +import ProjectUsage from './ProjectUsage' const ProjectUsageSection = observer(() => { const { ref: projectRef } = useParams() diff --git a/studio/components/interfaces/Home/index.ts b/studio/components/interfaces/Home/index.ts index e62885bb727..229677cb889 100644 --- a/studio/components/interfaces/Home/index.ts +++ b/studio/components/interfaces/Home/index.ts @@ -1,6 +1,5 @@ import ClientLibrary from './ClientLibrary' import ExampleProject from './ExampleProject' -import ProjectUsage from './ProjectUsage' import NewProjectPanel from './NewProjectPanel/NewProjectPanel' -export { ClientLibrary, ExampleProject, ProjectUsage, NewProjectPanel } +export { ClientLibrary, ExampleProject, NewProjectPanel } diff --git a/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx index a09fbb3cd77..c30aa76e556 100644 --- a/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx @@ -1,50 +1,33 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' -import { isEqual } from 'lodash' -import { useState } from 'react' -import { Button, Form, Input, Listbox } from 'ui' +import { useEffect } from 'react' +import { useParams } from 'common' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' +import { FormActions, FormPanel, FormSection, FormSectionContent } from 'components/ui/Forms' import NoPermission from 'components/ui/NoPermission' -import Panel from 'components/ui/Panel' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' import { useCheckPermissions, useStore } from 'hooks' +import { Form, Input, Listbox } from 'ui' import { COUNTRIES } from './BillingAddress.constants' const BillingAddress = () => { const { ui } = useStore() const { slug } = useParams() + const { data, error, isLoading, isSuccess, isError } = useOrganizationCustomerProfileQuery({ + slug, + }) + const { mutateAsync: updateCustomerProfile, isLoading: isUpdating } = + useOrganizationCustomerProfileUpdateMutation() - const { - data: customer, - error: customerError, - isError: isErrorCustomer, - isLoading: isLoadingCustomer, - isSuccess: isSuccessCustomer, - } = useOrganizationCustomerProfileQuery({ slug }) - - const { city, country, line1, line2, postal_code, state } = customer?.address ?? {} - const [isDirty, setIsDirty] = useState(false) - const initialValues = { city, country, line1, line2, postal_code, state } - - const { mutate: updateCustomer, isLoading: isUpdating } = - useOrganizationCustomerProfileUpdateMutation({ - onSuccess: () => { - ui.setNotification({ - category: 'success', - message: 'Successfully updated billing address', - }) - setIsDirty(false) - }, - onError: (error) => { - ui.setNotification({ - category: 'error', - message: `Failed to update billing address: ${error.message}`, - }) - }, - }) + const formId = 'billing-address-form' + const { city, country, line1, line2, postal_code, state } = data?.address ?? {} const canReadBillingAddress = useCheckPermissions( PermissionAction.BILLING_READ, @@ -55,7 +38,9 @@ const BillingAddress = () => { 'stripe.customer' ) - const onValidate = (values: any) => { + const initialValues = { city, country, line1, line2, postal_code, state } + + const validate = (values: any) => { const errors = {} as any if ( (values.line1 || values.line2 || values.postal_code || values.state || values.city) && @@ -63,145 +48,163 @@ const BillingAddress = () => { ) { errors['country'] = 'Please select a country' } - if (isEqual(values, initialValues)) { - setIsDirty(false) - } else { - setIsDirty(true) - } return errors } - const onSubmit = async (values: any) => { + const onSubmit = async (values: any, { resetForm }: any) => { if (!slug) return console.error('Slug is required') - updateCustomer({ slug, address: values }) + + try { + await updateCustomerProfile({ slug, address: values }) + ui.setNotification({ + category: 'success', + message: 'Successfully updated billing address', + }) + resetForm({ values, initialValues: values }) + } catch (error) {} } return ( -
-
-

Billing address

-

This will be reflected in every invoice

-
+ + +
+

Billing Address

+

+ This will be reflected in every upcoming invoice, past invoices are not affected +

+
+
+ + {!canReadBillingAddress ? ( + + ) : ( + <> + {isLoading && ( +
+ + + +
+ )} - {isLoadingCustomer && } + {isError && ( + + )} - {isErrorCustomer && ( - - )} + {isSuccess && ( +
+ {({ values, initialValues, handleReset, resetForm }: any) => { + const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) - {isSuccessCustomer && ( - - {!canReadBillingAddress ? ( - - ) : ( - - {({ handleReset }: any) => { - return ( - <> - - - -
- - - --- - - {COUNTRIES.map((country) => ( - { + if (isSuccess && data !== undefined) { + const { city, country, line1, line2, postal_code, state } = + data?.address ?? {} + const values = { city, country, line1, line2, postal_code, state } + resetForm({ values, initialValues: values }) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess]) + + return ( + + +
+ } + > + + + + +
+ - {country.name} - - ))} - - -
-
- - -
-
-
- - {!canUpdateBillingAddress ? ( -

- You need additional permissions to update this organization's billing - address -

- ) : ( -
- )} -
- - -
- - - ) - }} - - )} - - )} -
+ + --- + + {COUNTRIES.map((country) => ( + + {country.name} + + ))} + + +
+
+ + +
+ + + + ) + }} + + )} + + )} +
+
) } diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingBreakdown/BillingBreakdown.constants.ts b/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/BillingBreakdown/BillingBreakdown.constants.ts rename to studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingBreakdown/BillingBreakdown.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/BillingBreakdown/BillingBreakdown.tsx rename to studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingBreakdown/BillingMetric.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx similarity index 99% rename from studio/components/interfaces/Organization/BillingSettingsV2/BillingBreakdown/BillingMetric.tsx rename to studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx index 77040638aa9..5ff7e6d197e 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/BillingBreakdown/BillingMetric.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx @@ -2,7 +2,7 @@ import * as Tooltip from '@radix-ui/react-tooltip' import clsx from 'clsx' import Link from 'next/link' -import { BILLING_BREAKDOWN_METRICS } from 'components/interfaces/BillingV2/Subscription/Subscription.constants' +import { BILLING_BREAKDOWN_METRICS } from 'components/interfaces/Billing/Subscription/Subscription.constants' import SparkBar from 'components/ui/SparkBar' import { OrgSubscription } from 'data/subscriptions/org-subscription-query' import { OrgUsageResponse } from 'data/usage/org-usage-query' diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingBreakdown/UpcomingInvoice.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/BillingBreakdown/UpcomingInvoice.tsx rename to studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx diff --git a/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx index cd2854305cb..a7ae06dad72 100644 --- a/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx @@ -2,15 +2,16 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { useEffect } from 'react' -import { useParams } from 'common/hooks' +import { useParams } from 'common' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from 'components/layouts/Scaffold' import { FormActions, FormPanel, FormSection, FormSectionContent } from 'components/ui/Forms' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' -import { isResponseOk, patch } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' -import { Organization } from 'types' import { Form, Input } from 'ui' const BillingEmail = () => { @@ -21,94 +22,97 @@ const BillingEmail = () => { const { name, billing_email } = selectedOrganization ?? {} const formId = 'org-billing-email' - const initialValues = { - billing_email: billing_email ?? '', - } + const initialValues = { billing_email: billing_email ?? '' } const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') const canReadBillingEmail = useCheckPermissions(PermissionAction.READ, 'organizations') - const { mutate: updateOrganization, isLoading: isUpdating } = useOrganizationUpdateMutation() + const { mutateAsync: updateOrganization, isLoading: isUpdating } = useOrganizationUpdateMutation() - const onUpdateOrganization = async (values: any, { setSubmitting, resetForm }: any) => { + const onUpdateOrganizationEmail = async (values: any, { resetForm }: any) => { if (!canUpdateOrganization) { return ui.setNotification({ category: 'error', message: 'You do not have the required permissions to update this organization', }) } - if (!slug) return console.error('Slug is required') + if (!name) return console.error('Organization name is required') - updateOrganization( - { slug, billing_email: values.billing_email }, - { - onSuccess: ({ billing_email }) => { - resetForm({ values: { billing_email }, initialValues: { billing_email } }) - invalidateOrganizationsQuery(queryClient) - ui.setNotification({ category: 'success', message: 'Successfully saved settings' }) - }, - } - ) + try { + const { billing_email } = await updateOrganization({ + slug, + name, + billing_email: values.billing_email, + }) + resetForm({ values: { billing_email }, initialValues: { billing_email } }) + invalidateOrganizationsQuery(queryClient) + ui.setNotification({ + category: 'success', + message: 'Successfully saved settings', + }) + } finally { + } } return ( -
-

Billing email

-

All billing correspondence will go to this email

+ + +
+

Email Recipient

+

+ All billing correspondence will go to this email +

+
+
+ +
+ {({ handleReset, values, initialValues, resetForm }: any) => { + const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) -
- {selectedOrganization === undefined ? ( - - ) : ( - - {({ handleReset, values, initialValues, resetForm }: any) => { - const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + // [Alaister] although this "technically" is breaking the rules of React hooks + // it won't error because the hooks are always rendered in the same order + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + const values = { billing_email: billing_email ?? '' } + resetForm({ values, initialValues: values }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [slug]) - // [Alaister] although this "technically" is breaking the rules of React hooks - // it won't error because the hooks are always rendered in the same order - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - const values = { billing_email: billing_email ?? '' } - resetForm({ values, initialValues: values }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [slug]) - - return ( - - -
- } - > - - - - - - - ) - }} - - )} -
-
+ return ( + + + + } + > + + + + + + + ) + }} + + + ) } diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.constants.ts b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.constants.ts similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.constants.ts rename to studio/components/interfaces/Organization/BillingSettings/BillingSettings.constants.ts diff --git a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx index 4bb0d5768bc..a92669de10b 100644 --- a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx @@ -1,56 +1,81 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' - -import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold' -import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' -import { useProjectsQuery } from 'data/projects/projects-query' -import { useCheckPermissions, useFlag, useSelectedOrganization } from 'hooks' -import OrganizationBillingMigrationPanel from '../GeneralSettings/OrganizationBillingMigrationPanel' +import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' import BillingAddress from './BillingAddress/BillingAddress' import BillingEmail from './BillingEmail' +import CostControl from './CostControl/CostControl' import CreditBalance from './CreditBalance' -import PaymentMethods from './PaymentMethods' -import ProjectsSummary from './ProjectsSummary' +import PaymentMethods from './PaymentMethods/PaymentMethods' +import Subscription from './Subscription/Subscription' import TaxID from './TaxID/TaxID' +import BillingBreakdown from './BillingBreakdown/BillingBreakdown' +import { useIsFeatureEnabled } from 'hooks' const BillingSettings = () => { - const { slug } = useParams() - - const organization = useSelectedOrganization() - const { data: allProjects } = useProjectsQuery() - const projects = - allProjects?.filter((project) => project.organization_id === organization?.id) ?? [] - - const { data: customer } = useOrganizationCustomerProfileQuery({ slug }) - - const customerBalance = customer && customer.balance ? customer.balance / 100 : 0 - const isCredit = customerBalance < 0 - const isDebt = customerBalance > 0 - const balance = - isCredit && customerBalance !== 0 - ? customerBalance.toString().replace('-', '') - : customerBalance - - const orgBillingMigrationEnabled = useFlag('orgBillingMigration') - const selectedOrganization = useSelectedOrganization() - const { subscription_id } = selectedOrganization ?? {} + const { + billingAccountData: billingAccountDataEnabled, + billingPaymentMethods: billingPaymentMethodsEnabled, + billingCredits: billingCreditsEnabled, + } = useIsFeatureEnabled(['billing:account_data', 'billing:payment_methods', 'billing:credits']) return ( - - {orgBillingMigrationEnabled && !subscription_id && } + <> + + + - + - + + + - + - + + + - + {billingCreditsEnabled && ( + <> + - - + + + + + )} + + {billingPaymentMethodsEnabled && ( + <> + + + + + + + )} + + {billingAccountDataEnabled && ( + <> + + + + + + + + + + + + + + + + + + + )} + ) } diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/CostControl/CostControl.tsx b/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx similarity index 98% rename from studio/components/interfaces/Organization/BillingSettingsV2/CostControl/CostControl.tsx rename to studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx index 800dc532996..21476ea7f09 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/CostControl/CostControl.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx @@ -22,7 +22,7 @@ import { IconCalendar, IconExternalLink, } from 'ui' -import ProjectUpdateDisabledTooltip from '../../BillingSettings/ProjectUpdateDisabledTooltip' +import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip' import SpendCapSidePanel from './SpendCapSidePanel' import Image from 'next/image' import dayjs from 'dayjs' diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/CostControl/SpendCapSidePanel.tsx b/studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/CostControl/SpendCapSidePanel.tsx rename to studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx diff --git a/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx b/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx index d7f869fb452..9c809c4aaa9 100644 --- a/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx @@ -1,29 +1,65 @@ +import { useParams } from 'common' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' import { Badge } from 'ui' -interface CreditBalanceProps { - balance: string | number - isCredit: boolean - isDebt: boolean -} +const CreditBalance = () => { + const { slug } = useParams() + const { data, error, isLoading, isError, isSuccess } = useOrganizationCustomerProfileQuery({ + slug, + }) + + const customerBalance = (data?.balance ?? 0) / 100 + const isCredit = customerBalance < 0 + const isDebt = customerBalance > 0 + const balance = + isCredit && customerBalance !== 0 + ? customerBalance.toFixed(2).toString().replace('-', '') + : customerBalance.toFixed(2) -const CreditBalance = ({ balance, isCredit, isDebt }: CreditBalanceProps) => { return ( -
-
-
-

Credit balance

- {isCredit && You have credits available} - {isDebt && Outstanding payments} + + +
+
+

Credit balance

+ {isCredit && You have credits available} + {isDebt && Outstanding payments} +
+

+ Charges will be deducted from your balance first +

-

Charges will be deducted from your balance first

-
-
- {isDebt &&

-

} -

$

-

{balance}

- {isCredit &&

/credits

} -
-
+ + + {isLoading && ( +
+ + + +
+ )} + + {isError && ( + + )} + + {isSuccess && ( +
+ {isDebt &&

-

} +

$

+

{balance}

+ {isCredit &&

/credits

} +
+ )} +
+ ) } diff --git a/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx index ac9fef7b848..023f6f732ea 100644 --- a/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx @@ -18,7 +18,6 @@ import { Modal, } from 'ui' -import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2' import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import Panel from 'components/ui/Panel' @@ -30,6 +29,7 @@ import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organizat import { useCheckPermissions, useStore } from 'hooks' import { BASE_PATH } from 'lib/constants' import { getURL } from 'lib/helpers' +import AddNewPaymentMethodModal from 'components/interfaces/Billing/Payment/AddNewPaymentMethodModal' const PaymentMethods = () => { const { ui } = useStore() diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/PaymentMethods/ChangePaymentMethodModal.tsx b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/ChangePaymentMethodModal.tsx similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/PaymentMethods/ChangePaymentMethodModal.tsx rename to studio/components/interfaces/Organization/BillingSettings/PaymentMethods/ChangePaymentMethodModal.tsx diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/PaymentMethods/DeletePaymentMethodModal.tsx b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/DeletePaymentMethodModal.tsx similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/PaymentMethods/DeletePaymentMethodModal.tsx rename to studio/components/interfaces/Organization/BillingSettings/PaymentMethods/DeletePaymentMethodModal.tsx diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/PaymentMethods/PaymentMethods.tsx b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx similarity index 99% rename from studio/components/interfaces/Organization/BillingSettingsV2/PaymentMethods/PaymentMethods.tsx rename to studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx index 633a3f5958c..e98b7e7c833 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/PaymentMethods/PaymentMethods.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx @@ -3,7 +3,6 @@ import { useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useParams } from 'common' -import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2' import { ScaffoldSection, ScaffoldSectionContent, @@ -35,6 +34,7 @@ import { } from 'ui' import ChangePaymentMethodModal from './ChangePaymentMethodModal' import DeletePaymentMethodModal from './DeletePaymentMethodModal' +import AddNewPaymentMethodModal from 'components/interfaces/Billing/Payment/AddNewPaymentMethodModal' const PaymentMethods = () => { const { ui } = useStore() diff --git a/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx b/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx deleted file mode 100644 index f1ae0314b5c..00000000000 --- a/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import dayjs from 'dayjs' -import Link from 'next/link' -import { useEffect } from 'react' -import { IconChevronRight, IconLoader } from 'ui' - -import Panel from 'components/ui/Panel' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useStore } from 'hooks' - -interface ProjectSummaryProps { - project: any -} - -const ProjectSummary = ({ project }: ProjectSummaryProps) => { - const { ui } = useStore() - const { - data: subscription, - isLoading: loading, - error, - } = useProjectSubscriptionV2Query({ projectRef: project.ref }) - - const currentPeriodStart = subscription?.current_period_start ?? 0 - const currentPeriodEnd = subscription?.current_period_end ?? 0 - - useEffect(() => { - if (error) { - ui.setNotification({ - category: 'error', - message: `Failed to get project subscription: ${(error as any)?.message ?? 'unknown'}`, - }) - } - }, [error]) - - return ( -
-
-

{project.name}

-
- {loading ? ( -
- -
- ) : ( - <> -
-

{subscription?.plan.name ?? ''}

-
-
-

{dayjs.unix(currentPeriodStart).utc().format('MMM D, YYYY')}

-

-

-

{dayjs.unix(currentPeriodEnd).utc().format('MMM D, YYYY')}

-
-
- -

View details

- - -
- - )} -
- ) -} - -interface ProjectsSummaryProps { - projects: any -} - -const ProjectsSummary = ({ projects }: ProjectsSummaryProps) => { - return ( -
-

Projects at a glance

- -
-

Name

-
-
-

Plan

-
-
-

Billing cycle

-
-
-
- } - > - {projects.map((project: any) => ( - - ))} - {projects.length === 0 && ( - -

No projects created yet

-
- )} -
-
- ) -} - -export default ProjectsSummary diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/DowngradeModal.tsx b/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/Subscription/DowngradeModal.tsx rename to studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/EnterpriseCard.tsx b/studio/components/interfaces/Organization/BillingSettings/Subscription/EnterpriseCard.tsx similarity index 100% rename from studio/components/interfaces/BillingV2/Subscription/Tier/EnterpriseCard.tsx rename to studio/components/interfaces/Organization/BillingSettings/Subscription/EnterpriseCard.tsx diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/ExitSurveyModal.tsx b/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx similarity index 98% rename from studio/components/interfaces/Organization/BillingSettingsV2/Subscription/ExitSurveyModal.tsx rename to studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx index 04d265b4ad5..910d5a291da 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/ExitSurveyModal.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx @@ -8,7 +8,7 @@ import { OrgSubscription } from 'data/subscriptions/org-subscription-query' import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation' import { useFlag, useStore } from 'hooks' import { Alert, Button, Input, Modal } from 'ui' -import ProjectUpdateDisabledTooltip from '../../BillingSettings/ProjectUpdateDisabledTooltip' +import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip' import { CANCELLATION_REASONS } from '../BillingSettings.constants' export interface ExitSurveyModalProps { diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/MembersExceedLimitModal.tsx b/studio/components/interfaces/Organization/BillingSettings/Subscription/MembersExceedLimitModal.tsx similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/Subscription/MembersExceedLimitModal.tsx rename to studio/components/interfaces/Organization/BillingSettings/Subscription/MembersExceedLimitModal.tsx diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx b/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx similarity index 98% rename from studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx rename to studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx index 42eccd4a3a7..e21e18613b6 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx @@ -3,13 +3,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useEffect, useMemo, useState } from 'react' import { useParams } from 'common' -import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2' import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { BASE_PATH } from 'lib/constants' import { getURL } from 'lib/helpers' import { Button, IconAlertCircle, IconCreditCard, IconLoader, IconPlus, Listbox } from 'ui' +import AddNewPaymentMethodModal from 'components/interfaces/Billing/Payment/AddNewPaymentMethodModal' export interface PaymentMethodSelectionProps { selectedPaymentMethod?: string diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PlanUpdateSidePanel.tsx b/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx similarity index 99% rename from studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PlanUpdateSidePanel.tsx rename to studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index 0fa948d3703..53b28f6be0f 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PlanUpdateSidePanel.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -199,7 +199,7 @@ const PlanUpdateSidePanel = () => { ) : (

${price}

)} -

{tierMeta?.costUnitOrg}

+

{tierMeta?.costUnit}

diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/Subscription.tsx b/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx similarity index 99% rename from studio/components/interfaces/Organization/BillingSettingsV2/Subscription/Subscription.tsx rename to studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx index c1936f69749..3405bc0f9d9 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/Subscription.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx @@ -22,7 +22,7 @@ import { IconCalendar, IconExternalLink, } from 'ui' -import ProjectUpdateDisabledTooltip from '../../BillingSettings/ProjectUpdateDisabledTooltip' +import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip' import PlanUpdateSidePanel from './PlanUpdateSidePanel' import { useOrganizationBillingSubscriptionCancelSchedule } from 'data/subscriptions/org-subscription-cancel-schedule-mutation' diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts index b52e11f899b..9595d935efb 100644 --- a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts +++ b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts @@ -11,14 +11,6 @@ export interface TaxId { vatPrefix?: string } -export interface StripeTaxId { - id: string - type: string - value: string - name: string - country?: string -} - export const TAX_IDS: TaxId[] = [ { name: 'AE TRN', diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx index 9b9be26b446..ce567a66886 100644 --- a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx @@ -1,39 +1,37 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' -import { isEqual } from 'lodash' import { useEffect, useState } from 'react' -import { Button, IconPlus, IconX, Input, Listbox } from 'ui' +import { useParams } from 'common' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' +import { FormActions, FormPanel, FormSection, FormSectionContent } from 'components/ui/Forms' import NoPermission from 'components/ui/NoPermission' -import Panel from 'components/ui/Panel' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' -import { useOrganizationTaxIDsQuery } from 'data/organizations/organization-tax-ids-query' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { TaxId, useOrganizationTaxIDsQuery } from 'data/organizations/organization-tax-ids-query' import { TaxIdValue, useOrganizationTaxIDsUpdateMutation, } from 'data/organizations/organization-tax-ids-update-mutation' import { useCheckPermissions, useStore } from 'hooks' import { uuidv4 } from 'lib/helpers' -import { StripeTaxId, TAX_IDS } from './TaxID.constants' -import { sanitizeTaxID } from './TaxID.utils' - -// Stripe recommends to delete tax ids and create new ones to update -// https://stripe.com/docs/billing/customer/tax-ids +import { Button, Form, IconPlus, IconX, Input, Listbox } from 'ui' +import { TAX_IDS } from './TaxID.constants' +import { checkTaxIdsEqual, sanitizeTaxID } from './TaxID.utilts' const TaxID = () => { const { ui } = useStore() const { slug } = useParams() - const { - data, - error: taxIdsError, - isLoading: isLoadingTaxIds, - isError: isErrorTaxIds, - isSuccess: isSuccessTaxIds, + data: taxIds, + error, + isLoading, + isSuccess, + isError, } = useOrganizationTaxIDsQuery({ slug }) - const taxIds = data || [] - const { mutate: updateTaxIDs, isLoading: isUpdating } = useOrganizationTaxIDsUpdateMutation({ onSuccess: (res) => { const { created, errors } = res @@ -64,20 +62,27 @@ const TaxID = () => { const [errors, setErrors] = useState([]) const [taxIdValues, setTaxIdValues] = useState([]) - const formattedTaxIds: StripeTaxId[] = taxIds.map((taxId) => { - return { - id: taxId.id, - type: taxId.type, - value: taxId.value, - name: - taxId.type === 'eu_vat' - ? `${taxId.country} VAT` - : TAX_IDS.find((option) => option.code === taxId.type)?.name ?? '', - } - }) + + const formattedTaxIds = + taxIds?.map((taxId: TaxId) => { + return { + id: taxId.id, + type: taxId.type, + value: taxId.value, + name: + taxId.type === 'eu_vat' + ? `${taxId.country} VAT` + : TAX_IDS.find((option) => option.code === taxId.type)?.name ?? '', + } + }) ?? [] + + const formId = 'tax-id-form' + const hasChanges = !checkTaxIdsEqual(taxIdValues, formattedTaxIds) + const canReadTaxIds = useCheckPermissions(PermissionAction.BILLING_READ, 'stripe.tax_ids') + const canUpdateTaxIds = useCheckPermissions(PermissionAction.BILLING_WRITE, 'stripe.tax_ids') useEffect(() => { - if (isSuccessTaxIds) { + if (isSuccess) { if (taxIdValues.length === 0) { setTaxIdValues(formattedTaxIds) } else { @@ -85,11 +90,8 @@ const TaxID = () => { setTaxIdValues(formattedTaxIds.concat(erroredTaxIds)) } } - }, [isSuccessTaxIds]) - - const hasChanges = !isEqual(taxIdValues, formattedTaxIds) - const canReadTaxIds = useCheckPermissions(PermissionAction.BILLING_READ, 'stripe.tax_ids') - const canUpdateTaxIds = useCheckPermissions(PermissionAction.BILLING_WRITE, 'stripe.tax_ids') + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess]) const onUpdateTaxId = (id: string, key: string, value: string) => { const updatedTaxIds = taxIdValues.map((taxId: any) => { @@ -113,9 +115,7 @@ const TaxID = () => { } const onRemoveTaxId = (id: string) => { - const updatedTaxIds = taxIdValues.filter((taxId: StripeTaxId) => { - return taxId.id !== id - }) + const updatedTaxIds = taxIdValues.filter((taxId) => taxId.id !== id) setTaxIdValues(updatedTaxIds) } @@ -123,7 +123,7 @@ const TaxID = () => { setTaxIdValues(formattedTaxIds) } - const onSaveTaxIds = async () => { + const onSaveTaxIds = async (values: any) => { if (!slug) return console.error('Slug is required') if (taxIds === undefined) return console.error('Tax IDs are required') @@ -132,123 +132,129 @@ const TaxID = () => { } return ( -
-
-

Tax ID

-

- If you would like to include specific tax ID(s) to your invoices.
- Make sure the tax ID looks exactly like the placeholder text. -

-
- - {isLoadingTaxIds && } - - {!isLoadingTaxIds && !canReadTaxIds && ( - - - - )} - - {isErrorTaxIds && } - - {isSuccessTaxIds && ( - - {!canUpdateTaxIds ? ( -

- You need additional permissions to update this organization's tax IDs -

- ) : ( -
- )} -
- - -
+ + +
+

Tax ID

+

+ Add tax ID(s) to have them appear in your invoices. Old invoices are not affected. +

+

+ Make sure the tax ID looks exactly like the placeholder text. +

+
+
+ + {!canReadTaxIds ? ( + + ) : ( + <> + {isLoading && ( +
+ + +
- ) - } - > - - {taxIdValues.length >= 1 ? ( -
- {taxIdValues.map((taxId: StripeTaxId, idx: number) => { - const selectedTaxId = TAX_IDS.find((option) => option.name === taxId.name) + )} - return ( -
- onUpdateTaxId(taxId.id, 'name', e)} - disabled={!canUpdateTaxIds} - className="w-[200px]" - > - {TAX_IDS.sort((a, b) => a.country.localeCompare(b.country)).map( - (option) => ( - - {option.country} - {option.name} - - ) - )} - - onUpdateTaxId(taxId.id, 'value', e.target.value)} - disabled={!canUpdateTaxIds} - /> - {canUpdateTaxIds && ( -
- ) - })} -
- ) : ( -
-

- No tax IDs -

-
+
+ } + > + + + {taxIdValues.length >= 1 ? ( +
+ {taxIdValues.map((taxId, idx: number) => { + const selectedTaxId = TAX_IDS.find( + (option) => option.name === taxId.name + ) + return ( +
+ onUpdateTaxId(taxId.id, 'name', e)} + disabled={!canUpdateTaxIds} + className="w-[200px]" + > + {TAX_IDS.sort((a, b) => a.country.localeCompare(b.country)).map( + (option) => ( + + {option.country} - {option.name} + + ) + )} + + + onUpdateTaxId(taxId.id, 'value', e.target.value) + } + disabled={!canUpdateTaxIds} + /> + {canUpdateTaxIds && ( +
+ ) + })} +
+ ) : ( +
+

+ No tax IDs +

+
+ )} + {canUpdateTaxIds && ( +
onAddNewTaxId()} + > + +

Add another ID

+
+ )} +
+
+ + )} + )} - {canUpdateTaxIds && ( -
onAddNewTaxId()} - > - -

Add another ID

-
- )} - -
- )} -
+ + )} + + ) } diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts deleted file mode 100644 index 90a37a324e0..00000000000 --- a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { StripeTaxId, TAX_IDS } from './TaxID.constants' - -/** - * Sanitize EU VAT ids so they get prepended with the country code - * - * Ex: Belgium's VAT ID should be BE0123456789 not 0123456789 - * Only EU VAT ids are affected - */ -export const sanitizeTaxID = (taxId: StripeTaxId) => { - let newID = taxId - - const selectedTaxId = TAX_IDS.find((option) => option.name === taxId.name) - - const vatIdPrefix = selectedTaxId?.vatPrefix - - // if the value doesn't start with the prefix, prepend them - if (vatIdPrefix && !taxId.value.startsWith(vatIdPrefix)) { - newID.value = `${vatIdPrefix}${taxId.value}` - } - - return newID -} diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.utilts.ts b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utilts.ts similarity index 100% rename from studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.utilts.ts rename to studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utilts.ts diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingAddress/BillingAddress.constants.ts b/studio/components/interfaces/Organization/BillingSettingsV2/BillingAddress/BillingAddress.constants.ts deleted file mode 100644 index 728b4ecef37..00000000000 --- a/studio/components/interfaces/Organization/BillingSettingsV2/BillingAddress/BillingAddress.constants.ts +++ /dev/null @@ -1,248 +0,0 @@ -export const COUNTRIES = [ - { code: 'AF', name: 'Afghanistan' }, - { code: 'AL', name: 'Albania' }, - { code: 'DZ', name: 'Algeria' }, - { code: 'AS', name: 'American Samoa' }, - { code: 'AD', name: 'Andorra' }, - { code: 'AO', name: 'Angola' }, - { code: 'AI', name: 'Anguilla' }, - { code: 'AQ', name: 'Antarctica' }, - { code: 'AG', name: 'Antigua and Barbuda' }, - { code: 'AR', name: 'Argentina' }, - { code: 'AM', name: 'Armenia' }, - { code: 'AW', name: 'Aruba' }, - { code: 'AU', name: 'Australia' }, - { code: 'AT', name: 'Austria' }, - { code: 'AZ', name: 'Azerbaijan' }, - { code: 'BS', name: 'Bahamas' }, - { code: 'BH', name: 'Bahrain' }, - { code: 'BD', name: 'Bangladesh' }, - { code: 'BB', name: 'Barbados' }, - { code: 'BY', name: 'Belarus' }, - { code: 'BE', name: 'Belgium' }, - { code: 'BZ', name: 'Belize' }, - { code: 'BJ', name: 'Benin' }, - { code: 'BM', name: 'Bermuda' }, - { code: 'BT', name: 'Bhutan' }, - { code: 'BO', name: 'Bolivia' }, - { code: 'BA', name: 'Bosnia and Herzegovina' }, - { code: 'BW', name: 'Botswana' }, - { code: 'BV', name: 'Bouvet Island' }, - { code: 'BR', name: 'Brazil' }, - { code: 'IO', name: 'British Indian Ocean Territory' }, - { code: 'VG', name: 'British Virgin Islands' }, - { code: 'BN', name: 'Brunei Darussalam' }, - { code: 'BG', name: 'Bulgaria' }, - { code: 'BF', name: 'Burkina Faso' }, - { code: 'BI', name: 'Burundi' }, - { code: 'KH', name: 'Cambodia' }, - { code: 'CM', name: 'Cameroon' }, - { code: 'CA', name: 'Canada' }, - { code: 'CV', name: 'Cape Verde' }, - { code: 'KY', name: 'Cayman Islands' }, - { code: 'CF', name: 'Central African Republic' }, - { code: 'TD', name: 'Chad' }, - { code: 'CL', name: 'Chile' }, - { code: 'CN', name: 'China' }, - { code: 'CX', name: 'Christmas Island' }, - { code: 'CC', name: 'Cocos (Keeling) Islands' }, - { code: 'CO', name: 'Colombia' }, - { code: 'KM', name: 'Comoros' }, - { code: 'CG', name: 'Congo' }, - { code: 'CK', name: 'Cook Islands' }, - { code: 'CR', name: 'Costa Rica' }, - { code: 'HR', name: 'Croatia' }, - { code: 'CU', name: 'Cuba' }, - { code: 'CW', name: 'Curaçao' }, - { code: 'CY', name: 'Cyprus' }, - { code: 'CZ', name: 'Czech Republic' }, - { code: 'CI', name: "Côte d'Ivoire" }, - { code: 'DK', name: 'Denmark' }, - { code: 'DJ', name: 'Djibouti' }, - { code: 'DM', name: 'Dominica' }, - { code: 'DO', name: 'Dominican Republic' }, - { code: 'EC', name: 'Ecuador' }, - { code: 'EG', name: 'Egypt' }, - { code: 'SV', name: 'El Salvador' }, - { code: 'GQ', name: 'Equatorial Guinea' }, - { code: 'ER', name: 'Eritrea' }, - { code: 'EE', name: 'Estonia' }, - { code: 'SZ', name: 'Eswatini' }, - { code: 'ET', name: 'Ethiopia' }, - { code: 'FK', name: 'Falkland Islands' }, - { code: 'FO', name: 'Faroe Islands' }, - { code: 'FJ', name: 'Fiji' }, - { code: 'FI', name: 'Finland' }, - { code: 'FR', name: 'France' }, - { code: 'GF', name: 'French Guiana' }, - { code: 'PF', name: 'French Polynesia' }, - { code: 'TF', name: 'French Southern Territories' }, - { code: 'GA', name: 'Gabon' }, - { code: 'GM', name: 'Gambia' }, - { code: 'GE', name: 'Georgia' }, - { code: 'DE', name: 'Germany' }, - { code: 'GH', name: 'Ghana' }, - { code: 'GI', name: 'Gibraltar' }, - { code: 'GR', name: 'Greece' }, - { code: 'GL', name: 'Greenland' }, - { code: 'GD', name: 'Grenada' }, - { code: 'GP', name: 'Guadeloupe' }, - { code: 'GU', name: 'Guam' }, - { code: 'GT', name: 'Guatemala' }, - { code: 'GG', name: 'Guernsey' }, - { code: 'GN', name: 'Guinea' }, - { code: 'GW', name: 'Guinea-Bissau' }, - { code: 'GY', name: 'Guyana' }, - { code: 'HT', name: 'Haiti' }, - { code: 'HM', name: 'Heard & McDonald Islands' }, - { code: 'HN', name: 'Honduras' }, - { code: 'HK', name: 'Hong Kong' }, - { code: 'HU', name: 'Hungary' }, - { code: 'IS', name: 'Iceland' }, - { code: 'IN', name: 'India' }, - { code: 'ID', name: 'Indonesia' }, - { code: 'IR', name: 'Iran' }, - { code: 'IQ', name: 'Iraq' }, - { code: 'IE', name: 'Ireland' }, - { code: 'IM', name: 'Isle of Man' }, - { code: 'IL', name: 'Israel' }, - { code: 'IT', name: 'Italy' }, - { code: 'JM', name: 'Jamaica' }, - { code: 'JP', name: 'Japan' }, - { code: 'JE', name: 'Jersey' }, - { code: 'JO', name: 'Jordan' }, - { code: 'KZ', name: 'Kazakhstan' }, - { code: 'KE', name: 'Kenya' }, - { code: 'KI', name: 'Kiribati' }, - { code: 'KW', name: 'Kuwait' }, - { code: 'KG', name: 'Kyrgyzstan' }, - { code: 'LA', name: 'Laos' }, - { code: 'LV', name: 'Latvia' }, - { code: 'LB', name: 'Lebanon' }, - { code: 'LS', name: 'Lesotho' }, - { code: 'LR', name: 'Liberia' }, - { code: 'LY', name: 'Libya' }, - { code: 'LI', name: 'Liechtenstein' }, - { code: 'LT', name: 'Lithuania' }, - { code: 'LU', name: 'Luxembourg' }, - { code: 'MO', name: 'Macao' }, - { code: 'MG', name: 'Madagascar' }, - { code: 'MW', name: 'Malawi' }, - { code: 'MY', name: 'Malaysia' }, - { code: 'MV', name: 'Maldives' }, - { code: 'ML', name: 'Mali' }, - { code: 'MT', name: 'Malta' }, - { code: 'MH', name: 'Marshall Islands' }, - { code: 'MQ', name: 'Martinique' }, - { code: 'MR', name: 'Mauritania' }, - { code: 'MU', name: 'Mauritius' }, - { code: 'YT', name: 'Mayotte' }, - { code: 'MX', name: 'Mexico' }, - { code: 'FM', name: 'Micronesia' }, - { code: 'MD', name: 'Moldova' }, - { code: 'MC', name: 'Monaco' }, - { code: 'MN', name: 'Mongolia' }, - { code: 'ME', name: 'Montenegro' }, - { code: 'MS', name: 'Montserrat' }, - { code: 'MA', name: 'Morocco' }, - { code: 'MZ', name: 'Mozambique' }, - { code: 'MM', name: 'Myanmar' }, - { code: 'NA', name: 'Namibia' }, - { code: 'NR', name: 'Nauru' }, - { code: 'NP', name: 'Nepal' }, - { code: 'NL', name: 'Netherlands' }, - { code: 'NC', name: 'New Caledonia' }, - { code: 'NZ', name: 'New Zealand' }, - { code: 'NI', name: 'Nicaragua' }, - { code: 'NE', name: 'Niger' }, - { code: 'NG', name: 'Nigeria' }, - { code: 'NU', name: 'Niue' }, - { code: 'NF', name: 'Norfolk Island' }, - { code: 'KP', name: 'North Korea' }, - { code: 'MK', name: 'North Macedonia' }, - { code: 'MP', name: 'Northern Mariana Islands' }, - { code: 'NO', name: 'Norway' }, - { code: 'OM', name: 'Oman' }, - { code: 'PK', name: 'Pakistan' }, - { code: 'PW', name: 'Palau' }, - { code: 'PS', name: 'Palestinian Territories' }, - { code: 'PA', name: 'Panama' }, - { code: 'PG', name: 'Papua New Guinea' }, - { code: 'PY', name: 'Paraguay' }, - { code: 'PE', name: 'Peru' }, - { code: 'PH', name: 'Philippines' }, - { code: 'PN', name: 'Pitcairn Islands' }, - { code: 'PL', name: 'Poland' }, - { code: 'PT', name: 'Portugal' }, - { code: 'PR', name: 'Puerto Rico' }, - { code: 'QA', name: 'Qatar' }, - { code: 'RO', name: 'Romania' }, - { code: 'RU', name: 'Russia' }, - { code: 'RW', name: 'Rwanda' }, - { code: 'RE', name: 'Réunion' }, - { code: 'BL', name: 'Saint Barthélemy' }, - { code: 'WS', name: 'Samoa' }, - { code: 'SM', name: 'San Marino' }, - { code: 'ST', name: 'Sao Tome and Principe' }, - { code: 'SA', name: 'Saudi Arabia' }, - { code: 'SN', name: 'Senegal' }, - { code: 'RS', name: 'Serbia' }, - { code: 'SC', name: 'Seychelles' }, - { code: 'SL', name: 'Sierra Leone' }, - { code: 'SG', name: 'Singapore' }, - { code: 'SX', name: 'Sint Maarten' }, - { code: 'SK', name: 'Slovakia' }, - { code: 'SI', name: 'Slovenia' }, - { code: 'SB', name: 'Solomon Islands' }, - { code: 'SO', name: 'Somalia' }, - { code: 'ZA', name: 'South Africa' }, - { code: 'GS', name: 'South Georgia and the South Sandwich Islands' }, - { code: 'KR', name: 'South Korea' }, - { code: 'SS', name: 'South Sudan' }, - { code: 'ES', name: 'Spain' }, - { code: 'LK', name: 'Sri Lanka' }, - { code: 'SH', name: 'St Helena' }, - { code: 'KN', name: 'St Kitts and Nevis' }, - { code: 'LC', name: 'St Lucia' }, - { code: 'MF', name: 'St Martin' }, - { code: 'PM', name: 'St Pierre and Miquelon' }, - { code: 'VC', name: 'St Vincent and the Grenadines' }, - { code: 'SD', name: 'Sudan' }, - { code: 'SR', name: 'Suriname' }, - { code: 'SJ', name: 'Svalbard & Jan Mayen' }, - { code: 'SE', name: 'Sweden' }, - { code: 'CH', name: 'Switzerland' }, - { code: 'SY', name: 'Syria' }, - { code: 'TW', name: 'Taiwan' }, - { code: 'TJ', name: 'Tajikistan' }, - { code: 'TZ', name: 'Tanzania' }, - { code: 'TH', name: 'Thailand' }, - { code: 'TL', name: 'Timor-Leste' }, - { code: 'TG', name: 'Togo' }, - { code: 'TK', name: 'Tokelau' }, - { code: 'TO', name: 'Tonga' }, - { code: 'TT', name: 'Trinidad & Tobago' }, - { code: 'TN', name: 'Tunisia' }, - { code: 'TR', name: 'Turkey' }, - { code: 'TM', name: 'Turkmenistan' }, - { code: 'TC', name: 'Turks & Caicos Islands' }, - { code: 'TV', name: 'Tuvalu' }, - { code: 'VI', name: 'US Virgin Islands' }, - { code: 'UG', name: 'Uganda' }, - { code: 'UA', name: 'Ukraine' }, - { code: 'AE', name: 'United Arab Emirates' }, - { code: 'GB', name: 'United Kingdom' }, - { code: 'US', name: 'United States of America' }, - { code: 'UY', name: 'Uruguay' }, - { code: 'UZ', name: 'Uzbekistan' }, - { code: 'VU', name: 'Vanuatu' }, - { code: 'VA', name: 'Vatican City' }, - { code: 'VE', name: 'Venezuela' }, - { code: 'VN', name: 'Vietnam' }, - { code: 'WF', name: 'Wallis & Futuna' }, - { code: 'EH', name: 'Western Sahara' }, - { code: 'YE', name: 'Yemen' }, - { code: 'ZM', name: 'Zambia' }, - { code: 'ZW', name: 'Zimbabwe' }, - { code: 'AX', name: 'Åland Islands' }, -] diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingAddress/BillingAddress.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/BillingAddress/BillingAddress.tsx deleted file mode 100644 index c30aa76e556..00000000000 --- a/studio/components/interfaces/Organization/BillingSettingsV2/BillingAddress/BillingAddress.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useEffect } from 'react' - -import { useParams } from 'common' -import { - ScaffoldSection, - ScaffoldSectionContent, - ScaffoldSectionDetail, -} from 'components/layouts/Scaffold' -import AlertError from 'components/ui/AlertError' -import { FormActions, FormPanel, FormSection, FormSectionContent } from 'components/ui/Forms' -import NoPermission from 'components/ui/NoPermission' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' -import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' -import { useCheckPermissions, useStore } from 'hooks' -import { Form, Input, Listbox } from 'ui' -import { COUNTRIES } from './BillingAddress.constants' - -const BillingAddress = () => { - const { ui } = useStore() - const { slug } = useParams() - const { data, error, isLoading, isSuccess, isError } = useOrganizationCustomerProfileQuery({ - slug, - }) - const { mutateAsync: updateCustomerProfile, isLoading: isUpdating } = - useOrganizationCustomerProfileUpdateMutation() - - const formId = 'billing-address-form' - const { city, country, line1, line2, postal_code, state } = data?.address ?? {} - - const canReadBillingAddress = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.customer' - ) - const canUpdateBillingAddress = useCheckPermissions( - PermissionAction.BILLING_WRITE, - 'stripe.customer' - ) - - const initialValues = { city, country, line1, line2, postal_code, state } - - const validate = (values: any) => { - const errors = {} as any - if ( - (values.line1 || values.line2 || values.postal_code || values.state || values.city) && - !values.country - ) { - errors['country'] = 'Please select a country' - } - return errors - } - - const onSubmit = async (values: any, { resetForm }: any) => { - if (!slug) return console.error('Slug is required') - - try { - await updateCustomerProfile({ slug, address: values }) - ui.setNotification({ - category: 'success', - message: 'Successfully updated billing address', - }) - resetForm({ values, initialValues: values }) - } catch (error) {} - } - - return ( - - -
-

Billing Address

-

- This will be reflected in every upcoming invoice, past invoices are not affected -

-
-
- - {!canReadBillingAddress ? ( - - ) : ( - <> - {isLoading && ( -
- - - -
- )} - - {isError && ( - - )} - - {isSuccess && ( -
- {({ values, initialValues, handleReset, resetForm }: any) => { - const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) - - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (isSuccess && data !== undefined) { - const { city, country, line1, line2, postal_code, state } = - data?.address ?? {} - const values = { city, country, line1, line2, postal_code, state } - resetForm({ values, initialValues: values }) - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess]) - - return ( - - -
- } - > - - - - -
- - - --- - - {COUNTRIES.map((country) => ( - - {country.name} - - ))} - - -
-
- - -
-
-
- - ) - }} - - )} - - )} - - - ) -} - -export default BillingAddress diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingEmail.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/BillingEmail.tsx deleted file mode 100644 index a7ae06dad72..00000000000 --- a/studio/components/interfaces/Organization/BillingSettingsV2/BillingEmail.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useQueryClient } from '@tanstack/react-query' -import { useEffect } from 'react' - -import { useParams } from 'common' -import { - ScaffoldSection, - ScaffoldSectionContent, - ScaffoldSectionDetail, -} from 'components/layouts/Scaffold' -import { FormActions, FormPanel, FormSection, FormSectionContent } from 'components/ui/Forms' -import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' -import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' -import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' -import { Form, Input } from 'ui' - -const BillingEmail = () => { - const { ui } = useStore() - const { slug } = useParams() - const queryClient = useQueryClient() - const selectedOrganization = useSelectedOrganization() - const { name, billing_email } = selectedOrganization ?? {} - - const formId = 'org-billing-email' - const initialValues = { billing_email: billing_email ?? '' } - - const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') - const canReadBillingEmail = useCheckPermissions(PermissionAction.READ, 'organizations') - const { mutateAsync: updateOrganization, isLoading: isUpdating } = useOrganizationUpdateMutation() - - const onUpdateOrganizationEmail = async (values: any, { resetForm }: any) => { - if (!canUpdateOrganization) { - return ui.setNotification({ - category: 'error', - message: 'You do not have the required permissions to update this organization', - }) - } - if (!slug) return console.error('Slug is required') - if (!name) return console.error('Organization name is required') - - try { - const { billing_email } = await updateOrganization({ - slug, - name, - billing_email: values.billing_email, - }) - resetForm({ values: { billing_email }, initialValues: { billing_email } }) - invalidateOrganizationsQuery(queryClient) - ui.setNotification({ - category: 'success', - message: 'Successfully saved settings', - }) - } finally { - } - } - - return ( - - -
-

Email Recipient

-

- All billing correspondence will go to this email -

-
-
- -
- {({ handleReset, values, initialValues, resetForm }: any) => { - const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) - - // [Alaister] although this "technically" is breaking the rules of React hooks - // it won't error because the hooks are always rendered in the same order - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - const values = { billing_email: billing_email ?? '' } - resetForm({ values, initialValues: values }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [slug]) - - return ( - - -
- } - > - - - - - - - ) - }} - - - - ) -} - -export default BillingEmail diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.tsx deleted file mode 100644 index a92669de10b..00000000000 --- a/studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' -import BillingAddress from './BillingAddress/BillingAddress' -import BillingEmail from './BillingEmail' -import CostControl from './CostControl/CostControl' -import CreditBalance from './CreditBalance' -import PaymentMethods from './PaymentMethods/PaymentMethods' -import Subscription from './Subscription/Subscription' -import TaxID from './TaxID/TaxID' -import BillingBreakdown from './BillingBreakdown/BillingBreakdown' -import { useIsFeatureEnabled } from 'hooks' - -const BillingSettings = () => { - const { - billingAccountData: billingAccountDataEnabled, - billingPaymentMethods: billingPaymentMethodsEnabled, - billingCredits: billingCreditsEnabled, - } = useIsFeatureEnabled(['billing:account_data', 'billing:payment_methods', 'billing:credits']) - - return ( - <> - - - - - - - - - - - - - - - - - {billingCreditsEnabled && ( - <> - - - - - - - )} - - {billingPaymentMethodsEnabled && ( - <> - - - - - - - )} - - {billingAccountDataEnabled && ( - <> - - - - - - - - - - - - - - - - - - - )} - - ) -} - -export default BillingSettings diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/CreditBalance.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/CreditBalance.tsx deleted file mode 100644 index 9c809c4aaa9..00000000000 --- a/studio/components/interfaces/Organization/BillingSettingsV2/CreditBalance.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useParams } from 'common' -import { - ScaffoldSection, - ScaffoldSectionContent, - ScaffoldSectionDetail, -} from 'components/layouts/Scaffold' -import AlertError from 'components/ui/AlertError' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' -import { Badge } from 'ui' - -const CreditBalance = () => { - const { slug } = useParams() - const { data, error, isLoading, isError, isSuccess } = useOrganizationCustomerProfileQuery({ - slug, - }) - - const customerBalance = (data?.balance ?? 0) / 100 - const isCredit = customerBalance < 0 - const isDebt = customerBalance > 0 - const balance = - isCredit && customerBalance !== 0 - ? customerBalance.toFixed(2).toString().replace('-', '') - : customerBalance.toFixed(2) - - return ( - - -
-
-

Credit balance

- {isCredit && You have credits available} - {isDebt && Outstanding payments} -
-

- Charges will be deducted from your balance first -

-
-
- - {isLoading && ( -
- - - -
- )} - - {isError && ( - - )} - - {isSuccess && ( -
- {isDebt &&

-

} -

$

-

{balance}

- {isCredit &&

/credits

} -
- )} -
-
- ) -} - -export default CreditBalance diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/EnterpriseCard.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/EnterpriseCard.tsx deleted file mode 100644 index 9bf42adeefb..00000000000 --- a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/EnterpriseCard.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import clsx from 'clsx' -import { PricingInformation } from 'shared-data' -import { Button, IconCheck } from 'ui' - -export interface EnterpriseCardProps { - plan: PricingInformation - isCurrentPlan: boolean -} - -const EnterpriseCard = ({ plan, isCurrentPlan }: EnterpriseCardProps) => { - return ( -
-
-
-

{plan.name}

- {isCurrentPlan ? ( -
- Current plan -
- ) : plan.nameBadge ? ( -
- {plan.nameBadge} -
- ) : null} -
- -

{plan.description}

- - - - -
- -
-
    - {plan.features.map((feature) => ( -
  • -
  • - ))} -
- - - - -
-
- ) -} - -export default EnterpriseCard diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.constants.ts b/studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.constants.ts deleted file mode 100644 index 9595d935efb..00000000000 --- a/studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.constants.ts +++ /dev/null @@ -1,459 +0,0 @@ -// List of available Tax IDs as reflected in Stripe's web portal -// This was manually ported over so there may be a chance of mistakes -// Last updated as of 29th March 2022. -// The code may not necessarily match with the name (ref SE_VAT) -// https://stripe.com/docs/api/customer_tax_ids/create -export interface TaxId { - name: string - code: string - country: string - placeholder: string - vatPrefix?: string -} - -export const TAX_IDS: TaxId[] = [ - { - name: 'AE TRN', - code: 'ae_trn', - country: 'United Arab Emirates', - placeholder: '123456789012345', - }, - { - name: 'AT VAT', - code: 'eu_vat', - country: 'Austria', - placeholder: 'ATU12345678', - vatPrefix: 'ATU', - }, - { - name: 'AU ABN', - code: 'au_abn', - country: 'Australia', - placeholder: '12345678912', - }, - { - name: 'AU ARN', - code: 'au_arn', - country: 'Australia', - placeholder: '123456789123', - }, - { - name: 'BE VAT', - code: 'eu_vat', - country: 'Belgium', - placeholder: 'BE0123456789', - vatPrefix: 'BE', - }, - { - name: 'BG VAT', - code: 'eu_vat', - country: 'Bulgaria', - placeholder: 'BG0123456789', - vatPrefix: 'BG', - }, - { - name: 'BR CNPJ', - code: 'br_cnpj', - country: 'Brazil', - placeholder: '01.234.456/5432-10', - }, - { - name: 'BR CPF', - code: 'br_cpf', - country: 'Brazil', - placeholder: '123.456.789-87', - }, - { - name: 'CA BN', - code: 'ca_bn', - country: 'Canada', - placeholder: '123456789', - }, - { - name: 'CA GST/HST', - code: 'ca_gst_hst', - country: 'Canada', - placeholder: '123456789RT0002', - }, - { - name: 'CA PST-BC', - code: 'ca_pst_bc', - country: 'Canada', - placeholder: 'PST-1234-5678', - }, - { - name: 'CA PST-MB', - code: 'ca_pst_mb', - country: 'Canada', - placeholder: '123456-7', - }, - { - name: 'CA PST-SK', - code: 'ca_pst_mb', - country: 'Canada', - placeholder: '1234567', - }, - { - name: 'CA QST', - code: 'ca_qst', - country: 'Canada', - placeholder: '1234567890TQ1234', - }, - { - name: 'CH VAT', - code: 'ch_vat', - country: 'Switzerland', - placeholder: 'CHE-123.456.789 MWST', - vatPrefix: 'CHE', - }, - { - name: 'CL TIN', - code: 'cl_tin', - country: 'Chile', - placeholder: '12.345.678-K', - }, - { - name: 'CY VAT', - code: 'eu_vat', - country: 'Cyprus', - placeholder: 'CY12345678Z', - vatPrefix: 'CY', - }, - { - name: 'CZ VAT', - code: 'eu_vat', - country: 'Czech Republic', - placeholder: 'CZ1234567890', - vatPrefix: 'CZ', - }, - { - name: 'DE VAT', - code: 'eu_vat', - country: 'Germany', - placeholder: 'DE123456789', - vatPrefix: 'DE', - }, - { - name: 'DK VAT', - code: 'eu_vat', - country: 'Denmark', - placeholder: 'DK12345678', - vatPrefix: 'DK', - }, - { - name: 'EE VAT', - code: 'eu_vat', - country: 'Estonia', - placeholder: 'EE123456789', - vatPrefix: 'EE', - }, - { - name: 'ES CIF', - code: 'es_cif', - country: 'Spain', - placeholder: 'A12345678', - }, - { - name: 'ES VAT', - code: 'eu_vat', - country: 'Spain', - placeholder: 'ESX1234567Z', - vatPrefix: 'ES', - }, - { - name: 'FI VAT', - code: 'eu_vat', - country: 'Finland', - placeholder: 'FI12345678', - vatPrefix: 'FI', - }, - { - name: 'FR VAT', - code: 'eu_vat', - country: 'France', - placeholder: 'FRAB123456789', - vatPrefix: 'FR', - }, - { - name: 'GB VAT', - code: 'eu_vat', - country: 'United Kingdom', - placeholder: 'GB123456789', - vatPrefix: 'GB', - }, - { - name: 'GE VAT', - code: 'ge_vat', - country: 'Georgia', - placeholder: '123456789', - }, - { - name: 'EL VAT', - code: 'eu_vat', - country: 'Greece', - placeholder: 'EL123456789', - vatPrefix: 'EL', - }, - { - name: 'HK BR', - code: 'hk_br', - country: 'Hong Kong SAR China', - placeholder: '12345678', - }, - { - name: 'HR VAT', - code: 'eu_vat', - country: 'Croatia', - placeholder: 'HR12345678912', - vatPrefix: 'HR', - }, - { - name: 'HU VAT', - code: 'eu_vat', - country: 'Hungary', - placeholder: 'HU12345678912', - vatPrefix: 'HU', - }, - { - name: 'ID NPWP', - code: 'id_npwp', - country: 'Indonesia', - placeholder: '12.345.678.9-012.345', - }, - { - name: 'IE VAT', - code: 'eu_vat', - country: 'Ireland', - placeholder: 'IE1234567AB', - vatPrefix: 'IE', - }, - { - name: 'IL VAT', - code: 'il_vat', - country: 'Israel', - placeholder: '000012345', - }, - { - name: 'IN GST', - code: 'in_gst', - country: 'India', - placeholder: '12ABCDE3456FGZH', - }, - { - name: 'IS VAT', - code: 'is_vat', - country: 'Iceland', - placeholder: '123456', - }, - { - name: 'IT VAT', - code: 'eu_vat', - country: 'Italy', - placeholder: 'IT12345678912', - vatPrefix: 'IT', - }, - { - name: 'JP CN', - code: 'jp_cn', - country: 'Japan', - placeholder: '1234567891234', - }, - { - name: 'JP RN', - code: 'jp_rn', - country: 'Japan', - placeholder: '12345', - }, - { - name: 'KR BRN', - code: 'kr_brn', - country: 'Korea', - placeholder: '123-45-67890', - }, - { - name: 'LI UID', - code: 'li_uid', - country: 'Liechtenstein', - placeholder: 'CHE123456789', - }, - { - name: 'LT VAT', - code: 'eu_vat', - country: 'Lithuania', - placeholder: 'LT123456789123', - vatPrefix: 'LT', - }, - { - name: 'LU VAT', - code: 'eu_vat', - country: 'Luxembourg', - placeholder: 'LU12345678', - vatPrefix: 'LU', - }, - { - name: 'LV VAT', - code: 'eu_vat', - country: 'Latvia', - placeholder: 'LV12345678912', - vatPrefix: 'LV', - }, - { - name: 'MT VAT', - code: 'eu_vat', - country: 'Malta', - placeholder: 'MT12345678', - vatPrefix: 'MT', - }, - { - name: 'MX RFC', - code: 'mx_rfc', - country: 'Mexico', - placeholder: 'ABC010203AB9', - }, - { - name: 'MY FRP', - code: 'my_frp', - country: 'Malaysia', - placeholder: '12345678', - }, - { - name: 'MY ITN', - code: 'my_itn', - country: 'Malaysia', - placeholder: 'C 1234567890', - }, - { - name: 'MY SST', - code: 'my_sst', - country: 'Malaysia', - placeholder: 'A12-3456-78912345', - }, - { - name: 'NL VAT', - code: 'eu_vat', - country: 'Netherlands', - placeholder: 'NL123456789B12', - vatPrefix: 'NL', - }, - { - name: 'NO VAT', - code: 'no_vat', - country: 'Norway', - placeholder: '123456789MVA', - }, - { - name: 'NZ GST', - code: 'nz_gst', - country: 'New Zealand', - placeholder: '123456789', - }, - { - name: 'PL VAT', - code: 'eu_vat', - country: 'Poland', - placeholder: 'PL1234567890', - vatPrefix: 'PL', - }, - { - name: 'PT VAT', - code: 'eu_vat', - country: 'Portugal', - placeholder: 'PT123456789', - vatPrefix: 'PT', - }, - { - name: 'RO VAT', - code: 'eu_vat', - country: 'Romania', - placeholder: 'RO1234567891', - vatPrefix: 'RO', - }, - { - name: 'RU INN', - code: 'ru_inn', - country: 'Russia', - placeholder: '1234567891', - }, - { - name: 'RU KPP', - code: 'ru_kpp', - country: 'Russia', - placeholder: '123456789', - }, - { - name: 'SA VAT', - code: 'sa_vat', - country: 'Saudi Arabia', - placeholder: '123456789012345', - }, - { - name: 'SE VAT', - code: 'eu_vat', - country: 'Sweden', - placeholder: 'SE123456789123', - vatPrefix: 'SE', - }, - { - name: 'SG GST', - code: 'sg_gst', - country: 'Singapore', - placeholder: 'M12345678X', - }, - { - name: 'SG UEN', - code: 'sg_uen', - country: 'Singapore', - placeholder: '123456789F', - }, - { - name: 'SI VAT', - code: 'eu_vat', - country: 'Slovenia', - placeholder: 'SI12345678', - vatPrefix: 'SI', - }, - { - name: 'SK VAT', - code: 'eu_vat', - country: 'Slovakia', - placeholder: 'SK1234567891', - vatPrefix: 'SK', - }, - { - name: 'TH VAT', - code: 'th_vat', - country: 'Thailand', - placeholder: '1234567891234', - }, - { - name: 'TW VAT', - code: 'tw_vat', - country: 'Taiwan', - placeholder: '12345678', - }, - { - name: 'UA VAT', - code: 'ua_vat', - country: 'Ukraine', - placeholder: '123456789', - }, - { - name: 'US EIN', - code: 'us_ein', - country: 'United States', - placeholder: '12-3456789', - }, - { - name: 'XI VAT', - code: 'eu_vat', - country: 'United Kingdom (Northern Ireland)', - placeholder: 'XI123456789', - vatPrefix: 'XI', - }, - { - name: 'ZA VAT', - code: 'za_vat', - country: 'South Africa', - placeholder: '4123456789', - }, -] diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.tsx deleted file mode 100644 index cf1ca436d83..00000000000 --- a/studio/components/interfaces/Organization/BillingSettingsV2/TaxID/TaxID.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useEffect, useState } from 'react' - -import { useParams } from 'common' -import { - ScaffoldSection, - ScaffoldSectionContent, - ScaffoldSectionDetail, -} from 'components/layouts/Scaffold' -import AlertError from 'components/ui/AlertError' -import { FormActions, FormPanel, FormSection, FormSectionContent } from 'components/ui/Forms' -import NoPermission from 'components/ui/NoPermission' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { TaxId, useOrganizationTaxIDsQuery } from 'data/organizations/organization-tax-ids-query' -import { - TaxIdValue, - useOrganizationTaxIDsUpdateMutation, -} from 'data/organizations/organization-tax-ids-update-mutation' -import { useCheckPermissions, useStore } from 'hooks' -import { uuidv4 } from 'lib/helpers' -import { Button, Form, IconPlus, IconX, Input, Listbox } from 'ui' -import { TAX_IDS } from './TaxID.constants' -import { checkTaxIdsEqual, sanitizeTaxID } from './TaxID.utilts' - -const TaxID = () => { - const { ui } = useStore() - const { slug } = useParams() - const { - data: taxIds, - error, - isLoading, - isSuccess, - isError, - } = useOrganizationTaxIDsQuery({ slug }) - const { mutate: updateTaxIDs, isLoading: isUpdating } = useOrganizationTaxIDsUpdateMutation({ - onSuccess: (res) => { - const { created, errors } = res - setErrors(errors?.map((taxId) => taxId.id) ?? []) - const updatedTaxIds = - created?.map((x) => { - return { - id: x.id, - type: x.type, - value: x.value, - name: - x.type === 'eu_vat' - ? `${x.country} VAT` - : TAX_IDS.find((option) => option.code === x.type)?.name ?? '', - } - }) ?? [] - setTaxIdValues(updatedTaxIds) - - if (errors !== undefined && errors.length > 0) { - errors.forEach((taxId: any) => { - ui.setNotification({ category: 'error', message: taxId.result.error.message }) - }) - } else { - ui.setNotification({ category: 'success', message: 'Successfully updated tax IDs' }) - } - }, - }) - - const [errors, setErrors] = useState([]) - const [taxIdValues, setTaxIdValues] = useState([]) - - const formattedTaxIds = - taxIds?.map((taxId: TaxId) => { - return { - id: taxId.id, - type: taxId.type, - value: taxId.value, - name: - taxId.type === 'eu_vat' - ? `${taxId.country} VAT` - : TAX_IDS.find((option) => option.code === taxId.type)?.name ?? '', - } - }) ?? [] - - const formId = 'tax-id-form' - const hasChanges = !checkTaxIdsEqual(taxIdValues, formattedTaxIds) - const canReadTaxIds = useCheckPermissions(PermissionAction.BILLING_READ, 'stripe.tax_ids') - const canUpdateTaxIds = useCheckPermissions(PermissionAction.BILLING_WRITE, 'stripe.tax_ids') - - useEffect(() => { - if (isSuccess) { - if (taxIdValues.length === 0) { - setTaxIdValues(formattedTaxIds) - } else { - const erroredTaxIds = taxIdValues.filter((taxId: any) => errors.includes(taxId.id)) - setTaxIdValues(formattedTaxIds.concat(erroredTaxIds)) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess]) - - const onUpdateTaxId = (id: string, key: string, value: string) => { - const updatedTaxIds = taxIdValues.map((taxId: any) => { - if (taxId.id === id) { - if (key === 'name') { - const selectedTaxIdOption = TAX_IDS.find((option) => option.name === value) - return { ...taxId, [key]: value, type: selectedTaxIdOption?.code } - } else { - return { ...taxId, [key]: value } - } - } - return taxId - }) - setTaxIdValues(updatedTaxIds) - } - - const onAddNewTaxId = () => { - const newTaxId = { id: uuidv4(), type: TAX_IDS[0].code, value: '', name: TAX_IDS[0].name } - const updatedTaxIds = taxIdValues.concat([newTaxId]) - setTaxIdValues(updatedTaxIds) - } - - const onRemoveTaxId = (id: string) => { - const updatedTaxIds = taxIdValues.filter((taxId) => taxId.id !== id) - setTaxIdValues(updatedTaxIds) - } - - const onResetTaxIds = () => { - setTaxIdValues(formattedTaxIds) - } - - const onSaveTaxIds = async (values: any) => { - if (!slug) return console.error('Slug is required') - if (taxIds === undefined) return console.error('Tax IDs are required') - - const newIds = taxIdValues.map((x) => sanitizeTaxID(x)) - updateTaxIDs({ slug, existingIds: taxIds, newIds }) - } - - return ( - - -
-

Tax ID

-

- If you would like to include specific tax ID(s) to your invoices. Old invoices are not - affected. -

-

- Make sure the tax ID looks exactly like the placeholder text. -

-
-
- - {!canReadTaxIds ? ( - - ) : ( - <> - {isLoading && ( -
- - - -
- )} - - {isError && ( - - )} - - {isSuccess && ( -
- {() => ( - - onResetTaxIds()} - helper={ - !canUpdateTaxIds - ? "You need additional permissions to manage this organization's tax IDs" - : undefined - } - /> - - } - > - - - {taxIdValues.length >= 1 ? ( -
- {taxIdValues.map((taxId, idx: number) => { - const selectedTaxId = TAX_IDS.find( - (option) => option.name === taxId.name - ) - return ( -
- onUpdateTaxId(taxId.id, 'name', e)} - disabled={!canUpdateTaxIds} - className="w-[200px]" - > - {TAX_IDS.sort((a, b) => a.country.localeCompare(b.country)).map( - (option) => ( - - {option.country} - {option.name} - - ) - )} - - - onUpdateTaxId(taxId.id, 'value', e.target.value) - } - disabled={!canUpdateTaxIds} - /> - {canUpdateTaxIds && ( -
- ) - })} -
- ) : ( -
-

- No tax IDs -

-
- )} - {canUpdateTaxIds && ( -
onAddNewTaxId()} - > - -

Add another ID

-
- )} -
-
-
- )} - - )} - - )} -
-
- ) -} - -export default TaxID diff --git a/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx b/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx index 92d15630abc..b021de7dc84 100644 --- a/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx +++ b/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx @@ -7,7 +7,7 @@ import Link from 'next/link' import { useEffect, useState } from 'react' import { Collapsible, Form, IconChevronRight, Input, Toggle } from 'ui' -import NoProjectsOnPaidOrgInfo from 'components/interfaces/BillingV2/NoProjectsOnPaidOrgInfo' +import NoProjectsOnPaidOrgInfo from 'components/interfaces/Billing/NoProjectsOnPaidOrgInfo' import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold' import { FormActions, diff --git a/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx b/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx deleted file mode 100644 index c9a4bddd019..00000000000 --- a/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx +++ /dev/null @@ -1,508 +0,0 @@ -import * as Tooltip from '@radix-ui/react-tooltip' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { observer } from 'mobx-react-lite' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect, useMemo, useState } from 'react' -import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - Button, - IconAlertCircle, - IconAlertTriangle, - IconHelpCircle, - Listbox, - Loading, - Modal, - Toggle, -} from 'ui' - -import { SpendCapModal } from 'components/interfaces/BillingV2' -import InformationBox from 'components/ui/InformationBox' -import { useOrganizationBillingMigrationMutation } from 'data/organizations/organization-migrate-billing-mutation' -import { useOrganizationBillingMigrationPreview } from 'data/organizations/organization-migrate-billing-preview-query' -import { useCheckPermissions, useFlag, useSelectedOrganization, useStore } from 'hooks' -import { PRICING_TIER_LABELS_ORG } from 'lib/constants' -import PaymentMethodSelection from '../BillingSettingsV2/Subscription/PaymentMethodSelection' - -const MigrateOrganizationBillingButton = observer(() => { - const { ui } = useStore() - const router = useRouter() - const organization = useSelectedOrganization() - - const disableOrgBillingMigration = useFlag('disableOrgBillingMigration') - const canMigrateOrg = useCheckPermissions(PermissionAction.BILLING_WRITE, 'stripe.subscriptions') - - const [isOpen, setIsOpen] = useState(false) - const [tier, setTier] = useState('') - const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false) - const [isSpendCapEnabled, setIsSpendCapEnabled] = useState(false) - const [paymentMethodId, setPaymentMethodId] = useState('') - - const dbTier = useMemo(() => { - if (tier === '') return '' - if (tier === 'PRO' && !isSpendCapEnabled) { - return `tier_payg` - } else { - return `tier_` + tier.toLocaleLowerCase() - } - }, [tier, isSpendCapEnabled]) - - const { - error: migrationError, - mutate: migrateBilling, - isLoading: isMigrating, - } = useOrganizationBillingMigrationMutation({ - onSuccess: () => { - ui.setNotification({ - message: 'Successfully migrated to organization-based billing', - category: 'success', - duration: 5000, - }) - router.push('/projects') - setIsOpen(false) - }, - }) - - const { - data: migrationPreviewData, - error: migrationPreviewError, - isLoading: migrationPreviewIsLoading, - isSuccess: migrationPreviewIsSuccess, - remove, - refetch: previewMigration, - } = useOrganizationBillingMigrationPreview( - { tier: dbTier, organizationSlug: organization?.slug }, - { enabled: false, refetchOnWindowFocus: false } - ) - - useEffect(() => { - if (dbTier) { - previewMigration() - } - }, [dbTier]) - - useEffect(() => { - if (isOpen) { - // reset state - setTier('') - } else { - // Invalidate cache - remove() - } - }, [isOpen]) - - const selectedLimitedUsage = useMemo( - () => tier === 'PRO' && isSpendCapEnabled, - [tier, isSpendCapEnabled] - ) - - const downgradingToLimitedUsage = useMemo(() => { - if (migrationPreviewData) { - const hadUsageBillingEnabled = migrationPreviewData.old_tiers.some((it) => - ['tier_payg', 'tier_team', 'tier_enterprise'].includes(it) - ) - - return hadUsageBillingEnabled && selectedLimitedUsage - } else { - return false - } - }, [migrationPreviewData, selectedLimitedUsage]) - - const toggle = () => { - setIsOpen(!isOpen) - } - - const onConfirmMigrate = async () => { - if (!tier) return - if (!canMigrateOrg) { - return ui.setNotification({ - category: 'error', - message: 'You do not have the required permissions to migrate this organization', - }) - } - migrateBilling({ organizationSlug: organization?.slug, tier: dbTier, paymentMethodId }) - } - - return ( - <> -
- - - - - {(!canMigrateOrg || disableOrgBillingMigration) && ( - - - -
- - {!canMigrateOrg - ? 'You need additional permissions to migrate this organization' - : 'Migrations are temporarily disabled, please try again later.'} - -
-
-
- )} -
-
- -
Migrate organization
- - } - > -
- {migrationPreviewData?.addons_to_be_removed && - migrationPreviewData.addons_to_be_removed.length > 0 && ( - <> - -
- - - Project addons will be removed - -
- The following project addons will be removed when downgrading -
    - {migrationPreviewData.addons_to_be_removed.map((addon) => - addon.addons.map((variant) => ( -
  • - {variant.type === 'pitr' - ? 'PITR - ' - : variant.type === 'compute_instance' - ? 'Compute Instance - ' - : ''} - {variant.name} for project {addon.projectName || addon.projectRef} -
  • - )) - )} -
-
-
-
-
-
- - - )} - -
-

- With organization-based billing, you'll have a single subscription for your entire - organization. For a detailed breakdown of changes, see{' '} - - Billing Migration Docs - - . To transfer projects to a different organization, visit{' '} - - general project settings - - . -

-
-
- - - - - Select Plan - - {Object.entries(PRICING_TIER_LABELS_ORG).map(([k, v]) => { - return ( - - {v} - - ) - })} - - -

- The pricing plan, along with included usage limits will apply to your entire - organization. See{' '} - - Pricing - {' '} - for more details. -

- - {tier !== '' && tier !== 'FREE' && ( -
-

- Paid plans come with one compute instance included. Additional projects will at - least cost the compute instance hours used (min $10/month). See{' '} - - Compute Instance Usage Billing - {' '} - for more details. -

-
- )} -
- - - - {tier === 'PRO' && ( - <> - -
-
-

- Enable spend cap{' '} - setShowSpendCapHelperModal(true)} - /> -

-
- -
- setIsSpendCapEnabled(!isSpendCapEnabled)} - /> -
- -
-

- When enabled, usage is limited to the plan's quota, with restrictions when - limits are exceeded. When disabled, you scale beyond Pro limits without - restrictions and pay for over-usage beyond the quota. -

-
- - setShowSpendCapHelperModal(false)} - /> -
-
- - - )} - - {tier && tier !== 'FREE' && ( - <> - - setPaymentMethodId(pm)} - /> - - - - )} - - - -
- {migrationPreviewError && ( - - - Organization cannot be migrated - - {migrationPreviewError.message} - - - )} - - {(downgradingToLimitedUsage || - (!downgradingToLimitedUsage && selectedLimitedUsage)) && ( - - - Spend Cap is enabled - -
- {downgradingToLimitedUsage ? ( -

- You previously had the spend cap disabled to scale seamlessly beyond the - included quota. You will run into restrictions in case you exceed the - included quota. -

- ) : ( -

- With the Spend Cap enabled, your projects will be restricted once you - exhaust the included quota. To scale seamlessly beyond the included - quota, disable the Spend Cap. -

- )} - - -
-
-
- )} -
-
- - {migrationError && ( - - - Organization cannot be migrated - {migrationError.message} - - )} -
- - {migrationPreviewIsSuccess && dbTier !== 'tier_free' && ( - - - Estimated monthly price is $ - {migrationPreviewData.monthly_invoice_breakdown.reduce( - (prev, cur) => prev + cur.total_price, - 0 - )}{' '} - + usage - - } - hideCollapse={false} - description={ -
-
- - - - - - - - - - {migrationPreviewData.monthly_invoice_breakdown.map((item) => ( - - - - - - - ))} - - - - - - - - -
- Item - - Count - - Unit price - - Price -
{item.description ?? 'Unknown'}{item.quantity} - {item.unit_price === 0 ? 'FREE' : `$${item.unit_price}`} - ${item.total_price}
Total - - - $ - {migrationPreviewData.monthly_invoice_breakdown.reduce( - (prev, cur) => prev + cur.total_price, - 0 - ) ?? 0} -
- - {migrationPreviewData.old_tiers.some((it) => it === 'tier_free') && ( -
-

- While paused projects do not incur compute costs, the breakdown assumes - that none of your projects are paused. Unpausing projects is self-serve - and pausing projects on paid plans is currently only possible via support - due to infrastructure restrictions. -

-
- )} -
- } - /> - - )} - - -

- The migration can take up to 30 seconds, please do not cancel the request or close - your browser. -

- - -
- - - - ) -}) - -export default MigrateOrganizationBillingButton diff --git a/studio/components/interfaces/Organization/GeneralSettings/OrganizationBillingMigrationPanel.tsx b/studio/components/interfaces/Organization/GeneralSettings/OrganizationBillingMigrationPanel.tsx deleted file mode 100644 index 5c6953a564f..00000000000 --- a/studio/components/interfaces/Organization/GeneralSettings/OrganizationBillingMigrationPanel.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { observer } from 'mobx-react-lite' -import Link from 'next/link' -import { Badge, Button, IconExternalLink, IconTruck } from 'ui' - -import Panel from 'components/ui/Panel' -import MigrateOrganizationBillingButton from './MigrateOrganizationBillingButton' - -const OrganizationBillingMigrationPanel = observer(() => { - return ( -
-
-

Organization-based billing

- - - New - - -
-

Migrate this project to organization-based billing

- - -
-
-

- You will have a single subscription for all projects inside the organization, rather - than individual subscriptions per project. -

-
-
- - -
-
-
-
-
- ) -}) - -export default OrganizationBillingMigrationPanel diff --git a/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx b/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx index acdb93e2612..6845cd7f0fc 100644 --- a/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx +++ b/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx @@ -2,8 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import Link from 'next/link' import { useEffect, useState } from 'react' -import { InvoiceStatusBadge } from 'components/interfaces/BillingV2' -import { InvoiceStatus } from 'components/interfaces/BillingV2/Invoices.types' +import { InvoiceStatus } from 'components/interfaces/Billing/Invoices.types' import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' @@ -14,6 +13,7 @@ import { useInvoicesCountQuery } from 'data/invoices/invoices-count-query' import { useInvoicesQuery } from 'data/invoices/invoices-query' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { Button, IconChevronLeft, IconChevronRight, IconDownload, IconFileText } from 'ui' +import InvoiceStatusBadge from 'components/interfaces/Billing/InvoiceStatusBadge' const PAGE_LIMIT = 10 diff --git a/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index d112c3128e6..4cf76281f04 100644 --- a/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -16,7 +16,7 @@ import { } from 'ui' import { useParams } from 'common' -import { SpendCapModal } from 'components/interfaces/BillingV2' +import SpendCapModal from 'components/interfaces/Billing/SpendCapModal' import InformationBox from 'components/ui/InformationBox' import Panel from 'components/ui/Panel' import { useOrganizationCreateMutation } from 'data/organizations/organization-create-mutation' diff --git a/studio/components/interfaces/Organization/NewProject/EmptyPaymentMethodWarning.tsx b/studio/components/interfaces/Organization/NewProject/EmptyPaymentMethodWarning.tsx index 7cc2b3e5243..931fa17584a 100644 --- a/studio/components/interfaces/Organization/NewProject/EmptyPaymentMethodWarning.tsx +++ b/studio/components/interfaces/Organization/NewProject/EmptyPaymentMethodWarning.tsx @@ -1,10 +1,10 @@ import { useState } from 'react' -import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2' import InformationBox from 'components/ui/InformationBox' import { useSelectedOrganization } from 'hooks' import { getURL } from 'lib/helpers' import { Button, IconAlertCircle } from 'ui' +import AddNewPaymentMethodModal from 'components/interfaces/Billing/Payment/AddNewPaymentMethodModal' const EmptyPaymentMethodWarning = ({ onPaymentMethodAdded, diff --git a/studio/components/interfaces/Organization/Usage/Usage.constants.tsx b/studio/components/interfaces/Organization/Usage/Usage.constants.tsx index 085424a9dbe..1f7b77f13dd 100644 --- a/studio/components/interfaces/Organization/Usage/Usage.constants.tsx +++ b/studio/components/interfaces/Organization/Usage/Usage.constants.tsx @@ -1,4 +1,4 @@ -import { USAGE_APPROACHING_THRESHOLD } from 'components/interfaces/BillingV2/Billing.constants' +import { USAGE_APPROACHING_THRESHOLD } from 'components/interfaces/Billing/Billing.constants' import { EgressType, PricingMetric } from 'data/analytics/org-daily-stats-query' import { OrgSubscription } from 'data/subscriptions/org-subscription-query' import { OrgUsageResponse } from 'data/usage/org-usage-query' diff --git a/studio/components/interfaces/Organization/Usage/Usage.utils.ts b/studio/components/interfaces/Organization/Usage/Usage.utils.ts index 8ec8d3d9f8e..574f9f844da 100644 --- a/studio/components/interfaces/Organization/Usage/Usage.utils.ts +++ b/studio/components/interfaces/Organization/Usage/Usage.utils.ts @@ -1,8 +1,5 @@ import { DataPoint } from 'data/analytics/constants' -import { ProjectUsageResponse } from 'data/usage/project-usage-query' -import { USAGE_APPROACHING_THRESHOLD } from 'components/interfaces/BillingV2/Billing.constants' -import { CategoryAttribute, USAGE_STATUS } from './Usage.constants' -import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query' +import { OrgSubscription } from 'data/subscriptions/org-subscription-query' // [Joshen] This is just for development to generate some test data for chart rendering export const generateUsageData = (attribute: string, days: number): DataPoint[] => { @@ -16,25 +13,7 @@ export const generateUsageData = (attribute: string, days: number): DataPoint[] }) } -export const getUsageStatus = (attributes: CategoryAttribute[], usage?: ProjectUsageResponse) => { - if (!usage) return USAGE_STATUS.NORMAL - - const attributeStatuses = attributes.map((attribute) => { - const usageMeta = usage?.[attribute.key as keyof ProjectUsageResponse] - const usageRatio = - typeof usageMeta !== 'number' ? (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0) : 0 - if (usageRatio >= 1) return USAGE_STATUS.EXCEEDED - else if (usageRatio >= USAGE_APPROACHING_THRESHOLD) return USAGE_STATUS.APPROACHING - else return USAGE_STATUS.NORMAL - }) - - if (attributeStatuses.find((x) => x === USAGE_STATUS.EXCEEDED)) return USAGE_STATUS.EXCEEDED - else if (attributeStatuses.find((x) => x === USAGE_STATUS.APPROACHING)) - return USAGE_STATUS.APPROACHING - else return USAGE_STATUS.NORMAL -} - -export const getUpgradeUrl = (slug: string, subscription?: ProjectSubscriptionResponse) => { +export const getUpgradeUrl = (slug: string, subscription?: OrgSubscription) => { if (!subscription) { return `/org/${slug}/billing` } diff --git a/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx b/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx index 394c9ab25de..31067597203 100644 --- a/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx +++ b/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx @@ -21,7 +21,6 @@ import { } from '../Usage.utils' import UsageBarChart from '../UsageBarChart' import { ChartMeta } from './UsageSection' -import { useIsFeatureEnabled } from 'hooks' export interface AttributeUsageProps { slug: string diff --git a/studio/components/interfaces/Organization/index.ts b/studio/components/interfaces/Organization/index.ts index c76c6de58b9..d98a1842db3 100644 --- a/studio/components/interfaces/Organization/index.ts +++ b/studio/components/interfaces/Organization/index.ts @@ -1,5 +1,4 @@ import BillingSettings from './BillingSettings/BillingSettings' -import BillingSettingsV2 from './BillingSettingsV2/BillingSettings' import GeneralSettings from './GeneralSettings/GeneralSettings' import IntegrationSettings from './IntegrationSettings/IntegrationSettings' import InvoicesSettings from './InvoicesSettings/InvoicesSettings' @@ -12,7 +11,6 @@ import Documents from './Documents/Documents' export { BillingSettings, - BillingSettingsV2, GeneralSettings, IntegrationSettings, InvoicesSettings, diff --git a/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx b/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx index 5cdb27d22d8..5c2496d76e1 100644 --- a/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx +++ b/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx @@ -3,12 +3,13 @@ import { useParams } from 'common' import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation' import { SqlSnippet } from 'data/content/sql-snippets-query' import { isError } from 'data/utils/error-check' -import { useFlag, useStore } from 'hooks' +import { useFlag, useSelectedOrganization, useStore } from 'hooks' import { useEffect, useState } from 'react' import { useSqlEditorStateSnapshot } from 'state/sql-editor' import { AiIconAnimation, Button, Form, Input, Modal } from 'ui' -import { subscriptionHasHipaaAddon } from '../BillingV2/Subscription/Subscription.utils' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' +import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' + export interface RenameQueryModalProps { snippet: SqlSnippet visible: boolean @@ -19,9 +20,10 @@ export interface RenameQueryModalProps { const RenameQueryModal = ({ snippet, visible, onCancel, onComplete }: RenameQueryModalProps) => { const { ui } = useStore() const { ref } = useParams() + const organization = useSelectedOrganization() const snap = useSqlEditorStateSnapshot() const supabaseAIEnabled = useFlag('sqlEditorSupabaseAI') - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef: ref }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) // Customers on HIPAA plans should not have access to Supabase AI const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) diff --git a/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 4d6e728e3fb..612816a975b 100644 --- a/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -31,7 +31,6 @@ import { SqlSnippet } from 'data/content/sql-snippets-query' import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' import { useFormatQueryMutation } from 'data/sql/format-sql-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { isError } from 'data/utils/error-check' import { useFlag, @@ -46,7 +45,7 @@ import { uuidv4 } from 'lib/helpers' import { useProfile } from 'lib/profile' import Telemetry from 'lib/telemetry' import { getSqlEditorStateSnapshot, useSqlEditorStateSnapshot } from 'state/sql-editor' -import { subscriptionHasHipaaAddon } from '../BillingV2/Subscription/Subscription.utils' +import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils' import AISchemaSuggestionPopover from './AISchemaSuggestionPopover' import AISettingsModal from './AISettingsModal' import { sqlAiDisclaimerComment, untitledSnippetTitle } from './SQLEditor.constants' @@ -64,6 +63,7 @@ import { getDiffTypeDropdownLabel, } from './SQLEditor.utils' import UtilityPanel from './UtilityPanel/UtilityPanel' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' // Load the monaco editor client-side only (does not behave well server-side) const MonacoEditor = dynamic(() => import('./MonacoEditor'), { ssr: false }) @@ -98,6 +98,7 @@ const SQLEditor = () => { const { profile } = useProfile() const project = useSelectedProject() + const organization = useSelectedOrganization() const snap = useSqlEditorStateSnapshot() const { mutate: formatQuery } = useFormatQueryMutation() @@ -113,7 +114,7 @@ const SQLEditor = () => { const inputRef = useRef(null) const supabaseAIEnabled = useFlag('sqlEditorSupabaseAI') - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef: project?.ref }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) // Customers on HIPAA plans should not have access to Supabase AI const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) diff --git a/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabResults.tsx b/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabResults.tsx index 0e54dac3d4c..aae6f0985e9 100644 --- a/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabResults.tsx +++ b/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabResults.tsx @@ -1,7 +1,6 @@ -import { subscriptionHasHipaaAddon } from 'components/interfaces/BillingV2/Subscription/Subscription.utils' +import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils' import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation' import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { isError } from 'data/utils/error-check' import { useFlag, @@ -17,6 +16,7 @@ import { AiIconAnimation, Button } from 'ui' import { useSqlEditor } from '../SQLEditor' import { sqlAiDisclaimerComment } from '../SQLEditor.constants' import Results from './Results' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' export type UtilityTabResultsProps = { id: string @@ -25,6 +25,7 @@ export type UtilityTabResultsProps = { const UtilityTabResults = ({ id, isExecuting }: UtilityTabResultsProps) => { const { ui } = useStore() + const organization = useSelectedOrganization() const snap = useSqlEditorStateSnapshot() const { mutateAsync: debugSql, isLoading: isDebugSqlLoading } = useSqlDebugMutation() const { setDebugSolution, setAiInput, setSqlDiff, sqlDiff } = useSqlEditor() @@ -50,7 +51,7 @@ const UtilityTabResults = ({ id, isExecuting }: UtilityTabResultsProps) => { const isUtilityPanelCollapsed = (snippet?.splitSizes?.[1] ?? 0) === 0 const supabaseAIEnabled = useFlag('sqlEditorSupabaseAI') - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef: selectedProject?.ref }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) // Customers on HIPAA plans should not have access to Supabase AI const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) diff --git a/studio/components/interfaces/Settings/Addons/Addons.tsx b/studio/components/interfaces/Settings/Addons/Addons.tsx index 9932c1cf331..33268e24f8a 100644 --- a/studio/components/interfaces/Settings/Addons/Addons.tsx +++ b/studio/components/interfaces/Settings/Addons/Addons.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react' import { useParams } from 'common' import { useTheme } from 'next-themes' -import { getAddons } from 'components/interfaces/BillingV2/Subscription/Subscription.utils' +import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils' import ProjectUpdateDisabledTooltip from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip' import { useIsProjectActive, @@ -36,8 +36,10 @@ import { IconChevronRight, IconExternalLink, } from 'ui' -import { ComputeInstanceSidePanel, CustomDomainSidePanel, PITRSidePanel } from './' import Image from 'next/image' +import ComputeInstanceSidePanel from './ComputeInstanceSidePanel' +import PITRSidePanel from './PITRSidePanel' +import CustomDomainSidePanel from './CustomDomainSidePanel' const Addons = () => { const { resolvedTheme } = useTheme() diff --git a/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx b/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx index c0fdcd033ff..79d4c53ae04 100644 --- a/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx +++ b/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx @@ -12,7 +12,6 @@ import { setProjectStatus } from 'data/projects/projects-query' import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon-remove-mutation' import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils' import { BASE_PATH, PROJECT_STATUS } from 'lib/constants' @@ -32,7 +31,7 @@ import { } from 'ui' import * as Tooltip from '@radix-ui/react-tooltip' -import Image from 'next/image' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' const COMPUTE_CATEGORY_OPTIONS: { id: 'micro' | 'optimized' @@ -62,7 +61,6 @@ const ComputeInstanceSidePanel = () => { const { resolvedTheme } = useTheme() const { project: selectedProject } = useProjectContext() const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id const [showConfirmationModal, setShowConfirmationModal] = useState(false) const [selectedCategory, setSelectedCategory] = useState<'micro' | 'optimized'>('micro') @@ -78,7 +76,7 @@ const ComputeInstanceSidePanel = () => { const onClose = () => snap.setPanelKey(undefined) const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef }) - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const { mutate: updateAddon, isLoading: isUpdating } = useProjectAddonUpdateMutation({ onSuccess: () => { ui.setNotification({ @@ -276,17 +274,11 @@ const ComputeInstanceSidePanel = () => { variant="info" title="Changing your compute size is only available on the Pro plan" actions={ - isOrgBilling ? ( - - ) : ( - - ) + + } > Upgrade your plan to change the compute size of your project @@ -379,12 +371,7 @@ const ComputeInstanceSidePanel = () => { )} {hasChanges && - (selectedCategory === 'micro' && !isOrgBilling ? ( -

- Upon clicking confirm, the amount of that's unused during the current billing - cycle will be returned as credits that can be used for subsequent billing cycles -

- ) : selectedCategory !== 'micro' && selectedCompute?.price_interval === 'monthly' ? ( + (selectedCategory !== 'micro' && selectedCompute?.price_interval === 'monthly' ? ( // Monthly payment with project-level subscription

Upon clicking confirm, the amount of{' '} diff --git a/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx b/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx index 399950807bd..287776db6c3 100644 --- a/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx +++ b/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx @@ -8,7 +8,6 @@ import { useParams } from 'common' import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon-remove-mutation' import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import Telemetry from 'lib/telemetry' import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' @@ -22,13 +21,13 @@ import { Radio, SidePanel, } from 'ui' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' const CustomDomainSidePanel = () => { const { ui } = useStore() const router = useRouter() const { ref: projectRef } = useParams() const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id const [selectedOption, setSelectedOption] = useState('cd_none') @@ -42,7 +41,7 @@ const CustomDomainSidePanel = () => { const onClose = () => snap.setPanelKey(undefined) const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef }) - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const { mutate: updateAddon, isLoading: isUpdating } = useProjectAddonUpdateMutation({ onSuccess: () => { ui.setNotification({ @@ -273,17 +272,11 @@ const CustomDomainSidePanel = () => { variant="info" title="Custom domains are unavailable on the free plan" actions={ - isOrgBilling ? ( - - ) : ( - - ) + + } > Upgrade your plan to add a custom domain to your project diff --git a/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx b/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx index 1fc90f27e39..87d53f17845 100644 --- a/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx +++ b/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx @@ -9,13 +9,13 @@ import { useTheme } from 'next-themes' import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon-remove-mutation' import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { BASE_PATH } from 'lib/constants' import Telemetry from 'lib/telemetry' import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' import { Alert, Button, IconExternalLink, Radio, SidePanel } from 'ui' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' const PITR_CATEGORY_OPTIONS: { id: 'off' | 'on' @@ -43,7 +43,6 @@ const PITRSidePanel = () => { const { ref: projectRef } = useParams() const { resolvedTheme } = useTheme() const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id const [selectedCategory, setSelectedCategory] = useState<'on' | 'off'>('off') const [selectedOption, setSelectedOption] = useState('pitr_0') @@ -55,7 +54,7 @@ const PITRSidePanel = () => { const onClose = () => snap.setPanelKey(undefined) const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef }) - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const { mutate: updateAddon, isLoading: isUpdating } = useProjectAddonUpdateMutation({ onSuccess: () => { ui.setNotification({ @@ -235,17 +234,11 @@ const PITRSidePanel = () => { className="mb-4" title="Changing your Point-In-Time-Recovery is only available on the Pro plan" actions={ - isOrgBilling ? ( - - ) : ( - - ) + + } > Upgrade your plan to change PITR for your project diff --git a/studio/components/interfaces/Settings/Addons/index.ts b/studio/components/interfaces/Settings/Addons/index.ts deleted file mode 100644 index cc080f030a2..00000000000 --- a/studio/components/interfaces/Settings/Addons/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import ComputeInstanceSidePanel from './ComputeInstanceSidePanel' -import CustomDomainSidePanel from './CustomDomainSidePanel' -import PITRSidePanel from './PITRSidePanel' - -import Addons from './Addons' - -export { ComputeInstanceSidePanel, CustomDomainSidePanel, PITRSidePanel } -export default Addons diff --git a/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx b/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx index e7ff73d886c..655751e9132 100644 --- a/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx +++ b/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx @@ -16,26 +16,26 @@ import { import Panel from 'components/ui/Panel' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useProjectSettingsQuery } from 'data/config/project-settings-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' import { useSelectedOrganization } from 'hooks' import { pluckObjectFields } from 'lib/helpers' import Telemetry from 'lib/telemetry' import ConfirmDisableReadOnlyModeModal from './ConfirmDisableReadOnlyModal' import ResetDbPassword from './ResetDbPassword' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' const DatabaseSettings = () => { const router = useRouter() const { ref: projectRef } = useParams() const telemetryProps = useTelemetryProps() + const organization = useSelectedOrganization() const selectedOrganization = useSelectedOrganization() const [showConfirmationModal, setShowConfirmationModal] = useState(false) const { data, isLoading, isError } = useProjectSettingsQuery({ projectRef }) - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const { data: resourceWarnings } = useResourceWarningsQuery() - const isOrgBilling = !!selectedOrganization?.subscription_id const isReadOnlyMode = (resourceWarnings ?? [])?.find((warning) => warning.project === projectRef) ?.is_readonly_mode_enabled ?? false @@ -179,11 +179,7 @@ const DatabaseSettings = () => { {subscription?.plan.id === 'free' ? (

  • Upgrade to the Pro plan {' '} @@ -192,11 +188,7 @@ const DatabaseSettings = () => { ) : subscription?.plan.id === 'pro' && subscription?.usage_billing_enabled ? (
  • Disable your Spend Cap {' '} diff --git a/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx b/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx index 55dafef487e..f6ccac95a20 100644 --- a/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx +++ b/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx @@ -11,9 +11,9 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import { FormHeader } from 'components/ui/Forms' import Panel from 'components/ui/Panel' import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mutation' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useProjectUsageQuery } from 'data/usage/project-usage-query' -import { useCheckPermissions, useStore } from 'hooks' +import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' export interface DiskSizeConfigurationProps { disabled?: boolean @@ -22,6 +22,7 @@ export interface DiskSizeConfigurationProps { const DiskSizeConfiguration = ({ disabled = false }: DiskSizeConfigurationProps) => { const { ui } = useStore() const { project } = useProjectContext() + const organization = useSelectedOrganization() const { ref: projectRef } = useParams() const { lastDatabaseResizeAt } = project ?? {} @@ -43,7 +44,7 @@ const DiskSizeConfiguration = ({ disabled = false }: DiskSizeConfigurationProps) }) const { data: projectUsage } = useProjectUsageQuery({ projectRef }) - const { data: projectSubscriptionData } = useProjectSubscriptionV2Query({ projectRef }) + const { data: projectSubscriptionData } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const { mutate: updateProjectUsage, isLoading: isUpdatingDiskSize } = useProjectDiskResizeMutation({ onSuccess: (res, variables) => { diff --git a/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx index a9bf4b06747..69f7cfad7f4 100644 --- a/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx +++ b/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx @@ -6,18 +6,20 @@ import Panel from 'components/ui/Panel' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useProjectApiQuery } from 'data/config/project-api-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { IconAlertCircle } from 'ui' import CustomDomainActivate from './CustomDomainActivate' import CustomDomainDelete from './CustomDomainDelete' import CustomDomainsConfigureHostname from './CustomDomainsConfigureHostname' import CustomDomainsShimmerLoader from './CustomDomainsShimmerLoader' import CustomDomainVerify from './CustomDomainVerify' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSelectedOrganization } from 'hooks' const CustomDomainConfig = () => { const { ref } = useParams() + const organization = useSelectedOrganization() - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef: ref }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const plan = subscription?.plan?.id const { isLoading: isSettingsLoading, data: settings } = useProjectApiQuery({ @@ -64,6 +66,7 @@ const CustomDomainConfig = () => { icon={} primaryText="Custom domains are a Pro plan add-on" projectRef={ref as string} + organizationSlug={organization!.slug} secondaryText={ plan === 'free' ? 'To configure a custom domain for your project, please upgrade to the Pro plan with the custom domains add-on selected' diff --git a/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx b/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx index b992452981f..243af0b0fb5 100644 --- a/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx +++ b/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx @@ -4,13 +4,13 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { Button, Input } from 'ui' -import { CANCELLATION_REASONS } from 'components/interfaces/BillingV2/Billing.constants' +import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import TextConfirmModal from 'components/ui/Modals/TextConfirmModal' import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send' import { useProjectDeleteMutation } from 'data/projects/project-delete-mutation' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useCheckPermissions, useStore } from 'hooks' +import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' export interface DeleteProjectButtonProps { type?: 'danger' | 'default' @@ -20,9 +20,10 @@ const DeleteProjectButton = ({ type = 'danger' }: DeleteProjectButtonProps) => { const router = useRouter() const { ui } = useStore() const { project } = useProjectContext() + const organization = useSelectedOrganization() const projectRef = project?.ref - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const projectPlan = subscription?.plan?.id ?? 'free' const isFree = projectPlan === 'free' diff --git a/studio/components/interfaces/Settings/General/General.tsx b/studio/components/interfaces/Settings/General/General.tsx index dbd16707600..0dd3647e4e2 100644 --- a/studio/components/interfaces/Settings/General/General.tsx +++ b/studio/components/interfaces/Settings/General/General.tsx @@ -35,7 +35,6 @@ const General = () => { const parentProject = useProjectByRef(project?.parent_project_ref) const isBranch = parentProject !== undefined - const isOrgBilling = !!organization?.subscription_id const formId = 'project-general-settings' const initialValues = { name: project?.name ?? '', ref: project?.ref ?? '' } const canUpdateProject = useCheckPermissions(PermissionAction.UPDATE, 'projects', { @@ -119,7 +118,7 @@ const General = () => { }} )} - {!isBranch && isOrgBilling && ( + {!isBranch && ( <>
    @@ -165,7 +164,7 @@ const General = () => {
    diff --git a/studio/components/interfaces/Settings/General/Infrastructure/Infrastructure.tsx b/studio/components/interfaces/Settings/General/Infrastructure/Infrastructure.tsx index 8732ba8eeff..44d4ca6de9c 100644 --- a/studio/components/interfaces/Settings/General/Infrastructure/Infrastructure.tsx +++ b/studio/components/interfaces/Settings/General/Infrastructure/Infrastructure.tsx @@ -13,18 +13,20 @@ import { FormSectionLabel, } from 'components/ui/Forms' import { useProjectUpgradeEligibilityQuery } from 'data/config/project-upgrade-eligibility-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useFlag } from 'hooks' +import { useFlag, useSelectedOrganization } from 'hooks' import PauseProjectButton from './PauseProjectButton' import ProjectUpgradeAlert from './ProjectUpgradeAlert' import RestartServerButton from './RestartServerButton' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' interface InfrastructureProps {} const Infrastructure = ({}: InfrastructureProps) => { const { ref } = useParams() const { project } = useProjectContext() - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef: ref }) + const organization = useSelectedOrganization() + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) + const isFreeProject = subscription?.plan?.id === 'free' const { diff --git a/studio/components/interfaces/Settings/Infrastructure/Infrastructure.constants.ts b/studio/components/interfaces/Settings/Infrastructure/Infrastructure.constants.ts index c351a9e25de..db60a819379 100644 --- a/studio/components/interfaces/Settings/Infrastructure/Infrastructure.constants.ts +++ b/studio/components/interfaces/Settings/Infrastructure/Infrastructure.constants.ts @@ -1,8 +1,3 @@ -// [Joshen TODO] Please clean up - a lot of repeated code from other files here -// all of these interfaces, see what can be reused - -// [Joshen TODO] Remove any dependency on BillingV2 stuff so that we can deprecate easily - export interface Attribute { key: string name?: string diff --git a/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx b/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx index d8c4fa01f6b..13b5249182a 100644 --- a/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx +++ b/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx @@ -4,12 +4,12 @@ import Link from 'next/link' import { Fragment, useMemo, useState } from 'react' import { IconBarChart2, IconExternalLink } from 'ui' -import { getAddons } from 'components/interfaces/BillingV2/Subscription/Subscription.utils' +import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils' import { CPUWarnings, DiskIOBandwidthWarnings, RAMWarnings, -} from 'components/interfaces/BillingV2/Usage/UsageWarningAlerts' +} from 'components/interfaces/Billing/Usage/UsageWarningAlerts' import UsageBarChart from 'components/interfaces/Organization/Usage/UsageBarChart' import { ScaffoldContainer, @@ -33,7 +33,6 @@ import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants' const InfrastructureActivity = () => { const { ref: projectRef } = useParams() const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id const [dateRange, setDateRange] = useState() const { data: subscription, isLoading: isLoadingSubscription } = useOrgSubscriptionQuery({ @@ -170,8 +169,6 @@ const InfrastructureActivity = () => { }, } - if (!isOrgBilling) return null - return ( <> diff --git a/studio/components/interfaces/Settings/Logs/Logs.utils.ts b/studio/components/interfaces/Settings/Logs/Logs.utils.ts index 22c8558faec..95be9f8610b 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.utils.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.utils.ts @@ -6,7 +6,7 @@ import logConstants from 'shared-data/logConstants' import BackwardIterator from 'components/ui/CodeEditor/Providers/BackwardIterator' import uniqBy from 'lodash/uniqBy' import { useEffect } from 'react' -import { PlanId } from 'data/subscriptions/project-subscription-v2-query' +import { PlanId } from 'data/subscriptions/org-subscription-query' /** * Convert a micro timestamp from number/string to iso timestamp diff --git a/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index 3dec21423a0..c176ea46cbf 100644 --- a/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -18,11 +18,12 @@ import { import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel' import LoadingOpacity from 'components/ui/LoadingOpacity' import ShimmerLine from 'components/ui/ShimmerLine' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import useLogsPreview from 'hooks/analytics/useLogsPreview' import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt' import { LOGS_TABLES } from './Logs.constants' import UpgradePrompt from './UpgradePrompt' +import { useSelectedOrganization } from 'hooks' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' /** * Acts as a container component for the entire log display @@ -52,7 +53,8 @@ export const LogsPreviewer = ({ const router = useRouter() const { s, ite, its } = useParams() const [showChart, setShowChart] = useState(true) - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const organization = useSelectedOrganization() + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const table = !tableName ? LOGS_TABLES[queryType] : tableName diff --git a/studio/components/interfaces/Support/SupportForm.tsx b/studio/components/interfaces/Support/SupportForm.tsx index 3199917838c..d80aa255198 100644 --- a/studio/components/interfaces/Support/SupportForm.tsx +++ b/studio/components/interfaces/Support/SupportForm.tsx @@ -26,7 +26,6 @@ import { getProjectAuthConfig } from 'data/auth/auth-config-query' import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProjectsQuery } from 'data/projects/projects-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useFlag, useStore } from 'hooks' import useLatest from 'hooks/misc/useLatest' import { detectBrowser } from 'lib/helpers' @@ -35,6 +34,7 @@ import { Project } from 'types' import DisabledStateForFreeTier from './DisabledStateForFreeTier' import { CATEGORY_OPTIONS, SERVICE_OPTIONS, SEVERITY_OPTIONS } from './Support.constants' import { formatMessage, uploadAttachments } from './SupportForm.utils' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' const MAX_ATTACHMENTS = 5 const INCLUDE_DISCUSSIONS = ['Problem', 'Database_unresponsive'] @@ -110,10 +110,9 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => { })?.slug : organizations?.[0]?.slug - const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionV2Query( - { projectRef: selectedProjectRef }, - { enabled: selectedProjectRef !== 'no-project' } - ) + const { data: subscription, isLoading: isLoadingSubscription } = useOrgSubscriptionQuery({ + orgSlug: selectedOrganizationSlug, + }) useEffect(() => { if (!uploadedFiles) return diff --git a/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx b/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx index 0585648dec9..b35f761c6e9 100644 --- a/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx +++ b/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx @@ -1,19 +1,8 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useParams } from 'common' -import Link from 'next/link' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' -import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - Button, - Form_Shadcn_, - IconAlertCircle, - IconFileText, - IconGitBranch, - Modal, -} from 'ui' +import { Button, Form_Shadcn_, IconFileText, IconGitBranch, Modal } from 'ui' import * as z from 'zod' import SidePanelGitHubRepoLinker from 'components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoLinker' @@ -36,7 +25,6 @@ const EnableBranchingModal = () => { // but calling form.formState.isValid somehow removes the onBlur check, // and makes the validation run onChange instead. This is a workaround const [isValid, setIsValid] = useState(false) - const isOrgBilling = !!selectedOrg?.subscription_id const { data: integrations, @@ -136,124 +124,83 @@ const EnableBranchingModal = () => {
    - {!isOrgBilling ? ( + {isLoadingIntegrations && ( <> - - - - - Organization-based billing migration required - - -

    - Your organization will first need to be migrated to use our new - organization-based billing before you can enable Branching. -

    -

    - You may do so under the billing settings of your organization. -

    -
    -
    -
    - -
    - - -
    -
    - - ) : ( - <> - {isLoadingIntegrations && ( - <> - - - - - - - )} - - {isErrorIntegrations && ( - <> - - - - - - - )} - - {isSuccessIntegrations && ( - - )} - - -

    Please keep in mind the following:

    -
    -
    -
    - -
    -
    -
    -

    - You will not be able to use the dashboard to make changes to the database -

    -

    - Schema changes for database preview branches must be done via Git. We are - nonetheless working on allowing the dashboard to make schema changes for - preview branches. -

    -
    -
    -
    - - - -
    - - -
    + + + )} + + {isErrorIntegrations && ( + <> + + + + + + + )} + + {isSuccessIntegrations && ( + + )} + + +

    Please keep in mind the following:

    +
    +
    +
    + +
    +
    +
    +

    + You will not be able to use the dashboard to make changes to the database +

    +

    + Schema changes for database preview branches must be done via Git. We are + nonetheless working on allowing the dashboard to make schema changes for preview + branches. +

    +
    +
    +
    + + + + +
    + + +
    +
    diff --git a/studio/components/layouts/AppLayout/OrganizationDropdown.tsx b/studio/components/layouts/AppLayout/OrganizationDropdown.tsx index cfb146006dc..ded658b071d 100644 --- a/studio/components/layouts/AppLayout/OrganizationDropdown.tsx +++ b/studio/components/layouts/AppLayout/OrganizationDropdown.tsx @@ -37,11 +37,7 @@ const OrganizationDropdown = ({ isNewNav = false }: OrganizationDropdownProps) = const slug = selectedOrganization?.slug const orgName = selectedOrganization?.name - const isOrgBilling = !!selectedOrganization?.subscription_id - const { data: subscription, isSuccess } = useOrgSubscriptionQuery( - { orgSlug: slug }, - { enabled: isOrgBilling } - ) + const { data: subscription, isSuccess } = useOrgSubscriptionQuery({ orgSlug: slug }) const [open, setOpen] = useState(false) diff --git a/studio/components/layouts/AppLayout/ProjectDropdown.tsx b/studio/components/layouts/AppLayout/ProjectDropdown.tsx index c72011468c6..64cff6204a5 100644 --- a/studio/components/layouts/AppLayout/ProjectDropdown.tsx +++ b/studio/components/layouts/AppLayout/ProjectDropdown.tsx @@ -6,12 +6,10 @@ import { useState } from 'react' import { useParams } from 'common' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useProjectsQuery } from 'data/projects/projects-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useIsFeatureEnabled, useSelectedOrganization, useSelectedProject } from 'hooks' import { IS_PLATFORM } from 'lib/constants' import { Organization, Project } from 'types' import { - Badge, Button, CommandEmpty_Shadcn_, CommandGroup_Shadcn_, @@ -53,7 +51,6 @@ export const sanitizeRoute = (route: string, routerQueries: ParsedUrlQuery) => { const ProjectLink = ({ project, - organization, setOpen, }: { project: Project @@ -63,19 +60,9 @@ const ProjectLink = ({ const router = useRouter() const { ref } = useParams() const sanitizedRoute = sanitizeRoute(router.route, router.query) - const isOrgBilling = !!organization?.subscription_id // [Joshen] Temp while we're interim between v1 and v2 billing let href = sanitizedRoute?.replace('[ref]', project.ref) ?? `/project/${project.ref}` - if (href.endsWith('settings/addons') && !isOrgBilling) { - href = href.replace('settings/addons', 'settings/billing/subscription') - } else if (href.endsWith('settings/billing/subscription') && isOrgBilling) { - href = href.replace('settings/billing/subscription', 'settings/addons') - } else if (href.endsWith('settings/infrastructure') && !isOrgBilling) { - href = href.replace('settings/infrastructure', 'settings/billing/usage') - } else if (href.endsWith('settings/billing/usage') && !isOrgBilling) { - href = href.replace('settings/billing/usage', 'settings/infrastructure') - } return ( { const projectCreationEnabled = useIsFeatureEnabled('projects:create') const isBranch = projectDetails?.parentRef !== projectDetails?.ref - const isOrgBilling = !!selectedOrganization?.subscription_id - const { data: subscription, isSuccess } = useProjectSubscriptionV2Query( - { projectRef: ref }, - { enabled: !isOrgBilling } - ) + const projects = isNewNav ? allProjects ?.filter((x) => x.organization_id === selectedOrganization?.id) @@ -143,7 +126,6 @@ const ProjectDropdown = ({ isNewNav = false }: ProjectDropdownProps) => { >

    {selectedProject?.name}

    - {isSuccess && !isOrgBilling && {subscription?.plan.name}}
    @@ -155,12 +137,7 @@ const ProjectDropdown = ({ isNewNav = false }: ProjectDropdownProps) => { 7 ? 'h-[210px]' : ''}> {projects?.map((project) => ( - + ))} diff --git a/studio/components/layouts/OrganizationLayout.tsx b/studio/components/layouts/OrganizationLayout.tsx index 4ca49888fd8..a4231320f16 100644 --- a/studio/components/layouts/OrganizationLayout.tsx +++ b/studio/components/layouts/OrganizationLayout.tsx @@ -13,7 +13,6 @@ const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => { const router = useRouter() const { slug } = useParams() const id = router.asPath.split('/').at(-1)?.split('?')[0]?.split('#')[0] - const isOrgBilling = !!selectedOrganization?.subscription_id const invoicesEnabled = useIsFeatureEnabled('billing:invoices') @@ -52,13 +51,12 @@ const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => { )} - {isOrgBilling && } + {invoicesEnabled && } {showOAuthApps && } {showAuditLogs && } - {isOrgBilling && ( - - )} + + diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index ee4c4a30221..c37b8bf5a83 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -6,13 +6,8 @@ import BranchDropdown from 'components/layouts/AppLayout/BranchDropdown' import EnableBranchingButton from 'components/layouts/AppLayout/EnableBranchingButton/EnableBranchingButton' import OrganizationDropdown from 'components/layouts/AppLayout/OrganizationDropdown' import ProjectDropdown from 'components/layouts/AppLayout/ProjectDropdown' -import { - getResourcesExceededLimits, - getResourcesExceededLimitsOrg, -} from 'components/ui/OveragesBanner/OveragesBanner.utils' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' +import { getResourcesExceededLimitsOrg } from 'components/ui/OveragesBanner/OveragesBanner.utils' import { useOrgUsageQuery } from 'data/usage/org-usage-query' -import { useProjectUsageQuery } from 'data/usage/project-usage-query' import { useFlag, useSelectedOrganization, useSelectedProject } from 'hooks' import { IS_PLATFORM } from 'lib/constants' import { Badge } from 'ui' @@ -20,6 +15,7 @@ import BreadcrumbsView from './BreadcrumbsView' import FeedbackDropdown from './FeedbackDropdown' import HelpPopover from './HelpPopover' import NotificationsPopover from './NotificationsPopover' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => { const { ref: projectRef } = useParams() @@ -31,40 +27,17 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = const isBranchingEnabled = selectedProject?.is_branch_enabled === true || selectedProject?.parent_project_ref !== undefined - // Skip with org-based-billing, as quota is for the entire org - const { data: projectUsage } = useProjectUsageQuery( - { projectRef }, - { enabled: Boolean(selectedOrganization && !selectedOrganization.subscription_id) } - ) - const { data: orgUsage } = useOrgUsageQuery( - { orgSlug: selectedOrganization?.slug }, - { enabled: Boolean(selectedOrganization && selectedOrganization.subscription_id) } - ) + const { data: orgUsage } = useOrgUsageQuery({ orgSlug: selectedOrganization?.slug }) const exceedingLimits = useMemo(() => { if (orgUsage) { return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0 - } else if (projectUsage) { - return getResourcesExceededLimits(projectUsage).length > 0 } else { return false } - }, [projectUsage, orgUsage]) + }, [orgUsage]) - // Skip with org-based-billing, as quota is for the entire org - const { data: projectSubscription } = useProjectSubscriptionV2Query( - { projectRef }, - { enabled: selectedOrganization && !selectedOrganization.subscription_id } - ) - - const { data: orgSubscription } = useProjectSubscriptionV2Query( - { projectRef }, - { enabled: Boolean(selectedOrganization && selectedOrganization.subscription_id) } - ) - - const subscription = useMemo(() => { - return projectSubscription || orgSubscription - }, [projectSubscription, orgSubscription]) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrganization?.slug }) const projectHasNoLimits = subscription?.usage_billing_enabled === true diff --git a/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx b/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx index e4cce1e00e6..dfaf94f1f66 100644 --- a/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx @@ -10,10 +10,10 @@ import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { useProjectRestoreMutation } from 'data/projects/project-restore-mutation' import { setProjectStatus } from 'data/projects/projects-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { PROJECT_STATUS } from 'lib/constants' import { useProjectContext } from './ProjectContext' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' export interface ProjectPausedStateProps { product?: string @@ -26,7 +26,8 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { const selectedOrganization = useSelectedOrganization() const { project } = useProjectContext() const orgSlug = selectedOrganization?.slug - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef: ref }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug }) + const isFreePlan = subscription?.plan?.id === 'free' const billedViaOrg = Boolean(selectedOrganization?.subscription_id) diff --git a/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts b/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts index 81fde950615..83fa44e7b73 100644 --- a/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts +++ b/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts @@ -16,222 +16,118 @@ export const generateSettingsMenu = ( const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP const buildingUrl = `/project/${ref}/building` - const isOrgBilling = !!organization?.subscription_id - const authEnabled = features?.auth ?? true const edgeFunctionsEnabled = features?.edgeFunctions ?? true const storageEnabled = features?.storage ?? true - const invoicesEnabled = features?.invoices ?? true - if (isOrgBilling) { - return [ - { - title: 'Project Settings', - items: [ + return [ + { + title: 'Project Settings', + items: [ + { + name: 'General', + key: 'general', + url: `/project/${ref}/settings/general`, + items: [], + }, + { + name: 'Infrastructure', + key: 'infrastructure', + url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/infrastructure`, + items: [], + }, + ...(IS_PLATFORM + ? [ + { + name: 'Integrations', + key: 'integrations', + url: `/project/${ref}/settings/integrations`, + items: [], + }, + ] + : []), + ...[ { - name: 'General', - key: 'general', - url: `/project/${ref}/settings/general`, - items: [], - }, - { - name: 'Infrastructure', - key: 'infrastructure', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/infrastructure`, - items: [], - }, - ...(IS_PLATFORM - ? [ - { - name: 'Integrations', - key: 'integrations', - url: `/project/${ref}/settings/integrations`, - items: [], - }, - ] - : []), - ...[ - { - name: 'Add Ons', - key: 'addons', - url: `/project/${ref}/settings/addons`, - items: [], - }, - ], - { - name: 'Vault', - key: 'vault', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/vault/secrets`, - items: [], - label: 'BETA', - }, - ], - }, - { - title: '', - items: [ - { - name: 'Database', - key: 'database', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/database`, - items: [], - }, - { - name: 'API', - key: 'api', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/api`, - items: [], - }, - ...(IS_PLATFORM && authEnabled - ? [ - { - name: 'Authentication', - key: 'auth', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/auth`, - items: [], - }, - ] - : []), - ...(IS_PLATFORM && storageEnabled - ? [ - { - name: 'Storage', - key: 'storage', - url: `/project/${ref}/settings/storage`, - items: [], - }, - ] - : []), - ...(IS_PLATFORM && edgeFunctionsEnabled - ? [ - { - name: 'Edge Functions', - key: 'functions', - url: `/project/${ref}/settings/functions`, - items: [], - }, - ] - : []), - ], - }, - - { - title: 'Billing', - items: [ - { - name: 'Subscription', - key: 'subscription', - url: `/org/${organization?.slug}/billing`, - items: [], - }, - - { - name: 'Usage', - key: 'usage', - url: `/org/${organization?.slug}/usage?projectRef=${ref}`, + name: 'Add Ons', + key: 'addons', + url: `/project/${ref}/settings/addons`, items: [], }, ], - }, - ] - } else { - return [ - { - title: 'Project Settings', - items: [ - { - name: 'General', - key: 'general', - url: `/project/${ref}/settings/general`, - items: [], - }, - { - name: 'Database', - key: 'database', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/database`, - items: [], - }, - { - name: 'API', - key: 'api', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/api`, - items: [], - }, - ...(IS_PLATFORM - ? [ - { - name: 'Integrations', - key: 'integrations', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/integrations`, - items: [], - }, - ] - : []), - ...(IS_PLATFORM && authEnabled - ? [ - { - name: 'Auth', - key: 'auth', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/auth`, - items: [], - }, - ] - : []), - ...(IS_PLATFORM && storageEnabled - ? [ - { - name: 'Storage', - key: 'storage', - url: `/project/${ref}/settings/storage`, - items: [], - }, - ] - : []), - { - name: 'Vault', - key: 'vault', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/vault/secrets`, - items: [], - label: 'BETA', - }, - ], - }, - ...(IS_PLATFORM - ? [ - { - title: 'Billing', - items: [ - { - name: 'Subscription', - key: 'subscription', - url: isProjectBuilding - ? buildingUrl - : `/project/${ref}/settings/billing/subscription`, - items: [], - }, + { + name: 'Vault', + key: 'vault', + url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/vault/secrets`, + items: [], + label: 'BETA', + }, + ], + }, + { + title: '', + items: [ + { + name: 'Database', + key: 'database', + url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/database`, + items: [], + }, + { + name: 'API', + key: 'api', + url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/api`, + items: [], + }, + ...(IS_PLATFORM && authEnabled + ? [ + { + name: 'Authentication', + key: 'auth', + url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/auth`, + items: [], + }, + ] + : []), + ...(IS_PLATFORM && storageEnabled + ? [ + { + name: 'Storage', + key: 'storage', + url: `/project/${ref}/settings/storage`, + items: [], + }, + ] + : []), + ...(IS_PLATFORM && edgeFunctionsEnabled + ? [ + { + name: 'Edge Functions', + key: 'functions', + url: `/project/${ref}/settings/functions`, + items: [], + }, + ] + : []), + ], + }, - { - name: 'Usage', - key: 'usage', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/billing/usage`, - items: [], - }, - ...(invoicesEnabled - ? [ - { - name: 'Invoices', - key: 'invoices', - url: isProjectBuilding - ? buildingUrl - : `/project/${ref}/settings/billing/invoices`, - items: [], - }, - ] - : []), - ], - }, - ] - : []), - ] - } + { + title: 'Billing', + items: [ + { + name: 'Subscription', + key: 'subscription', + url: `/org/${organization?.slug}/billing`, + items: [], + }, + + { + name: 'Usage', + key: 'usage', + url: `/org/${organization?.slug}/usage?projectRef=${ref}`, + items: [], + }, + ], + }, + ] } diff --git a/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx b/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx index 447a84bdfab..ae554ac45af 100644 --- a/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx +++ b/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx @@ -6,7 +6,6 @@ import { IconChevronRight, } from 'ui' -import { useSelectedOrganization } from 'hooks' import { Project } from 'types' import SettingsMenuItem from './SettingsMenuItem' @@ -16,22 +15,13 @@ interface ProjectSettingsMenuItemProps { const ProjectSettingsMenuItem = ({ project }: ProjectSettingsMenuItemProps) => { const router = useRouter() - const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id // [Joshen] Links need to be updated once we start implementing these - const projectSettings = isOrgBilling - ? [ - { label: 'General', pathname: `/project/[ref]/settings/general` }, - { label: 'Infrastructure', pathname: `/project/[ref]/settings/infrastructure` }, - { label: 'Add Ons', pathname: `/project/[ref]/settings/addons` }, - ] - : [ - { label: 'General', pathname: `/project/[ref]/settings/general` }, - { label: 'Subscription', pathname: `/project/[ref]/settings/billing/subscription` }, - { label: 'Usage', pathname: `/project/[ref]/settings/billing/usage` }, - { label: 'Invoices', pathname: `/project/[ref]/settings/billing/invoices` }, - ] + const projectSettings = [ + { label: 'General', pathname: `/project/[ref]/settings/general` }, + { label: 'Infrastructure', pathname: `/project/[ref]/settings/infrastructure` }, + { label: 'Add Ons', pathname: `/project/[ref]/settings/addons` }, + ] return ( diff --git a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx index 1ace3909581..bf803f8c5c0 100644 --- a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx +++ b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx @@ -13,9 +13,10 @@ import { convertFromBytes, convertToBytes } from './StorageSettings.utils' export type StorageSettingsProps = { projectRef: string | undefined + organizationSlug: string | undefined } -const StorageSettings = ({ projectRef }: StorageSettingsProps) => { +const StorageSettings = ({ projectRef, organizationSlug }: StorageSettingsProps) => { const { data, error } = useProjectStorageConfigQuery({ projectRef }, { enabled: IS_PLATFORM }) if (error || data?.error) { @@ -34,10 +35,10 @@ const StorageSettings = ({ projectRef }: StorageSettingsProps) => { ) } - return + return } -const StorageConfig = ({ config, projectRef }: any) => { +const StorageConfig = ({ config, projectRef, organizationSlug }: any) => { const { fileSizeLimit, isFreeTier } = config const { value, unit } = convertFromBytes(fileSizeLimit) @@ -177,6 +178,7 @@ const StorageConfig = ({ config, projectRef }: any) => {
    } + organizationSlug={organizationSlug} primaryText="Free Plan has a fixed upload file size limit of 50 MB." projectRef={projectRef} secondaryText="Upgrade to the Pro plan for a configurable upload file size limit of up to 5 GB." diff --git a/studio/components/ui/OveragesBanner/OveragesBanner.tsx b/studio/components/ui/OveragesBanner/OveragesBanner.tsx deleted file mode 100644 index 4e8ddd990d8..00000000000 --- a/studio/components/ui/OveragesBanner/OveragesBanner.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useParams } from 'common' -import Link from 'next/link' -import { Alert, Button } from 'ui' - -import { useProjectUsageQuery } from 'data/usage/project-usage-query' -import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' -import { getResourcesApproachingLimits, getResourcesExceededLimits } from './OveragesBanner.utils' - -interface OveragesBannerProps { - tier: string - minimal?: boolean -} - -// Banner will not be shown for PAYG or Enterprise projects - -const OveragesBanner = ({ tier, minimal }: OveragesBannerProps) => { - const { ref: projectRef } = useParams() - const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef }) - - const resourcesApproachingLimits = getResourcesApproachingLimits(usage) - const isApproachingUsageLimits = resourcesApproachingLimits.length > 0 - - const resourcesExceededLimits = getResourcesExceededLimits(usage) - const isOverUsageLimits = resourcesExceededLimits.length > 0 - - if ( - isLoading || - error || - (!isApproachingUsageLimits && !isOverUsageLimits) || - tier === PRICING_TIER_PRODUCT_IDS.PAYG || - tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE || - tier === PRICING_TIER_PRODUCT_IDS.TEAM - ) { - return <> - } - - const tierName = - tier === PRICING_TIER_PRODUCT_IDS.FREE - ? 'free' - : tier === PRICING_TIER_PRODUCT_IDS.PRO - ? 'pro' - : tier === PRICING_TIER_PRODUCT_IDS.PAYG - ? 'pro' - : '' - - const minimalDescription = - tier === PRICING_TIER_PRODUCT_IDS.FREE - ? "You can check your project's usage details or upgrade to the pro plan to support the growth of your project." - : tier === PRICING_TIER_PRODUCT_IDS.PRO - ? "You can check your project's usage details or consider disabling spend cap to support the growth of your project." - : tier === PRICING_TIER_PRODUCT_IDS.PAYG - ? "As you have disabled spend cap, additional resources will be charged on a per-usage basis. You can check your project's usage details for more information." - : '' - - const description = - tier === PRICING_TIER_PRODUCT_IDS.FREE - ? 'Upgrade to the pro plan to support the growth of your project.' - : tier === PRICING_TIER_PRODUCT_IDS.PRO - ? 'Consider disabling spend cap to support the growth of your project' - : tier === PRICING_TIER_PRODUCT_IDS.PAYG - ? "As you have disabled spend cap, additional resources will be charged on a per-usage basis. You can check your project's usage details for more information." - : '' - - return ( -
    - - -
    - ) : ( - <> - ) - } - > - {minimal ? minimalDescription : description} - -
    - ) -} - -export default OveragesBanner diff --git a/studio/components/ui/OveragesBanner/OveragesBanner.utils.ts b/studio/components/ui/OveragesBanner/OveragesBanner.utils.ts index a16eab8ce41..b5b65aa5f65 100644 --- a/studio/components/ui/OveragesBanner/OveragesBanner.utils.ts +++ b/studio/components/ui/OveragesBanner/OveragesBanner.utils.ts @@ -1,32 +1,4 @@ -import { compact } from 'lodash' -import { USAGE_APPROACHING_THRESHOLD } from 'lib/constants' -import { OrgUsageResponse, UsageMetric } from 'data/usage/org-usage-query' - -export const getResourcesApproachingLimits = (usages: any) => { - if (!usages) return [] - - return compact( - Object.keys(usages) - .filter((resourceName) => usages[resourceName] !== null) - .map((resourceName) => { - const resource = usages[resourceName] - if (resource.usage / resource.limit >= USAGE_APPROACHING_THRESHOLD) return resourceName - }) - ) -} - -export const getResourcesExceededLimits = (usages: any) => { - if (!usages) return [] - - return compact( - Object.keys(usages) - .filter((resourceName) => usages[resourceName] !== null) - .map((resourceName) => { - const resource = usages[resourceName] - if (resource.limit > 0 && resource.usage / resource.limit > 1) return resourceName - }) - ) -} +import { UsageMetric } from 'data/usage/org-usage-query' export const getResourcesExceededLimitsOrg = (usageMetrics: UsageMetric[]): string[] => { if (!usageMetrics.length) return [] diff --git a/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx b/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx index 49da0f799d7..784a3e3b2e6 100644 --- a/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx +++ b/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx @@ -24,8 +24,6 @@ const ResourceExhaustionWarningBanner = () => { (warning) => warning.project === ref ) - const isOrgBilling = !!organization?.subscription_id - // [Joshen] Read only takes higher precedence over multiple resource warnings const activeWarnings = projectResourceWarnings !== undefined @@ -87,7 +85,7 @@ const ResourceExhaustionWarningBanner = () => { : `/project/[ref]/settings/[infra-path]#${metric}` ) ?.replace('[ref]', ref ?? 'default') - ?.replace('[infra-path]', isOrgBilling ? 'infrastructure' : 'billing/usage') + ?.replace('[infra-path]', 'infrastructure') const buttonText = activeWarnings.length > 1 diff --git a/studio/components/ui/UpgradeToPro.tsx b/studio/components/ui/UpgradeToPro.tsx index fad765cbfbe..a20a8fd6544 100644 --- a/studio/components/ui/UpgradeToPro.tsx +++ b/studio/components/ui/UpgradeToPro.tsx @@ -4,13 +4,14 @@ import Link from 'next/link' import { ReactNode } from 'react' import { Button } from 'ui' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useCheckPermissions, useFlag, useIsFeatureEnabled } from 'hooks' +import { useCheckPermissions, useFlag } from 'hooks' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' interface UpgradeToProProps { icon?: ReactNode primaryText: string projectRef: string + organizationSlug: string secondaryText: string addon?: 'pitr' | 'customDomain' | 'computeInstance' } @@ -19,10 +20,11 @@ const UpgradeToPro = ({ icon, primaryText, projectRef, + organizationSlug, secondaryText, addon, }: UpgradeToProProps) => { - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organizationSlug }) const plan = subscription?.plan?.id const canUpdateSubscription = useCheckPermissions( @@ -52,9 +54,11 @@ const UpgradeToPro = ({ -
    -

    - You can rename your organization later -

    - -
    - - } - > - -

    This is your organization within Supabase.

    -

    - For example, you can use the name of your company or department. -

    -
    - - - - - - {Object.entries(ORG_KIND_TYPES).map(([k, v]) => { - return ( - - {v} - - ) - })} - - - {orgKind == 'COMPANY' ? ( - - - {Object.entries(ORG_SIZE_TYPES).map(([k, v]) => { - return ( - - {v} - - ) - })} - - - ) : ( - <> - )} - - ) -} - -Wizard.getLayout = (page) => ( - - {page} - -) - -export default observer(Wizard) diff --git a/studio/pages/org/[slug]/billing.tsx b/studio/pages/org/[slug]/billing.tsx index dfba45b9890..bb7bdbcd790 100644 --- a/studio/pages/org/[slug]/billing.tsx +++ b/studio/pages/org/[slug]/billing.tsx @@ -1,5 +1,5 @@ import { useParams } from 'common' -import { BillingSettings, BillingSettingsV2 } from 'components/interfaces/Organization' +import { BillingSettings } from 'components/interfaces/Organization' import { OrganizationLayout } from 'components/layouts' import Loading from 'components/ui/Loading' import { usePermissionsQuery } from 'data/permissions/permissions-query' @@ -16,7 +16,6 @@ const OrgBillingSettings: NextPageWithLayout = () => { const snap = useOrgSettingsPageStateSnapshot() const { isLoading: isLoadingPermissions } = usePermissionsQuery() const selectedOrganization = useSelectedOrganization() - const isOrgBilling = !!selectedOrganization?.subscription_id useEffect(() => { const allowedValues = ['subscriptionPlan', 'costControl'] @@ -32,15 +31,7 @@ const OrgBillingSettings: NextPageWithLayout = () => { {selectedOrganization === undefined && isLoadingPermissions ? ( ) : ( - <> - {isOrgBilling ? ( - - ) : ( -
    - -
    - )} - + )} ) diff --git a/studio/pages/org/[slug]/index.tsx b/studio/pages/org/[slug]/index.tsx index 05c4f47b852..c303957422b 100644 --- a/studio/pages/org/[slug]/index.tsx +++ b/studio/pages/org/[slug]/index.tsx @@ -1,7 +1,7 @@ import Link from 'next/link' import { Button, IconPlus } from 'ui' -import NoProjectsOnPaidOrgInfo from 'components/interfaces/BillingV2/NoProjectsOnPaidOrgInfo' +import NoProjectsOnPaidOrgInfo from 'components/interfaces/Billing/NoProjectsOnPaidOrgInfo' import ProjectCard from 'components/interfaces/Home/ProjectList/ProjectCard' import ShimmeringCard from 'components/interfaces/Home/ProjectList/ShimmeringCard' import AppLayout from 'components/layouts/AppLayout/AppLayout' diff --git a/studio/pages/project/[ref]/database/backups/pitr.tsx b/studio/pages/project/[ref]/database/backups/pitr.tsx index 945824f1672..9f00a075a14 100644 --- a/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -12,9 +12,9 @@ import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useBackupsQuery } from 'data/database/backups-query' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useCheckPermissions } from 'hooks' +import { useCheckPermissions, useSelectedOrganization } from 'hooks' import { NextPageWithLayout } from 'types' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' const DatabasePhysicalBackups: NextPageWithLayout = () => { const router = useRouter() @@ -56,6 +56,7 @@ DatabasePhysicalBackups.getLayout = (page) => ( const PITR = observer(() => { const { project } = useProjectContext() + const organization = useSelectedOrganization() const { data: backups, error, @@ -66,10 +67,7 @@ const PITR = observer(() => { projectRef: project?.ref, }) - const { data: subscription } = useProjectSubscriptionV2Query( - { projectRef: project?.ref }, - { enabled: project?.ref !== undefined } - ) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const ref = project?.ref ?? 'default' const plan = subscription?.plan?.id @@ -92,6 +90,7 @@ const PITR = observer(() => { ) : ( { const project = useSelectedProject() diff --git a/studio/pages/project/[ref]/logs/explorer/index.tsx b/studio/pages/project/[ref]/logs/explorer/index.tsx index 8a24e4d4dcd..ce6a24559b2 100644 --- a/studio/pages/project/[ref]/logs/explorer/index.tsx +++ b/studio/pages/project/[ref]/logs/explorer/index.tsx @@ -24,12 +24,12 @@ import LoadingOpacity from 'components/ui/LoadingOpacity' import LogsExplorerHeader from 'components/ui/Logs/LogsExplorerHeader' import ShimmerLine from 'components/ui/ShimmerLine' import { useContentInsertMutation } from 'data/content/content-insert-mutation' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useLocalStorage, useStore } from 'hooks' +import { useLocalStorage, useSelectedOrganization, useStore } from 'hooks' import useLogsQuery from 'hooks/analytics/useLogsQuery' import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt' import { uuidv4 } from 'lib/helpers' import { LogSqlSnippets, NextPageWithLayout } from 'types' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' const PLACEHOLDER_QUERY = 'select\n cast(timestamp as datetime) as timestamp,\n event_message, metadata \nfrom edge_logs \nlimit 5' @@ -38,11 +38,12 @@ export const LogsExplorerPage: NextPageWithLayout = () => { const { ui } = useStore() const router = useRouter() const { ref: projectRef, q, ite, its } = useParams() + const organization = useSelectedOrganization() const [editorId, setEditorId] = useState(uuidv4()) const [editorValue, setEditorValue] = useState(PLACEHOLDER_QUERY) const [saveModalOpen, setSaveModalOpen] = useState(false) const [warnings, setWarnings] = useState([]) - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const { params, logData, error, isLoading, changeQuery, runQuery, setParams } = useLogsQuery( projectRef as string, { diff --git a/studio/pages/project/[ref]/reports/api-overview.tsx b/studio/pages/project/[ref]/reports/api-overview.tsx index a01b01a4288..66cc691e317 100644 --- a/studio/pages/project/[ref]/reports/api-overview.tsx +++ b/studio/pages/project/[ref]/reports/api-overview.tsx @@ -23,13 +23,14 @@ import { useParams } from 'common' import { isEqual } from 'lodash' import ShimmerLine from 'components/ui/ShimmerLine' import ReportPadding from 'components/interfaces/Reports/ReportPadding' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSelectedOrganization } from 'hooks' export const ApiReport: NextPageWithLayout = () => { - const { ref: projectRef } = useParams() + const organization = useSelectedOrganization() const report = useApiReport() - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const plan = subscription?.plan const handleDatepickerChange = ({ from, to }: DatePickerToFrom) => { diff --git a/studio/pages/project/[ref]/reports/storage.tsx b/studio/pages/project/[ref]/reports/storage.tsx index e4e68a31d96..fc77bdd08d1 100644 --- a/studio/pages/project/[ref]/reports/storage.tsx +++ b/studio/pages/project/[ref]/reports/storage.tsx @@ -17,14 +17,16 @@ import { DatePickerToFrom, LogsEndpointParams } from 'components/interfaces/Sett import { useParams } from 'common' import ShimmerLine from 'components/ui/ShimmerLine' import ReportPadding from 'components/interfaces/Reports/ReportPadding' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import DatePickers from 'components/interfaces/Settings/Logs/Logs.DatePickers' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSelectedOrganization } from 'hooks' export const StorageReport: NextPageWithLayout = () => { const { ref: projectRef } = useParams() + const organization = useSelectedOrganization() const report = useStorageReport() - const { data: subscription } = useProjectSubscriptionV2Query({ projectRef }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const plan = subscription?.plan const handleDatepickerChange = ({ from, to }: DatePickerToFrom) => { diff --git a/studio/pages/project/[ref]/settings/addons.tsx b/studio/pages/project/[ref]/settings/addons.tsx index 430e7d4ae77..b6c18133f2d 100644 --- a/studio/pages/project/[ref]/settings/addons.tsx +++ b/studio/pages/project/[ref]/settings/addons.tsx @@ -1,34 +1,8 @@ -import { useParams } from 'common' -import Addons from 'components/interfaces/Settings/Addons' +import Addons from 'components/interfaces/Settings/Addons/Addons' import { SettingsLayout } from 'components/layouts' -import { useSelectedOrganization } from 'hooks' -import Link from 'next/link' import { NextPageWithLayout } from 'types' -import { Alert } from 'ui' const ProjectAddons: NextPageWithLayout = () => { - const { ref } = useParams() - const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id - - if (!isOrgBilling) { - return ( -
    - - You might be looking for the{' '} - - project's subscription - {' '} - page instead. - -
    - ) - } - return } diff --git a/studio/pages/project/[ref]/settings/billing/invoices.tsx b/studio/pages/project/[ref]/settings/billing/invoices.tsx deleted file mode 100644 index 094e2bcd671..00000000000 --- a/studio/pages/project/[ref]/settings/billing/invoices.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import Link from 'next/link' - -import { Invoices } from 'components/interfaces/BillingV2' -import { SettingsLayout } from 'components/layouts' -import InformationBox from 'components/ui/InformationBox' -import { useSelectedOrganization } from 'hooks' -import { useRouter } from 'next/router' -import { useEffect } from 'react' -import { NextPageWithLayout } from 'types' -import { Button, IconExternalLink, IconInfo } from 'ui' - -const ProjectBilling: NextPageWithLayout = () => { - const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id - const router = useRouter() - - useEffect(() => { - if (isOrgBilling) { - router.push(`/org/${organization.slug}/invoices`) - } - }, [router, organization?.slug, isOrgBilling]) - - if (isOrgBilling) return null - - return ( -
    -
    - -
    -
    - ) -} - -ProjectBilling.getLayout = (page) => ( - {page} -) - -export default ProjectBilling - -const Settings = () => { - const selectedOrganization = useSelectedOrganization() - const orgSlug = selectedOrganization?.slug ?? '_' - - return ( -
    -
    -
    - } - defaultVisibility={true} - hideCollapse - title="We're upgrading our billing system" - description={ -
    -

    - This organization uses the legacy project-based billing. We’ve recently made some - big improvements to our billing system. To migrate to the new organization-based - billing, head over to your{' '} - - organization billing settings - - . -

    - -
    - - -
    -
    - } - /> -
    - -

    Invoices

    - -
    - To manage your billing address, emails or Tax ID, head to your{' '} - - organization settings - - . -
    - - -
    -
    - ) -} diff --git a/studio/pages/project/[ref]/settings/billing/subscription.tsx b/studio/pages/project/[ref]/settings/billing/subscription.tsx deleted file mode 100644 index b1c2a7a5f2c..00000000000 --- a/studio/pages/project/[ref]/settings/billing/subscription.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import SubscriptionV2 from 'components/interfaces/BillingV2/Subscription/Subscription' -import { SettingsLayout } from 'components/layouts' -import { useSelectedOrganization } from 'hooks' -import { NextPageWithLayout } from 'types' -import { useRouter } from 'next/router' -import { useEffect } from 'react' - -const ProjectBilling: NextPageWithLayout = () => { - const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id - const router = useRouter() - - useEffect(() => { - if (isOrgBilling) { - const { ref, panel } = router.query - let redirectUri = `/org/${organization.slug}/billing` - switch (panel) { - case 'subscriptionPlan': - redirectUri = `/org/${organization.slug}/billing?panel=subscriptionPlan` - break - case 'costControl': - redirectUri = `/org/${organization.slug}/billing?panel=costControl` - break - case 'computeInstance': - redirectUri = `/project/${ref}/settings/addons?panel=computeInstance` - break - case 'pitr': - redirectUri = `/project/${ref}/settings/addons?panel=pitr` - break - case 'customDomain': - redirectUri = `/project/${ref}/settings/addons?panel=customDomain` - break - } - - router.push(redirectUri) - } - }, [router, organization?.slug, isOrgBilling]) - - // No need to bother rendering, we'll redirect anyway - if (isOrgBilling) { - return null - } - - return -} - -ProjectBilling.getLayout = (page) => ( - {page} -) - -export default ProjectBilling diff --git a/studio/pages/project/[ref]/settings/billing/usage.tsx b/studio/pages/project/[ref]/settings/billing/usage.tsx deleted file mode 100644 index 89302b719cc..00000000000 --- a/studio/pages/project/[ref]/settings/billing/usage.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import Usage from 'components/interfaces/BillingV2/Usage/Usage' -import { SettingsLayout } from 'components/layouts' -import { useSelectedOrganization } from 'hooks' -import { NextPageWithLayout } from 'types' -import { useParams } from 'common' -import { useEffect } from 'react' -import { useRouter } from 'next/router' - -const ProjectBillingUsage: NextPageWithLayout = () => { - const { ref } = useParams() - const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id - const router = useRouter() - const hash = router.asPath.split('#')[1] - - useEffect(() => { - if (ref && isOrgBilling && organization) { - let redirectUri = `/org/${organization.slug}/usage?projectRef=${ref}` - - if (['cpu', 'ram', 'disk_io'].includes(hash)) { - redirectUri = `/project/${ref}/settings/infrastructure#${hash}` - } else if (hash && hash.length > 0) { - redirectUri = redirectUri + `#${hash}` - } - - router.push(redirectUri) - } - }, [organization, isOrgBilling, ref, hash]) - - if (isOrgBilling) { - return null - } - - return -} - -ProjectBillingUsage.getLayout = (page) => ( - {page} -) - -export default ProjectBillingUsage diff --git a/studio/pages/project/[ref]/settings/general.tsx b/studio/pages/project/[ref]/settings/general.tsx index c759cc3bf1d..f95a7f0d272 100644 --- a/studio/pages/project/[ref]/settings/general.tsx +++ b/studio/pages/project/[ref]/settings/general.tsx @@ -2,19 +2,16 @@ import { CustomDomainConfig, DeleteProjectPanel, General, - Infrastructure, TransferProjectPanel, } from 'components/interfaces/Settings/General' import { SettingsLayout } from 'components/layouts' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { useFlag, useIsFeatureEnabled, useSelectedOrganization } from 'hooks' +import { useFlag, useIsFeatureEnabled } from 'hooks' import { NextPageWithLayout } from 'types' const ProjectSettings: NextPageWithLayout = () => { const { project } = useProjectContext() - const organization = useSelectedOrganization() const isBranch = !!project?.parent_project_ref - const isOrgBilling = !!organization?.subscription_id const transferProjectEnabledFlag = useFlag('transferProject') const { projectsTransfer: projectTransferEnabled } = useIsFeatureEnabled(['projects:transfer']) @@ -24,7 +21,6 @@ const ProjectSettings: NextPageWithLayout = () => { {!isBranch ? ( <> - {!isOrgBilling && } {projectTransferEnabled && transferProjectEnabledFlag && } diff --git a/studio/pages/project/[ref]/settings/infrastructure.tsx b/studio/pages/project/[ref]/settings/infrastructure.tsx index 3551c187d6e..a5456c9de88 100644 --- a/studio/pages/project/[ref]/settings/infrastructure.tsx +++ b/studio/pages/project/[ref]/settings/infrastructure.tsx @@ -1,36 +1,10 @@ -import { useParams } from 'common' import InfrastructureActivity from 'components/interfaces/Settings/Infrastructure/InfrastructureActivity' import InfrastructureInfo from 'components/interfaces/Settings/Infrastructure/InfrastructureInfo' import { SettingsLayout } from 'components/layouts' import { ScaffoldDivider } from 'components/layouts/Scaffold' -import { useSelectedOrganization } from 'hooks' -import Link from 'next/link' import { NextPageWithLayout } from 'types' -import { Alert } from 'ui' const ProjectInfrastructure: NextPageWithLayout = () => { - const { ref } = useParams() - const organization = useSelectedOrganization() - const isOrgBilling = !!organization?.subscription_id - - if (!isOrgBilling) { - return ( -
    - - You might be looking for the{' '} - - project's usage - {' '} - page instead. - -
    - ) - } - return ( <> diff --git a/studio/pages/project/[ref]/settings/storage.tsx b/studio/pages/project/[ref]/settings/storage.tsx index 0622e4f543c..cbfdd992d1d 100644 --- a/studio/pages/project/[ref]/settings/storage.tsx +++ b/studio/pages/project/[ref]/settings/storage.tsx @@ -2,14 +2,16 @@ import { useParams } from 'common' import { SettingsLayout } from 'components/layouts' import { StorageSettings } from 'components/to-be-cleaned/Storage' +import { useSelectedOrganization } from 'hooks' import { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { const { ref: projectRef } = useParams() + const organization = useSelectedOrganization() return (
    - +
    ) } diff --git a/studio/styles/users-table.scss b/studio/styles/users-table.scss deleted file mode 100644 index 8b137891791..00000000000 --- a/studio/styles/users-table.scss +++ /dev/null @@ -1 +0,0 @@ - diff --git a/studio/tests/components/Home/ProjectUsage.test.js b/studio/tests/components/Home/ProjectUsage.test.js deleted file mode 100644 index 881919391c6..00000000000 --- a/studio/tests/components/Home/ProjectUsage.test.js +++ /dev/null @@ -1,109 +0,0 @@ -import { waitFor, screen } from '@testing-library/react' -import { clickDropdown, render } from '../../helpers' -import userEvent from '@testing-library/user-event' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { get } from 'lib/common/fetch' -import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted' - -// TODO: abstract out to global setup -const ProjectUsage = jest.fn() -ProjectUsage.mockImplementation((props) => { - const Original = jest.requireActual('components/interfaces/Home/ProjectUsage').default - - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - logger: { - log: console.log, - warn: console.warn, - // ✅ no more errors on the console for tests - error: process.env.NODE_ENV === 'test' ? () => {} : console.error, - }, - }) - - // wrap with QueryClient to reset the cache each time - return ( - - - - ) -}) - -jest.mock('data/subscriptions/project-subscription-v2-query') -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' -import { useIsFeatureEnabled } from 'hooks' - -beforeEach(() => { - useIsFeatureEnabled.mockReset() - useIsFeatureEnabled.mockReturnValue({ - projectAuthAll: true, - projectStorageAll: true, - }) -}) - -useProjectSubscriptionV2Query.mockReturnValue({ - data: undefined, -}) - -beforeEach(() => { - get.mockReset() -}) - -const MOCK_CHART_DATA = { - result: [ - { - total_auth_requests: 123, - total_realtime_requests: 223, - total_storage_requests: 323, - total_rest_requests: 333, - timestamp: new Date().toISOString(), - }, - ], -} - -// This hook is globally mocked, so you don't need to call `jest.mock` first - but the tests -// for this component need this mock data returned -useFillTimeseriesSorted.mockReturnValue(MOCK_CHART_DATA.result) - -test('mounts correctly', async () => { - get.mockImplementation((url) => { - if (decodeURIComponent(url).includes('usage.api-counts')) { - return MOCK_CHART_DATA - } - return {} - }) - render() - await waitFor(() => screen.getByText(/Statistics for past 24 hours/)) - await waitFor(() => screen.getByText(/123/)) - await waitFor(() => screen.getByText(/223/)) - await waitFor(() => screen.getByText(/323/)) - await waitFor(() => screen.getByText(/333/)) -}) - -test('dropdown options changes chart query', async () => { - get.mockImplementation((url) => { - if (decodeURIComponent(url).includes('usage.api-counts')) { - return MOCK_CHART_DATA - } - return {} - }) - render() - await waitFor(() => screen.getByText(/Statistics for past 24 hours/)) - await waitFor(() => screen.getAllByRole('button', { name: '24 hours' })) - await waitFor(() => { - expect(get).toHaveBeenCalledWith(expect.stringContaining('interval=hourly'), expect.anything()) - }) - // find button that has radix id - const [btn] = screen.getAllByRole('button', { name: '24 hours' }).filter((e) => e.id) - clickDropdown(btn) - await waitFor(() => screen.getByText(/7 days/)) - await waitFor(() => screen.getByText(/60 minutes/)) - - // simulate changing of dropdown - userEvent.click(screen.getByText(/60 minutes/)) - await waitFor(() => screen.getByText(/Statistics for past 60 minutes/)) - expect(get).toHaveBeenCalledWith(expect.stringContaining('interval=minutely'), expect.anything()) -}) diff --git a/studio/tests/pages/projects/LogsPreviewer.test.js b/studio/tests/pages/projects/LogsPreviewer.test.js index 2aed28883f3..c4936cc82ce 100644 --- a/studio/tests/pages/projects/LogsPreviewer.test.js +++ b/studio/tests/pages/projects/LogsPreviewer.test.js @@ -16,7 +16,7 @@ const defaultRouterMock = () => { useRouter.mockReturnValue(defaultRouterMock()) import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { fireEvent, waitFor, screen, act } from '@testing-library/react' import { render } from '../../helpers' import userEvent from '@testing-library/user-event' @@ -498,7 +498,7 @@ test('filters accept filterOverride', async () => { describe.each(['free', 'pro', 'team', 'enterprise'])('upgrade modal for %s', (key) => { beforeEach(() => { - useProjectSubscriptionV2Query.mockReturnValue({ + useOrgSubscriptionQuery.mockReturnValue({ data: { plan: { id: key, @@ -522,7 +522,7 @@ describe.each(['free', 'pro', 'team', 'enterprise'])('upgrade modal for %s', (ke }) test('datepicker onChange will set the query params for outbound api request', async () => { - useProjectSubscriptionV2Query.mockReturnValue({ + useOrgSubscriptionQuery.mockReturnValue({ data: { plan: { id: 'enterprise', diff --git a/studio/tests/pages/projects/billing/Infrastructure.test.tsx b/studio/tests/pages/projects/billing/Infrastructure.test.tsx deleted file mode 100644 index f28fdb3522d..00000000000 --- a/studio/tests/pages/projects/billing/Infrastructure.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { get } from 'lib/common/fetch' -import { render } from '../../../helpers' -import { screen } from '@testing-library/react' -import Infrastructure from 'components/interfaces/BillingV2/Usage/Infrastructure' -beforeEach(() => { - // reset mocks between tests - ;(get as jest.Mock).mockReset() - ;(get as jest.Mock).mockImplementation(async (_url: string) => { - return [{ result: [] }] - }) -}) - -test(`renders category attributes static elements`, async () => { - render( - - ) - // renders usage categories info - await screen.findByText('Max CPU usage of your server') -}) diff --git a/studio/tests/pages/projects/logs-query.test.js b/studio/tests/pages/projects/logs-query.test.js index ef81e455226..97f2c764c5f 100644 --- a/studio/tests/pages/projects/logs-query.test.js +++ b/studio/tests/pages/projects/logs-query.test.js @@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event' import { logDataFixture } from '../../fixtures' import { clickDropdown } from 'tests/helpers' import dayjs from 'dayjs' -import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' jest.mock('common/hooks') import { useParams } from 'common/hooks' @@ -222,7 +222,7 @@ test('field reference', async () => { describe.each(['free', 'pro', 'team', 'enterprise'])('upgrade modal for %s', (key) => { beforeEach(() => { - useProjectSubscriptionV2Query.mockReturnValue({ + useOrgSubscriptionQuery.mockReturnValue({ data: { plan: { id: key,