From 16bb5187c971890f1e6174eb5affa0662cc603d9 Mon Sep 17 00:00:00 2001 From: Ignacio Dobronich Date: Tue, 22 Jul 2025 08:25:36 -0300 Subject: [PATCH 1/8] docs: credit top ups pay outstanding invoices (#37255) --- apps/docs/content/guides/platform/credits.mdx | 2 ++ .../interfaces/Organization/BillingSettings/CreditTopUp.tsx | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/guides/platform/credits.mdx b/apps/docs/content/guides/platform/credits.mdx index 04ced3dae40e8..a71d1574de0c7 100644 --- a/apps/docs/content/guides/platform/credits.mdx +++ b/apps/docs/content/guides/platform/credits.mdx @@ -29,6 +29,8 @@ As an example, if you start a Pro Plan subscription on January 1 and downgrade t You can top up credits at any time, with a maximum of per top-up. These credits do not expire and are non-refundable. +If you have any outstanding invoices, we’ll automatically use your credits to pay them off. Any remaining credits will be applied to future invoices. + You may want to consider this option to avoid issues with recurring payments, gain more control over how often your credit card is charged, and potentially make things easier for your accounting department. diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx index b33f047760d0e..341df59773789 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx @@ -227,8 +227,8 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => { Top Up Credits On successful payment, an invoice will be issued and you'll be granted credits. - Credits will be applied to future invoices only and are not refundable. The topped up - credits do not expire. + Credits will be applied to outstanding and future invoices and are not refundable. The + topped up credits do not expire. From 008cca006d6ddbdd468ec5947d12434db45c0c0e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 22 Jul 2025 19:47:07 +0800 Subject: [PATCH 2/8] Refactor custom domain verification (#37349) * Refactor custom domain verification * Add success toast when cancelling custom domain * Update toast success for other delete custom domain calls * Revert unnecessary change * Update copy * Update copy --- .../CustomDomainActivate.tsx | 8 ++- .../CustomDomainConfig/CustomDomainConfig.tsx | 25 ++++++---- .../CustomDomainConfig/CustomDomainDelete.tsx | 4 +- .../CustomDomainConfig/CustomDomainVerify.tsx | 50 +++++++++++-------- 4 files changed, 53 insertions(+), 34 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx index c471cd494385c..ef87d4d509dda 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx @@ -30,7 +30,13 @@ const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivate }, } ) - const { mutate: deleteCustomDomain, isLoading: isDeleting } = useCustomDomainDeleteMutation() + const { mutate: deleteCustomDomain, isLoading: isDeleting } = useCustomDomainDeleteMutation({ + onSuccess: () => { + toast.success( + 'Custom domain setup cancelled successfully. It may take a few seconds before your custom domain is fully removed, so you may need to refresh your browser.' + ) + }, + }) const endpoint = settings?.app_config?.endpoint diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx index 6a7e80b13f0e8..aa2391f660b4c 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx @@ -27,10 +27,10 @@ const CustomDomainConfig = () => { const hasCustomDomainAddon = !!addons?.selected_addons.find((x) => x.type === 'custom_domain') const { + data: customDomainData, isLoading: isCustomDomainsLoading, isError, isSuccess, - data, } = useCustomDomainsQuery( { projectRef: ref }, { @@ -45,6 +45,8 @@ const CustomDomainConfig = () => { } ) + const { status, customDomain } = customDomainData || {} + return (
@@ -94,24 +96,25 @@ const CustomDomainConfig = () => { - ) : data?.status === '0_no_hostname_configured' ? ( + ) : status === '0_no_hostname_configured' ? ( ) : ( {isSuccess && (
- {(data.status === '1_not_started' || - data.status === '2_initiated' || - data.status === '3_challenge_verified') && ( - - )} + {(status === '1_not_started' || + status === '2_initiated' || + status === '3_challenge_verified') && } - {data.status === '4_origin_setup_completed' && ( - + {customDomainData.status === '4_origin_setup_completed' && ( + )} - {data.status === '5_services_reconfigured' && ( - + {customDomainData.status === '5_services_reconfigured' && ( + )}
)} diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx index a029d8a1817f7..4ec3c26444264 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx @@ -18,7 +18,9 @@ const CustomDomainDelete = ({ projectRef, customDomain }: CustomDomainDeleteProp const [isDeleteConfirmModalVisible, setIsDeleteConfirmModalVisible] = useState(false) const { mutate: deleteCustomDomain } = useCustomDomainDeleteMutation({ onSuccess: () => { - toast.success(`Successfully deleted custom domain`) + toast.success( + `Successfully deleted custom domain. It may take a few seconds before your custom domain is fully removed, hence you may need to refresh your browser.` + ) setIsDeleteConfirmModalVisible(false) }, }) diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx index 9d97d93a1577b..2e83375d731b4 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx @@ -1,13 +1,14 @@ import { AlertCircle, HelpCircle, RefreshCw } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' +import { toast } from 'sonner' import { useParams } from 'common' import { DocsButton } from 'components/ui/DocsButton' import Panel from 'components/ui/Panel' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation' -import type { CustomDomainResponse } from 'data/custom-domains/custom-domains-query' +import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useCustomDomainReverifyMutation } from 'data/custom-domains/custom-domains-reverify-mutation' import { useInterval } from 'hooks/misc/useInterval' import { @@ -20,16 +21,17 @@ import { import DNSRecord from './DNSRecord' import { DNSTableHeaders } from './DNSTableHeaders' -export type CustomDomainVerifyProps = { - customDomain: CustomDomainResponse -} - -const CustomDomainVerify = ({ customDomain }: CustomDomainVerifyProps) => { +const CustomDomainVerify = () => { const { ref: projectRef } = useParams() const [isNotVerifiedYet, setIsNotVerifiedYet] = useState(false) const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) + const customDomain = customDomainData?.customDomain + const isSSLCertificateDeploying = + customDomain?.ssl.status !== undefined && customDomain.ssl.txt_name === undefined + const { mutate: reverifyCustomDomain, isLoading: isReverifyLoading } = useCustomDomainReverifyMutation({ onSuccess: (res) => { @@ -37,13 +39,19 @@ const CustomDomainVerify = ({ customDomain }: CustomDomainVerifyProps) => { }, }) - const { mutate: deleteCustomDomain, isLoading: isDeleting } = useCustomDomainDeleteMutation() + const { mutate: deleteCustomDomain, isLoading: isDeleting } = useCustomDomainDeleteMutation({ + onSuccess: () => { + toast.success( + 'Custom domain setup cancelled successfully. It may take a few seconds before your custom domain is fully removed, so you may need to refresh your browser.' + ) + }, + }) - const hasCAAErrors = customDomain.ssl.validation_errors?.reduce( + const hasCAAErrors = customDomain?.ssl.validation_errors?.reduce( (acc, error) => acc || error.message.includes('caa_error'), false ) - const isValidating = (customDomain.ssl.txt_name ?? '') === '' + const isValidating = (customDomain?.ssl.txt_name ?? '') === '' const onReverifyCustomDomain = () => { if (!projectRef) return console.error('Project ref is required') @@ -53,7 +61,7 @@ const CustomDomainVerify = ({ customDomain }: CustomDomainVerifyProps) => { useInterval( onReverifyCustomDomain, // Poll every 5 seconds if the SSL certificate is being deployed - customDomain.ssl.status !== undefined && customDomain.ssl.txt_name === undefined ? 5000 : false + isSSLCertificateDeploying && !isDeleting ? 5000 : false ) const onCancelCustomDomain = async () => { @@ -67,7 +75,7 @@ const CustomDomainVerify = ({ customDomain }: CustomDomainVerifyProps) => {

Configure TXT verification for your custom domain{' '} - {customDomain.hostname} + {customDomain?.hostname}

Set the following TXT record(s) in your DNS provider, then click verify to confirm your @@ -102,7 +110,7 @@ const CustomDomainVerify = ({ customDomain }: CustomDomainVerifyProps) => { here @@ -131,13 +139,13 @@ const CustomDomainVerify = ({ customDomain }: CustomDomainVerifyProps) => { Please add a CAA record allowing "digicert.com" to issue certificates for{' '} - {customDomain.hostname}. For example:{' '} + {customDomain?.hostname}. For example:{' '} 0 issue "digicert.com" )} - {customDomain.ssl.status === 'validation_timed_out' ? ( + {customDomain?.ssl.status === 'validation_timed_out' ? ( Validation timed out @@ -147,27 +155,27 @@ const CustomDomainVerify = ({ customDomain }: CustomDomainVerifyProps) => { ) : (

- + - {customDomain.verification_errors?.includes( + {customDomain?.verification_errors?.includes( 'custom hostname does not CNAME to this zone.' ) && ( )} - {!isValidating && customDomain.ssl.status === 'pending_validation' && ( + {!isValidating && customDomain?.ssl.status === 'pending_validation' && ( )} - {customDomain.ssl.status === 'pending_deployment' && ( + {customDomain?.ssl.status === 'pending_deployment' && (

From f9e330f3ca3db7bfa92d0825cbd5f3438eb1e586 Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Tue, 22 Jul 2025 20:51:08 +0800 Subject: [PATCH 3/8] Chore/track branching 2 (#37350) * add review branch w ai * track create branch * track update push branch event * track create merge request * track create and close merge request * track merge attempts and results * add delete branch event * combine handle update * fix lint --- .../BranchManagement/CreateBranchModal.tsx | 15 + .../BranchManagement/OutOfDateNotice.tsx | 37 +- .../BranchManagement/ReviewWithAI.tsx | 13 + .../LayoutHeader/MergeRequestButton.tsx | 16 + .../pages/project/[ref]/branches/index.tsx | 15 + .../project/[ref]/branches/merge-requests.tsx | 43 ++ apps/studio/pages/project/[ref]/merge.tsx | 70 +++ packages/common/telemetry-constants.ts | 433 +++++++++--------- 8 files changed, 428 insertions(+), 214 deletions(-) diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 93c82251740c0..a23bf75eb81ed 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -22,6 +22,7 @@ import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-ch import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query' import { projectKeys } from 'data/projects/keys' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { BASE_PATH, IS_PLATFORM } from 'lib/constants' @@ -83,12 +84,26 @@ export const CreateBranchModal = () => { onError: () => {}, }) + const { mutate: sendEvent } = useSendEventMutation() + const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({ onSuccess: async (data) => { toast.success(`Successfully created preview branch "${data.name}"`) if (projectRef) { await Promise.all([queryClient.invalidateQueries(projectKeys.detail(projectRef))]) } + sendEvent({ + action: 'branch_create_button_clicked', + properties: { + branchType: data.persistent ? 'persistent' : 'preview', + gitlessBranching, + }, + groups: { + project: ref ?? 'Unknown', + organization: selectedOrg?.slug ?? 'Unknown', + }, + }) + setShowCreateBranchModal(false) router.push(`/project/${data.project_ref}`) }, diff --git a/apps/studio/components/interfaces/BranchManagement/OutOfDateNotice.tsx b/apps/studio/components/interfaces/BranchManagement/OutOfDateNotice.tsx index 73444da1e93ec..c90b7f991662a 100644 --- a/apps/studio/components/interfaces/BranchManagement/OutOfDateNotice.tsx +++ b/apps/studio/components/interfaces/BranchManagement/OutOfDateNotice.tsx @@ -13,6 +13,9 @@ import { } from 'ui' import { GitBranchIcon } from 'lucide-react' import { Admonition } from 'ui-patterns' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' interface OutOfDateNoticeProps { isBranchOutOfDateMigrations: boolean @@ -41,6 +44,12 @@ export const OutOfDateNotice = ({ }: OutOfDateNoticeProps) => { const [isDialogOpen, setIsDialogOpen] = useState(false) const hasOutdatedMigrations = isBranchOutOfDateMigrations && missingMigrationsCount > 0 + const selectedOrg = useSelectedOrganization() + const project = useSelectedProject() + const { mutate: sendEvent } = useSendEventMutation() + + const isBranch = project?.parent_project_ref !== undefined + const parentProjectRef = isBranch ? project?.parent_project_ref : project?.ref const getTitle = () => { if (hasOutdatedMigrations && (hasMissingFunctions || hasOutOfDateFunctions)) { @@ -57,12 +66,24 @@ export const OutOfDateNotice = ({ return 'Update this branch to get the latest changes from the production branch.' } - const handleUpdateClick = () => { - onPush() - } + const handleUpdate = (shouldCloseDialog = false) => { + if (shouldCloseDialog) { + setIsDialogOpen(false) + } + + // Track branch update + sendEvent({ + action: 'branch_updated', + properties: { + modifiedEdgeFunctions: hasEdgeFunctionModifications, + source: 'out_of_date_notice', + }, + groups: { + project: parentProjectRef ?? 'Unknown', + organization: selectedOrg?.slug ?? 'Unknown', + }, + }) - const handleConfirmUpdate = () => { - setIsDialogOpen(false) onPush() } @@ -98,7 +119,9 @@ export const OutOfDateNotice = ({ Cancel - Update anyway + handleUpdate(true)}> + Update anyway + @@ -106,7 +129,7 @@ export const OutOfDateNotice = ({