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 { 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 ?? '') } 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) {