diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentConfirmation.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentConfirmation.tsx index 5b568e36437fa..dc32d28afbd7b 100644 --- a/apps/studio/components/interfaces/Billing/Payment/PaymentConfirmation.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/PaymentConfirmation.tsx @@ -7,11 +7,9 @@ export const PaymentConfirmation = ({ paymentIntentSecret, onPaymentIntentConfirm, onLoadingChange, - paymentMethodId, onError, }: { paymentIntentSecret: string - paymentMethodId: string onPaymentIntentConfirm: (response: PaymentIntentResult) => void onLoadingChange: (loading: boolean) => void onError?: (error: Error) => void @@ -22,7 +20,7 @@ export const PaymentConfirmation = ({ if (stripe && paymentIntentSecret) { onLoadingChange(true) stripe! - .confirmCardPayment(paymentIntentSecret, { payment_method: paymentMethodId }) + .confirmCardPayment(paymentIntentSecret) .then((res) => { onPaymentIntentConfirm(res) onLoadingChange(false) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx index a2e71206e62ac..b33f047760d0e 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx @@ -325,7 +325,6 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => { paymentIntentConfirmed(paymentIntentConfirmation) } onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)} - paymentMethodId={form.getValues().paymentMethod} /> )} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx index 99307541b1100..2a10e1bf6ae22 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx @@ -1,12 +1,10 @@ -import HCaptcha from '@hcaptcha/react-hcaptcha' import { includes, without } from 'lodash' -import { useReducer, useRef, useState } from 'react' +import { useReducer, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send' import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation' -import type { OrgSubscription } from 'data/subscriptions/types' import { useFlag } from 'hooks/ui/useFlag' import { Alert, Button, Input, Modal } from 'ui' import type { ProjectInfo } from '../../../../../data/projects/projects-query' @@ -22,17 +20,14 @@ export interface ExitSurveyModalProps { // [Joshen] For context - Exit survey is only when going to Free Plan from a paid plan const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) => { const { slug } = useParams() - const captchaRef = useRef(null) const [message, setMessage] = useState('') - const [captchaToken, setCaptchaToken] = useState(null) const [selectedReasons, dispatchSelectedReasons] = useReducer(reducer, []) const subscriptionUpdateDisabled = useFlag('disableProjectCreationAndUpdate') const { mutate: updateOrgSubscription, isLoading: isUpdating } = useOrgSubscriptionUpdateMutation( { onError: (error) => { - resetCaptcha() toast.error(`Failed to downgrade project: ${error.message}`) }, } @@ -55,23 +50,12 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) = } } - const resetCaptcha = () => { - setCaptchaToken(null) - captchaRef.current?.resetCaptcha() - } - const onSubmit = async () => { if (selectedReasons.length === 0) { return toast.error('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 downgradeOrganization() - } + await downgradeOrganization() } const downgradeOrganization = async () => { @@ -83,7 +67,6 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) = { slug, tier: 'tier_free' }, { onSuccess: async () => { - resetCaptcha() try { await sendExitSurvey({ orgSlug: slug, @@ -110,26 +93,6 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) = 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') - }} - /> -
- { const { resolvedTheme } = useTheme() - const queryClient = useQueryClient() const selectedOrganization = useSelectedOrganization() const [selectedPaymentMethod, setSelectedPaymentMethod] = useState() const [paymentIntentSecret, setPaymentIntentSecret] = useState(null) @@ -182,23 +181,6 @@ export const SubscriptionPlanUpdateDialog = ({ return } - if (paymentMethod) { - queryClient.setQueriesData( - organizationKeys.paymentMethods(selectedOrganization.slug), - (prev: any) => { - if (!prev) return prev - return { - ...prev, - defaultPaymentMethodId: paymentMethod?.id, - data: prev.data.map((pm: any) => ({ - ...pm, - is_default: pm.id === paymentMethod?.id, - })), - } - } - ) - } - // If the user is downgrading from team, should have spend cap disabled by default const tier = subscription?.plan?.id === 'team' && selectedTier === PRICING_TIER_PRODUCT_IDS.PRO @@ -253,6 +235,10 @@ export const SubscriptionPlanUpdateDialog = ({ { + // Do not allow closing mid-change + if (isUpdating || paymentConfirmationLoading || isConfirming) { + return + } if (!open) onClose() }} > @@ -561,16 +547,16 @@ export const SubscriptionPlanUpdateDialog = ({
- {!billingViaPartner && !subscriptionPreviewIsLoading && changeType === 'upgrade' && ( + {!billingViaPartner && subscriptionPreview != null && changeType === 'upgrade' && (
{}} createPaymentMethodInline={ - subscriptionPreview?.pending_subscription_flow === true + subscriptionPreview.pending_subscription_flow === true } readOnly={paymentConfirmationLoading || isConfirming || isUpdating} /> @@ -700,7 +686,6 @@ export const SubscriptionPlanUpdateDialog = ({ paymentIntentConfirmed(paymentIntentConfirmation) } onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)} - paymentMethodId={selectedPaymentMethod!} /> )} diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index c6598e976611c..40233432b40d9 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -645,7 +645,6 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr paymentIntentConfirmed(paymentIntentConfirmation) } onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)} - paymentMethodId={paymentMethod.id} onError={(err) => { toast.error(err.message, { duration: 10_000 }) setNewOrgLoading(false) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx index e9a20a5e1bd35..a268b1aaefff5 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx @@ -63,43 +63,17 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { const currentFolderIdx = openedFolders.length - 1 const currentFolder = openedFolders[currentFolderIdx] - if (itemSearchString) { - if (!currentFolder) { - // At root of bucket - await fetchFolderContents({ - bucketId: bucket.id, - folderId: bucket.id, - folderName: bucket.name, - index: -1, - searchString: itemSearchString, - }) - } else { - await fetchFolderContents({ - bucketId: bucket.id, - folderId: currentFolder.id, - folderName: currentFolder.name, - index: currentFolderIdx, - searchString: itemSearchString, - }) - } - } else { - if (!currentFolder) { - // At root of bucket - await fetchFolderContents({ - bucketId: bucket.id, - folderId: bucket.id, - folderName: bucket.name, - index: -1, - }) - } else { - await fetchFolderContents({ - bucketId: bucket.id, - folderId: currentFolder.id, - folderName: currentFolder.name, - index: currentFolderIdx, - }) - } - } + const folderId = !currentFolder ? bucket.id : currentFolder.id + const folderName = !currentFolder ? bucket.name : currentFolder.name + const index = !currentFolder ? -1 : currentFolderIdx + + await fetchFolderContents({ + bucketId: bucket.id, + folderId, + folderName, + index, + searchString: itemSearchString, + }) } else if (view === STORAGE_VIEWS.COLUMNS) { if (openedFolders.length > 0) { const paths = openedFolders.map((folder) => folder.name) @@ -110,6 +84,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { folderId: bucket.id, folderName: bucket.name, index: -1, + searchString: itemSearchString, }) } } diff --git a/apps/studio/data/feedback/support-ticket-send.ts b/apps/studio/data/feedback/support-ticket-send.ts index 45ed494472e9b..137215c244f1f 100644 --- a/apps/studio/data/feedback/support-ticket-send.ts +++ b/apps/studio/data/feedback/support-ticket-send.ts @@ -52,7 +52,7 @@ export async function sendSupportTicket({ }, }) - if (error) handleError(error) + if (error) handleError(error, { alwaysCapture: true }) return data } diff --git a/apps/studio/data/fetchers.ts b/apps/studio/data/fetchers.ts index d3849c3e83e96..9c369ee550472 100644 --- a/apps/studio/data/fetchers.ts +++ b/apps/studio/data/fetchers.ts @@ -128,8 +128,18 @@ export const { OPTIONS: options, } = client -export const handleError = (error: unknown): never => { +type HandleErrorOptions = { + alwaysCapture?: boolean +} + +export const handleError = ( + error: unknown, + options: HandleErrorOptions = { alwaysCapture: false } +): never => { if (error && typeof error === 'object') { + if (options.alwaysCapture) { + Sentry.captureException(error) + } const errorMessage = 'msg' in error && typeof error.msg === 'string' ? error.msg diff --git a/apps/studio/data/subscriptions/org-subscription-confirm-pending-change.ts b/apps/studio/data/subscriptions/org-subscription-confirm-pending-change.ts index 44df661518567..fc660901b76c1 100644 --- a/apps/studio/data/subscriptions/org-subscription-confirm-pending-change.ts +++ b/apps/studio/data/subscriptions/org-subscription-confirm-pending-change.ts @@ -74,7 +74,9 @@ export const useConfirmPendingSubscriptionChangeMutation = ({ queryClient.invalidateQueries(invoicesKeys.orgUpcomingPreview(slug)), queryClient.invalidateQueries(organizationKeys.detail(slug)), queryClient.invalidateQueries(organizationKeys.list()), + queryClient.invalidateQueries(organizationKeys.paymentMethods(slug)), ]) + await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/subscriptions/org-subscription-update-mutation.ts b/apps/studio/data/subscriptions/org-subscription-update-mutation.ts index d899039b23cb8..d2806ab227bda 100644 --- a/apps/studio/data/subscriptions/org-subscription-update-mutation.ts +++ b/apps/studio/data/subscriptions/org-subscription-update-mutation.ts @@ -68,6 +68,20 @@ export const useOrgSubscriptionUpdateMutation = ({ queryClient.invalidateQueries(organizationKeys.detail(slug)), queryClient.invalidateQueries(organizationKeys.list()), ]) + + if (variables.paymentMethod) { + queryClient.setQueriesData(organizationKeys.paymentMethods(slug), (prev: any) => { + if (!prev) return prev + return { + ...prev, + defaultPaymentMethodId: variables.paymentMethod, + data: prev.data.map((pm: any) => ({ + ...pm, + is_default: pm.id === variables.paymentMethod, + })), + } + }) + } } await onSuccess?.(data, variables, context) diff --git a/docker/.env.example b/docker/.env.example index 2ca5a229c209d..8ee5f75c680c3 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -26,10 +26,17 @@ POSTGRES_PORT=5432 ############ # Supavisor -- Database pooler ############ +# Port Supavisor listens on for transaction pooling connections POOLER_PROXY_PORT_TRANSACTION=6543 +# Maximum number of PostgreSQL connections Supavisor opens per pool POOLER_DEFAULT_POOL_SIZE=20 +# Maximum number of client connections Supavisor accepts per pool POOLER_MAX_CLIENT_CONN=100 +# Unique tenant identifier POOLER_TENANT_ID=your-tenant-id +# Pool size for internal metadata storage used by Supavisor +# This is separate from client connections and used only by Supavisor itself +POOLER_DB_POOL_SIZE=5 ############ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ddacfad276d33..6cd49f2921f19 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -506,7 +506,7 @@ services: POSTGRES_PORT: ${POSTGRES_PORT} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/_supabase + DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase CLUSTER_POSTGRES: true SECRET_KEY_BASE: ${SECRET_KEY_BASE} VAULT_ENC_KEY: ${VAULT_ENC_KEY} @@ -518,6 +518,7 @@ services: POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE} POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN} POOLER_POOL_MODE: transaction + DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE} command: [ "/bin/sh",