From aaaaeecdddf47e270c58f3bc203ad8d816b41882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Thu, 13 Jul 2023 13:45:59 +0200 Subject: [PATCH] feat: payment method selection w/ org-billing-migration --- .../Subscription/PaymentMethodSelection.tsx | 14 +- .../MigrateOrganizationBillingButton.tsx | 334 +++++++++--------- .../organization-migrate-billing-mutation.ts | 5 +- 3 files changed, 184 insertions(+), 169 deletions(-) diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx index c1e5151a91d..2f654f39ffa 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx +++ b/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx @@ -14,11 +14,13 @@ import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2' export interface PaymentMethodSelectionProps { selectedPaymentMethod?: string onSelectPaymentMethod: (id: string) => void + layout?: 'vertical' | 'horizontal' } const PaymentMethodSelection = ({ selectedPaymentMethod, onSelectPaymentMethod, + layout = 'vertical', }: PaymentMethodSelectionProps) => { const { ui } = useStore() const { ref: projectRef } = useParams() @@ -70,8 +72,7 @@ const PaymentMethodSelection = ({ return ( <> -
-

Select payment method

+
{isLoading ? (
@@ -91,6 +92,7 @@ const PaymentMethodSelection = ({ disabled={!canUpdatePaymentMethods} icon={} onClick={() => setShowAddNewPaymentMethodModal(true)} + htmlType='button' > Add new @@ -116,7 +118,13 @@ const PaymentMethodSelection = ({
) : ( - + {paymentMethods.map((method: any) => { const label = `•••• •••• •••• ${method.card.last4}` return ( diff --git a/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx b/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx index a258fbff558..43d4b25d44e 100644 --- a/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx +++ b/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx @@ -10,6 +10,7 @@ import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { PRICING_TIER_LABELS_ORG } from 'lib/constants' import Link from 'next/link' import { useRouter } from 'next/router' +import PaymentMethodSelection from '../BillingSettingsV2/Subscription/PaymentMethodSelection' const MigrateOrganizationBillingButton = observer(() => { const { ui } = useStore() @@ -21,6 +22,8 @@ const MigrateOrganizationBillingButton = observer(() => { const [tier, setTier] = useState('') const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false) const [isSpendCapEnabled, setIsSpendCapEnabled] = useState(true) + const [paymentMethodId, setPaymentMethodId] = useState('') + const [isSubmitting, setSubmitting] = useState(false) const dbTier = useMemo(() => { if (tier === '') return '' @@ -67,15 +70,9 @@ const MigrateOrganizationBillingButton = observer(() => { setIsOpen(!isOpen) } - const onValidate = (values: any) => { - const errors: any = {} - if (!values.tier) { - errors.tier = 'Please select a plan.' - } - return errors - } + const onConfirmMigrate = async () => { + if (!tier) return - const onConfirmMigrate = async (values: any, { setSubmitting }: any) => { if (!canMigrateOrganization) { return ui.setNotification({ category: 'error', @@ -86,7 +83,7 @@ const MigrateOrganizationBillingButton = observer(() => { setSubmitting(true) try { - await migrateBilling({ organizationSlug: organization?.slug, tier: dbTier }) + await migrateBilling({ organizationSlug: organization?.slug, tier: dbTier, paymentMethodId }) ui.setNotification({ message: 'Successfully migrated to organization-level billing', category: 'success', @@ -119,167 +116,174 @@ const MigrateOrganizationBillingButton = observer(() => {
} > -
- {({ isSubmitting }: { isSubmitting: boolean }) => ( -
- -
- -
-

- Migrating to new organization-level billing combines subscriptions for all - projects in the organization into a single subscription. This cannot be - reversed.{' '} -

+
+ +
+ +
+

+ Migrating to new organization-level billing combines subscriptions for all + projects in the organization into a single subscription. This cannot be + reversed.{' '} +

-

- For a detailed breakdown of changes, see{' '} - - - Billing Migration Docs - - - . To transfer projects to a different organization, visit{' '} - - - General settings - - - . -

-
-
+

+ For a detailed breakdown of changes, see{' '} + + + Billing Migration Docs + + + . To transfer projects to a different organization, visit{' '} + + + General 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. Please contact support if you are an Enterprise customer. +

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

+ Paid plans come with one compute instance included. Additional projects will at + least cost the compute instance hours used (min $7/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. To scale beyond Pro limits without restrictions, disable + the spend cap and pay for over-usage beyond the quota. +

+
+ + setShowSpendCapHelperModal(false)} + />
- - - - 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. Please contact support if you are an Enterprise customer. -

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

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

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

- Spend Cap{' '} - setShowSpendCapHelperModal(true)} - /> -

-

- When enabled, usage is limited to the plan's quota, with restrictions when - limits are exceeded.{' '} -

-

- To scale beyond Pro limits without restrictions, disable the spend cap and - pay for over-usage beyond the quota. -

-
- -
- - Enable Spend Cap -
- } - checked={isSpendCapEnabled} - onChange={() => setIsSpendCapEnabled(!isSpendCapEnabled)} - /> -
- - setShowSpendCapHelperModal(false)} - /> -
- )} - - - - {migrationPreviewError && ( - - {migrationPreviewError.message} - - )} - - - {migrationError && ( - - {migrationError.message} - - )} - - - - -
+ )} - + + {tier && tier !== 'FREE' && ( + <> + + setPaymentMethodId(pm)} + /> + + + + )} + + + + {migrationPreviewError && ( + + {migrationPreviewError.message} + + )} + + + {migrationError && ( + + {migrationError.message} + + )} + + + + +
) diff --git a/studio/data/organizations/organization-migrate-billing-mutation.ts b/studio/data/organizations/organization-migrate-billing-mutation.ts index b9a98271737..565aa053ba3 100644 --- a/studio/data/organizations/organization-migrate-billing-mutation.ts +++ b/studio/data/organizations/organization-migrate-billing-mutation.ts @@ -6,6 +6,7 @@ import { organizationKeys } from './keys' export type OrganizationBillingMigrationVariables = { organizationSlug?: string tier?: string + paymentMethodId?: string } type OrganizationBillingMigrationError = { @@ -15,12 +16,14 @@ type OrganizationBillingMigrationError = { export async function migrateBilling({ organizationSlug, tier, + paymentMethodId, }: OrganizationBillingMigrationVariables) { if (!organizationSlug) throw new Error('organizationSlug is required') if (!tier) throw new Error('tier is required') - const payload: { tier: string } = { + const payload: { tier: string; payment_method_id?: string } = { tier, + payment_method_id: paymentMethodId, } const response = await post(