From d346ee1ca8e2b7a12c0e14196f0f8c7cda420f21 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 20 Feb 2026 17:43:00 +0800 Subject: [PATCH 1/3] Tiny fix wording (#43040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Just to avoid confusion 🤦 ETL Private Alpha config cat feature flag holds org slugs, not project refs --- .../interfaces/Database/Replication/useIsETLPrivateAlpha.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts b/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts index fb78965963be1..04486c97f74c4 100644 --- a/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts +++ b/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts @@ -8,12 +8,12 @@ export const useIsETLPrivateAlpha = () => { const { data: organization } = useSelectedOrganizationQuery() const etlPrivateAlpha = useFlag('etlPrivateAlpha') - const privateAlphaProjectRefs = + const privateAlphaOrgSlugs = typeof etlPrivateAlpha === 'string' ? (etlPrivateAlpha as string).split(',').map((x) => x.trim()) : [] const etlShowForAllProjects = useFlag('etlPrivateAlphaOverride') - return etlShowForAllProjects || privateAlphaProjectRefs.includes(organization?.slug ?? '') + return etlShowForAllProjects || privateAlphaOrgSlugs.includes(organization?.slug ?? '') } From e15860f169326732662d7fb2a3420f7fa06f9ba6 Mon Sep 17 00:00:00 2001 From: zero Date: Fri, 20 Feb 2026 18:56:17 +0900 Subject: [PATCH 2/3] docs(auth): clarify kakao email optional (#42405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Docs update. ## What is the current behavior? The Kakao login guide implies `account_email` is required, even though it can be optional in Supabase. ## What is the new behavior? Clarifies that `account_email` is optional and notes that users should enable “Allow users without an email” in the Supabase Kakao provider settings when not requesting `account_email`. ## Additional context N/A ## Summary by CodeRabbit * **Documentation** * Updated Kakao social login documentation to clarify that email consent is optional. * Added guidance on configuring provider settings to allow users without email when email is not requested or unavailable. --- apps/docs/content/guides/auth/social-login/auth-kakao.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/guides/auth/social-login/auth-kakao.mdx b/apps/docs/content/guides/auth/social-login/auth-kakao.mdx index 5247599aa9edc..e2177b44cfc19 100644 --- a/apps/docs/content/guides/auth/social-login/auth-kakao.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-kakao.mdx @@ -67,10 +67,12 @@ This serves as the `client_id` when you make API calls to authenticate the user. - Set **State** to "ON" in the **Usage settings** section to enable Kakao Login. - Go to **Product Settings** > **Kakao Login** > **Consent Items**. - Set the following scopes under the **Consent Items**: - - account_email + - account_email (optional) - profile_image - profile_nickname +If you don't need an email address (or `account_email` isn't available for your app), you can omit `account_email` and enable **Allow users without an email** in the Supabase Kakao provider settings. + ![Kakao consent items configuration](/docs/img/guides/auth-kakao/kakao-developers-consent-items-set.png) @@ -83,6 +85,8 @@ In the Kakao Developers Portal, the "account_email" consent item is only availab <$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "Kakao" }} /> +If you did not request `account_email` in Kakao, enable **Allow users without an email** in the Kakao provider settings. + ## Add login code to your client app Date: Fri, 20 Feb 2026 15:32:11 +0530 Subject: [PATCH 3/3] fix: show uninstall progress (#42860) This PR makes the following changes: * Refactors the Stripe sync engine integration so that impossible states are unrepresentable in the code. * Factors out common code and functions into a single location to avoid duplication. * Factors out code to fetch installation/uninstallation and sync status into a separate hook to make the code more readable. * Shows uninstall progress when the user uninstalls the integration. * Moves the **Uninstall integration** button from the **Settings** page to the **Overview** page. This is done to avoid us having to track the uninstallation progress via the url query params when the user is redirected to the **Overview** page during uninstallation. Also it makes sense for installation/uninstallation to be available on the same location. Note that even with the above changes there are some limitations to how the uninstallation progress is shown. In particular, if the user refreshes the page, the uninstallation status is lost because the background uninstallation procedure currently doesn't update the comment on the stripe schema during uninstallation. We are making a change in the stripe sync engine code for that here: https://github.com/stripe-experiments/sync-engine/pull/113 --------- Co-authored-by: Joshen Lim --- .../Integration/IntegrationOverviewTab.tsx | 8 +- .../Landing/useInstalledIntegrations.tsx | 23 +- .../StripeSyncEngine/InstallationOverview.tsx | 544 ++++++++++-------- .../StripeSyncChangesCard.tsx | 48 +- .../StripeSyncSettingsPage.tsx | 200 ++----- .../StripeSyncEngine/stripe-sync-status.ts | 136 +++++ .../StripeSyncEngine/useStripeSyncStatus.ts | 65 +++ .../pages/api/integrations/stripe-sync.ts | 8 +- 8 files changed, 585 insertions(+), 447 deletions(-) create mode 100644 apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts create mode 100644 apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx index 78c379c1c82bd..2846e1dae461e 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx @@ -1,9 +1,9 @@ -import { PropsWithChildren, ReactNode } from 'react' - import { useParams } from 'common' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { PropsWithChildren, ReactNode } from 'react' import { Badge, Card, CardContent, cn, Separator } from 'ui' + import { INTEGRATIONS } from '../Landing/Integrations.constants' import { BuiltBySection } from './BuildBySection' import { MarkdownContent } from './MarkdownContent' @@ -53,8 +53,8 @@ export const IntegrationOverviewTab = ({ {dependsOnExtension && ( -
-

Required extensions

+
+

Required extensions

    diff --git a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx index 8b9d954af0aae..ed565b7ff4494 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx @@ -1,19 +1,20 @@ -import { useMemo } from 'react' - +import { useFlag } from 'common' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSchemasQuery } from 'data/database/schemas-query' import { useFDWsQuery } from 'data/fdw/fdws-query' -import { useFlag } from 'common' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { IS_PLATFORM } from 'lib/constants' import { EMPTY_ARR } from 'lib/void' -import { - INSTALLATION_INSTALLED_SUFFIX, - STRIPE_SCHEMA_COMMENT_PREFIX, -} from 'stripe-experiment-sync/supabase' +import { useMemo } from 'react' + import { wrapperMetaComparator } from '../Wrappers/Wrappers.utils' import { INTEGRATIONS } from './Integrations.constants' +import { + isInstalled as checkIsInstalled, + findStripeSchema, + parseStripeSchemaStatus, +} from '@/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status' export const useInstalledIntegrations = () => { const { data: project } = useSelectedProjectQuery() @@ -84,11 +85,9 @@ export const useInstalledIntegrations = () => { return true } if (integration.id === 'stripe_sync_engine') { - const stripeSchema = schemas?.find(({ name }) => name === 'stripe') - return ( - !!stripeSchema?.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && - !!stripeSchema.comment?.includes(INSTALLATION_INSTALLED_SUFFIX) - ) + const stripeSchema = findStripeSchema(schemas) + const status = parseStripeSchemaStatus(stripeSchema) + return checkIsInstalled(status) } if (integration.type === 'wrapper') { return wrappers.find((w) => wrapperMetaComparator(integration.meta, w)) diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx index c332f90d8ae1e..6e0528d6a5d36 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx @@ -1,27 +1,21 @@ import { zodResolver } from '@hookform/resolvers/zod' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useStripeSyncInstallMutation } from 'data/database-integrations/stripe/stripe-sync-install-mutation' import { useStripeSyncUninstallMutation } from 'data/database-integrations/stripe/stripe-sync-uninstall-mutation' -import { useStripeSyncingState } from 'data/database-integrations/stripe/sync-state-query' import { useSchemasQuery } from 'data/database/schemas-query' import { formatRelative } from 'date-fns' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useTrack } from 'lib/telemetry/track' import { AlertCircle, BadgeCheck, Check, ExternalLink, RefreshCwIcon } from 'lucide-react' import Link from 'next/link' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' -import { - INSTALLATION_ERROR_SUFFIX, - INSTALLATION_INSTALLED_SUFFIX, - INSTALLATION_STARTED_SUFFIX, - STRIPE_SCHEMA_COMMENT_PREFIX, -} from 'stripe-experiment-sync/supabase' import { Button, + Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - Form_Shadcn_, Sheet, SheetContent, SheetFooter, @@ -31,41 +25,68 @@ import { } from 'ui' import { Admonition } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' + import { IntegrationOverviewTab } from '../../Integration/IntegrationOverviewTab' +import { + canInstall as checkCanInstall, + hasInstallError, + hasUninstallError, + isInstallDone, + isInstalled, + isInstalling, + isSyncRunning, + isUninstallDone, + isUninstalling, +} from './stripe-sync-status' import { StripeSyncChangesCard } from './StripeSyncChangesCard' +import { useStripeSyncStatus } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus' +import { InlineLink } from '@/components/ui/InlineLink' const installFormSchema = z.object({ stripeSecretKey: z.string().min(1, 'Stripe API key is required'), }) export const StripeSyncInstallationPage = () => { - const { data: project } = useSelectedProjectQuery() const track = useTrack() const hasTrackedInstallFailed = useRef(false) + const { data: project } = useSelectedProjectQuery() const [shouldShowInstallSheet, setShouldShowInstallSheet] = useState(false) + const [showUninstallModal, setShowUninstallModal] = useState(false) + // These flags bridge the gap between mutation success and schema update const [isInstallInitiated, setIsInstallInitiated] = useState(false) + const [isUninstallInitiated, setIsUninstallInitiated] = useState(false) const formId = 'stripe-sync-install-form' const form = useForm>({ resolver: zodResolver(installFormSchema), - defaultValues: { - stripeSecretKey: '', - }, + defaultValues: { stripeSecretKey: '' }, mode: 'onSubmit', }) - const { data: schemas } = useSchemasQuery({ + // Use the unified status hook + const { installationStatus, syncState } = useStripeSyncStatus({ projectRef: project?.ref, connectionString: project?.connectionString, }) + const isSyncing = isSyncRunning(syncState) + + const installed = isInstalled(installationStatus) + const installError = hasInstallError(installationStatus) + const uninstallError = hasUninstallError(installationStatus) + const installInProgress = isInstalling(installationStatus) + const uninsallInProgress = isUninstalling(installationStatus) + const installDone = isInstallDone(installationStatus) + const uninstallDone = isUninstallDone(installationStatus) + const { mutate: installStripeSync, - isPending: isInstalling, - error: installError, + isPending: isInstallRequested, + error: installRequestError, reset: resetInstallError, } = useStripeSyncInstallMutation({ onSuccess: () => { @@ -76,103 +97,41 @@ export const StripeSyncInstallationPage = () => { }, }) - const { mutate: uninstallStripeSync, isPending: isUninstalling } = useStripeSyncUninstallMutation( - { + const { mutate: uninstallStripeSync, isPending: isUninstallRequested } = + useStripeSyncUninstallMutation({ onSuccess: () => { toast.success('Stripe Sync uninstallation started') + setShowUninstallModal(false) + setIsUninstallInitiated(true) }, - } - ) - - const stripeSchema = schemas?.find((s) => s.name === 'stripe') - - // Determine installation status from schema description - const isInstalled = - stripeSchema && - stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && - stripeSchema.comment.includes(INSTALLATION_INSTALLED_SUFFIX) - - const schemaShowsInProgress = - stripeSchema && - stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && - stripeSchema.comment?.includes(INSTALLATION_STARTED_SUFFIX) - - const setupInProgress = schemaShowsInProgress || isInstalling || isInstallInitiated - - const setupError = - stripeSchema && - stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && - stripeSchema.comment?.includes(INSTALLATION_ERROR_SUFFIX) - - useEffect(() => { - if (!setupError) { - hasTrackedInstallFailed.current = false - return - } - - if (!hasTrackedInstallFailed.current) { - hasTrackedInstallFailed.current = true - // This isn't ideal because it will fire on every page load while in error state - // in the future we should connect this in the backend to track accurately - track('integration_install_failed', { - integrationName: 'stripe_sync_engine', - }) - } - }, [setupError, track]) - - useEffect(() => { - // Clear the install initiated flag once we detect completion or error from the schema - if (isInstallInitiated && (isInstalled || setupError)) { - setIsInstallInitiated(false) - } - }, [isInstallInitiated, isInstalled, setupError]) - - // Check if there's an existing stripe schema that wasn't created by this integration - const hasConflictingSchema = - stripeSchema && !stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) - - const canInstall = !hasConflictingSchema && !isInstalled && !setupInProgress + }) - // Sync state query - only enabled when installed - const { data: syncState } = useStripeSyncingState( - { - projectRef: project?.ref!, - connectionString: project?.connectionString, - }, - { - refetchInterval: 4000, - enabled: !!isInstalled, - } - ) + // Combine schema status with mutation/initiated states for UI + const installing = installInProgress || isInstallRequested || isInstallInitiated + const uninstalling = uninsallInProgress || isUninstallRequested || isUninstallInitiated + const canInstall = checkCanInstall(installationStatus) && !installed && !installing - // Poll for schema changes during installation + // Poll for schema changes during transitions useSchemasQuery( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - }, - { - refetchInterval: setupInProgress ? 5000 : false, - } + { projectRef: project?.ref, connectionString: project?.connectionString }, + { refetchInterval: installing || uninstalling ? 5000 : false } ) - const isSyncing = !!syncState && !syncState.closed_at && syncState.status === 'running' - - const handleUninstall = () => { + const handleUninstall = useCallback(() => { if (!project?.ref) return uninstallStripeSync({ projectRef: project.ref, }) - } + }, [project?.ref, uninstallStripeSync]) - const handleOpenInstallSheet = () => { + const handleOpenInstallSheet = useCallback(() => { resetInstallError() setShouldShowInstallSheet(true) - } + }, [resetInstallError]) const handleCloseInstallSheet = (isOpen: boolean) => { - if (isInstalling) return + if (isInstallRequested) return setShouldShowInstallSheet(isOpen) if (!isOpen) { @@ -184,7 +143,28 @@ export const StripeSyncInstallationPage = () => { const tableEditorUrl = `/project/${project?.ref}/editor?schema=stripe` const alert = useMemo(() => { - if (setupError) { + if (uninstallError) { + return ( + +
    + There was an error during the uninstallation of the Stripe Sync Engine. Please try + again. If the problem persists, contact support. +
    +
    + +
    +
    + ) + } + + if (installError) { return (
    @@ -196,8 +176,8 @@ export const StripeSyncInstallationPage = () => { @@ -206,7 +186,7 @@ export const StripeSyncInstallationPage = () => { ) } - if (syncState) { + if (syncState && installed && !uninstalling) { return (
    @@ -225,7 +205,7 @@ export const StripeSyncInstallationPage = () => {
    All up to date
    -
    @@ -241,24 +221,44 @@ export const StripeSyncInstallationPage = () => { return null }, [ - setupError, - setupInProgress, + uninstallError, + installError, syncState, isSyncing, - isUninstalling, + installed, + isUninstallRequested, + tableEditorUrl, + uninstalling, handleOpenInstallSheet, handleUninstall, ]) - const status = useMemo(() => { - if (isInstalled) { + const statusDisplay = useMemo(() => { + if (uninstallError) { return ( - Installed + + Uninstallation error + + ) + } + if (uninstalling) { + return ( + + + Uninstalling... ) } - if (setupInProgress) { + if (installError) { + return ( + + + Installation error + + ) + } + if (installing) { return ( @@ -266,7 +266,7 @@ export const StripeSyncInstallationPage = () => { ) } - if (syncState) { + if (isSyncing && installed) { return ( @@ -274,146 +274,220 @@ export const StripeSyncInstallationPage = () => { ) } - if (setupError) { + if (installed) { return ( - - Installation error + Installed ) } return ( Not installed ) - }, [isInstalled, setupInProgress, syncState, setupError]) + }, [uninstallError, uninstalling, installError, installing, isSyncing, installed]) + + // Track install failures + useEffect(() => { + if (!installError) { + hasTrackedInstallFailed.current = false + return + } + + if (!hasTrackedInstallFailed.current) { + hasTrackedInstallFailed.current = true + track('integration_install_failed', { + integrationName: 'stripe_sync_engine', + }) + } + }, [installError, track]) + + // Clear install initiated flag once schema reflects completion or error + useEffect(() => { + if (isInstallInitiated && installDone) { + setIsInstallInitiated(false) + } + }, [isInstallInitiated, installDone]) + + // Clear uninstall initiated flag once schema is removed or error + useEffect(() => { + if (isUninstallInitiated && uninstallDone) { + setIsUninstallInitiated(false) + } + }, [isUninstallInitiated, uninstallDone]) return ( - setShouldShowInstallSheet(true)} - /> - ) : null - } - > - - - -
    { - if (!project?.ref) return - installStripeSync({ projectRef: project.ref, stripeSecretKey }) - })} - className="overflow-auto flex-grow px-0 flex flex-col" - > - - Install Stripe Sync Engine - - - - -

    - This integration currently requires{' '} - - SSL Enforcement - {' '} - to be disabled during initial setup. Support for SSL Enforcement will be added - in a future update. Once installed, all webhook and sync operations use - HTTPS/SSL. -

    -
    -

    Configuration

    - {installError && ( - -
    - -
    -

    Installation failed

    -

    {installError.message}

    -
    -
    + <> + + +
    + setShouldShowInstallSheet(true)} + disabled={!canInstall} + tooltip={{ + content: { + text: !canInstall + ? 'Your database already uses a schema named "stripe"' + : undefined, + }, + }} + > + Install integration + +
    + + ) : installed && !uninstalling ? ( +
    + +
    + ) : null + } + > + + + + { + if (!project?.ref) return + installStripeSync({ projectRef: project.ref, stripeSecretKey }) + })} + className="overflow-auto flex-grow px-0 flex flex-col" + > + + Install Stripe Sync Engine + + + + + +
    + This integration currently requires{' '} + + SSL Enforcement + {' '} + to be disabled during initial setup. +
    +

    + Support for SSL Enforcement will be added in a future update. Once installed, + all webhook and sync operations use HTTPS/SSL. +

    - )} - - ( - - - field.onChange(e.target.value)} - /> - - + +

    Configuration

    + + ( + + + field.onChange(e.target.value)} + /> + + + )} + /> + +
    + + +
    + + {installRequestError && ( + )} - /> - -
    - - -
    -
    - - - - - -
    -
    -
    -
    + + +
    +
    +
    + + setShowUninstallModal(false)} + onConfirm={handleUninstall} + > +

    + Are you sure you want to uninstall the Stripe Sync Engine? This will: +

    +
      +
    • + Remove the stripe schema and all tables +
    • +
    • Delete all synced Stripe data
    • +
    • Remove the associated Edge Functions
    • +
    • Remove the scheduled sync jobs
    • +
    +

    + This action cannot be undone. +

    +
    +
    + ) } diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx index 6ca75a277a553..0271b45a9c74d 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx @@ -1,40 +1,34 @@ -import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { EdgeFunctions } from 'icons' import { Layers, Table } from 'lucide-react' -import { Card, CardContent, CardFooter, cn } from 'ui' +import { Card, CardContent, cn } from 'ui' type StripeSyncChangesCardProps = { className?: string - canInstall?: boolean - onInstall?: () => void } -export const StripeSyncChangesCard = ({ - className, - canInstall = true, - onInstall, -}: StripeSyncChangesCardProps) => { - const showInstallButton = typeof onInstall === 'function' +const ListItemClassName = 'flex items-center gap-x-3 py-2 px-3 border-b' +export const StripeSyncChangesCard = ({ className }: StripeSyncChangesCardProps) => { return ( -
    -

    This integration will modify your Supabase project:

    +
    +

    This integration will modify your Supabase project:

      -
    • +
    • - Creates a new database schema named stripe + Creates a new database schema named stripe -
    • +
    • - Creates tables and views in the stripe schema for synced Stripe data + Creates tables and views in the stripe{' '} + schema for synced Stripe data -
    • +
    • Deploys Edge Functions to handle incoming webhooks from Stripe
    • -
    • +
    • Schedules automatic Stripe data syncs using Supabase Queues
    • - {showInstallButton && ( - - - Install integration - - - )} ) diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx index 6d5632832105c..3075db7778f34 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx @@ -1,25 +1,9 @@ -import { useSchemasQuery } from 'data/database/schemas-query' -import { useStripeSyncUninstallMutation } from 'data/database-integrations/stripe/stripe-sync-uninstall-mutation' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Loader2, Table2 } from 'lucide-react' +import { Table2 } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import { toast } from 'sonner' -import { - INSTALLATION_INSTALLED_SUFFIX, - STRIPE_SCHEMA_COMMENT_PREFIX, -} from 'stripe-experiment-sync/supabase' -import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - Button, - Card, - CardContent, - WarningIcon, -} from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { useEffect } from 'react' +import { Button, Card, CardContent } from 'ui' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, @@ -30,158 +14,62 @@ import { PageSectionTitle, } from 'ui-patterns/PageSection' +import { isInstalled } from './stripe-sync-status' +import { useStripeSyncStatus } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus' + export const StripeSyncSettingsPage = () => { const router = useRouter() const { data: project } = useSelectedProjectQuery() - const [showUninstallModal, setShowUninstallModal] = useState(false) - const [isUninstallInitiated, setIsUninstallInitiated] = useState(false) - const { data: schemas, isLoading: isSchemasLoading } = useSchemasQuery({ + const { installationStatus, isLoading: isLoadingInstallationStatus } = useStripeSyncStatus({ projectRef: project?.ref, connectionString: project?.connectionString, }) + const installed = isInstalled(installationStatus) - const stripeSchema = schemas?.find((s) => s.name === 'stripe') - - const isInstalled = - stripeSchema && - stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && - stripeSchema.comment.includes(INSTALLATION_INSTALLED_SUFFIX) - - // Redirect to overview if not installed + // Redirect to overview page when integration is not installed useEffect(() => { - if (!isSchemasLoading && !isInstalled && project?.ref) { - router.push(`/project/${project.ref}/integrations/stripe_sync_engine/overview`) - } - }, [isSchemasLoading, isInstalled, project?.ref, router]) - - const { mutate: uninstallStripeSync, isPending: isUninstalling } = useStripeSyncUninstallMutation( - { - onSuccess: () => { - toast.success('Stripe Sync uninstallation started') - setShowUninstallModal(false) - setIsUninstallInitiated(true) - // Redirect to overview after uninstall - if (project?.ref) { - router.push(`/project/${project.ref}/integrations/stripe_sync_engine/overview`) - } - }, - } - ) + if (isLoadingInstallationStatus || installed || !project?.ref) return - const handleUninstall = () => { - if (!project?.ref) return - uninstallStripeSync({ projectRef: project.ref }) - } - - const tableEditorUrl = `/project/${project?.ref}/editor?schema=stripe` - - // Show loading state while checking installation status - if (isSchemasLoading || !isInstalled) { - return ( -
      - -
      - ) - } + router.push(`/project/${project.ref}/integrations/stripe_sync_engine/overview`) + }, [isLoadingInstallationStatus, installed, project?.ref, router]) return ( - <> - - - - - Stripe Schema - - Access and manage the synced Stripe data in your database. - - - - - - -
      + + + + + Stripe Schema + + Access and manage the synced Stripe data in your database. + + + + + + +
      +
      -
      -
      -
      Open Stripe schema in Table Editor
      -

      - The Stripe Sync Engine stores all synced data in the stripe{' '} - schema. You can view and query this data directly in the Table Editor. -

      -
      - +
      +
      Open Stripe schema in Table Editor
      +

      + The Stripe Sync Engine stores all synced data in the{' '} + stripe schema. You can + view and query this data directly in the Table Editor. +

      - - - - - - - - - Uninstall Stripe Sync Engine - - Remove the integration and all synced data from your project. - - - - - -
      - +
      -
      -
      - - Uninstalling will remove the stripe schema and all synced data. - - This action cannot be undone. -
      -
      - -
      -
      -
      -
      -
      - - - setShowUninstallModal(false)} - onConfirm={handleUninstall} - > -

      - Are you sure you want to uninstall the Stripe Sync Engine? This will: -

      -
        -
      • - Remove the stripe schema and all tables -
      • -
      • Delete all synced Stripe data
      • -
      • Remove the associated Edge Functions
      • -
      • Remove the scheduled sync jobs
      • -
      -

      - This action cannot be undone. -

      -
      - + + + + + ) } diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts new file mode 100644 index 0000000000000..190f76ad64307 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts @@ -0,0 +1,136 @@ +import type { + StripeSyncState, + StripeSyncStateData, +} from 'data/database-integrations/stripe/sync-state-query' +import type { Schema } from 'data/database/schemas-query' +import { + INSTALLATION_ERROR_SUFFIX, + INSTALLATION_INSTALLED_SUFFIX, + INSTALLATION_STARTED_SUFFIX, + STRIPE_SCHEMA_COMMENT_PREFIX, +} from 'stripe-experiment-sync/supabase' + +/** + * All possible Stripe Sync installation states. + */ +export type StripeInstallationStatus = + | 'installing' + | 'installed' + | 'install_error' + | 'uninstalling' + | 'uninstalled' + | 'uninstall_error' + +/** + * Complete Stripe Sync status including schema, installation state, and sync state + */ +export interface StripeSyncStatusResult { + /** The installation status */ + installationStatus: StripeInstallationStatus + + /** Current sync run state (only available when installationStatus is installed) */ + syncState: StripeSyncState | undefined + + /** Whether the status is still being determined (schemas query is loading) */ + isLoading: boolean +} + +// TODO: The current version of the package 'stripe-experiment-sync' doesn't export +// these constants, but we plan to export them in future version. For now we +// declare the same constants here to deploy this code without waiting for +// a new version. We'll import them when we bump 'stripe-experiment-sync' package +// version. +const UNINSTALLATION_STARTED_SUFFIX = 'uninstallation:started' +const UNINSTALLATION_ERROR_SUFFIX = 'uninstallation:error' + +export function findStripeSchema(schemas: Schema[] | undefined): Schema | undefined { + return schemas?.find((s) => s.name === 'stripe') +} + +function isStripeSyncSchema(schema: Schema | undefined): boolean { + return !!schema?.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) +} + +function hasStatusSuffix(schema: Schema | undefined, suffix: string): boolean { + return isStripeSyncSchema(schema) && !!schema?.comment?.endsWith(suffix) +} + +/** + * Parse the installation status from a stripe schema. + * + * @param stripeSchema - The stripe schema from the database, if it exists + * @returns The installation status + */ +export function parseStripeSchemaStatus( + stripeSchema: Schema | undefined +): StripeInstallationStatus { + if (hasStatusSuffix(stripeSchema, UNINSTALLATION_ERROR_SUFFIX)) { + return 'uninstall_error' + } + + if (hasStatusSuffix(stripeSchema, INSTALLATION_ERROR_SUFFIX)) { + return 'install_error' + } + + if (hasStatusSuffix(stripeSchema, UNINSTALLATION_STARTED_SUFFIX)) { + return 'uninstalling' + } + + if (hasStatusSuffix(stripeSchema, INSTALLATION_STARTED_SUFFIX)) { + return 'installing' + } + + if (hasStatusSuffix(stripeSchema, INSTALLATION_INSTALLED_SUFFIX)) { + return 'installed' + } + + return 'uninstalled' +} + +export function isInstalled(status: StripeInstallationStatus): boolean { + return status === 'installed' +} + +export function isUninstalled(status: StripeInstallationStatus): boolean { + return status === 'uninstalled' +} + +export function hasInstallError(status: StripeInstallationStatus): boolean { + return status === 'install_error' +} + +export function hasUninstallError(status: StripeInstallationStatus): boolean { + return status === 'uninstall_error' +} + +export function hasError(status: StripeInstallationStatus): boolean { + return hasInstallError(status) || hasUninstallError(status) +} + +export function isInstalling(status: StripeInstallationStatus): boolean { + return status === 'installing' +} + +export function isUninstalling(status: StripeInstallationStatus): boolean { + return status === 'uninstalling' +} + +export function isInProgress(status: StripeInstallationStatus): boolean { + return isInstalling(status) || isUninstalling(status) +} + +export function isInstallDone(status: StripeInstallationStatus): boolean { + return isInstalled(status) || hasInstallError(status) +} + +export function isUninstallDone(status: StripeInstallationStatus): boolean { + return isUninstalled(status) || hasUninstallError(status) +} + +export function canInstall(status: StripeInstallationStatus): boolean { + return isUninstalled(status) || hasError(status) +} + +export function isSyncRunning(syncState: StripeSyncStateData | undefined): boolean { + return !!syncState && !syncState.closed_at && syncState.status === 'running' +} diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts new file mode 100644 index 0000000000000..a28f840598413 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts @@ -0,0 +1,65 @@ +import { useStripeSyncingState } from 'data/database-integrations/stripe/sync-state-query' +import { SchemasVariables, useSchemasQuery } from 'data/database/schemas-query' +import { useEffect } from 'react' + +import { + findStripeSchema, + isInProgress, + isInstalled, + parseStripeSchemaStatus, + type StripeSyncStatusResult, +} from '@/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status' + +/** + * Unified hook for Stripe Sync installation status. + * + * This hook consolidates all schema querying, status parsing, and polling logic + * into a single source of truth. It returns a discriminated union status that + * makes impossible states unrepresentable. + */ +export function useStripeSyncStatus({ + projectRef, + connectionString, +}: SchemasVariables): StripeSyncStatusResult { + // Query schemas once + const { + data: schemas, + isLoading: isSchemasLoading, + refetch, + } = useSchemasQuery({ projectRef, connectionString }, { enabled: !!projectRef }) + + // Find and parse stripe schema status + const stripeSchema = findStripeSchema(schemas) + const installationStatus = parseStripeSchemaStatus(stripeSchema) + + const installed = isInstalled(installationStatus) + const inProgress = isInProgress(installationStatus) + + // Poll schemas during install/uninstall operations + useEffect(() => { + // Return if installation/uninstallation is not in progress + // inProgres is likely to be false during initial render + if (!inProgress) return + + const interval = setInterval(() => { + refetch() + }, 5000) + + return () => clearInterval(interval) + }, [inProgress, refetch]) + + // Query sync state only when installed + const { data: syncState, isLoading: isSyncStateLoading } = useStripeSyncingState( + { projectRef: projectRef!, connectionString }, + { + refetchInterval: 4000, + enabled: !!projectRef && installed, + } + ) + + return { + installationStatus, + syncState: installed ? syncState : undefined, + isLoading: isSchemasLoading, + } +} diff --git a/apps/studio/pages/api/integrations/stripe-sync.ts b/apps/studio/pages/api/integrations/stripe-sync.ts index e88c53f5b28d2..0c35530202751 100644 --- a/apps/studio/pages/api/integrations/stripe-sync.ts +++ b/apps/studio/pages/api/integrations/stripe-sync.ts @@ -1,8 +1,8 @@ +import { waitUntil } from '@vercel/functions' import { NextApiRequest, NextApiResponse } from 'next' -import { z } from 'zod' -import { install, uninstall } from 'stripe-experiment-sync/supabase' import { VERSION } from 'stripe-experiment-sync' -import { waitUntil } from '@vercel/functions' +import { install, uninstall } from 'stripe-experiment-sync/supabase' +import { z } from 'zod' const ENABLE_FLAG_KEY = 'enableStripeSyncEngineIntegration' @@ -112,7 +112,7 @@ async function handleSetupStripeSyncInstall(req: NextApiRequest, res: NextApiRes errorData.error?.message || `Invalid Stripe API key (HTTP ${stripeResponse.status})` return res.status(400).json({ data: null, - error: { message: `Invalid Stripe API key: ${errorMessage}` }, + error: { message: errorMessage }, }) } } catch (error: any) {