import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useCallback, useEffect, useRef, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Form, FormControl, FormField, Sheet, SheetContent, SheetFooter, SheetHeader, SheetSection, SheetTitle, } 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 { IntegrationOverviewTabV2 } from '../../Integration/IntegrationOverviewTabV2' import { InstallationError } from './InstallationError' import { IntegrationInstalledActions, IntegrationNotInstalledActions } from './IntegrationActions' import { StatusDisplay } from './StatusDisplay' import { canInstall as checkCanInstall, hasInstallError, hasUninstallError, isInstallDone, isInstalled, isInstalling, isUninstallDone, isUninstalling, } from './stripe-sync-status' import { StripeSyncChangesCard } from './StripeSyncChangesCard' import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useStripeSyncStatus } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus' import { useStripeSyncInstallMutation } from '@/data/database-integrations/stripe/stripe-sync-install-mutation' import { useStripeSyncUninstallMutation } from '@/data/database-integrations/stripe/stripe-sync-uninstall-mutation' import { useSchemasQuery } from '@/data/database/schemas-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useTrack } from '@/lib/telemetry/track' const installFormSchema = z.object({ stripeSecretKey: z.string().min(1, 'Stripe API key is required'), }) export const StripeSyncEngineOverviewTab = () => { const track = useTrack() const hasTrackedInstallFailed = useRef(false) const { data: project } = useSelectedProjectQuery() const isMarketplaceEnabled = useIsMarketplaceEnabled() const [showUninstallModal, setShowUninstallModal] = useState(false) const [shouldShowInstallSheet, setShouldShowInstallSheet] = 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 as any), defaultValues: { stripeSecretKey: '' }, mode: 'onSubmit', }) const { schemaComment, schemaComment: { status: installationStatus }, latestAvailableVersion, timedOut, } = useStripeSyncStatus() // Check permissions for managing function secrets const { can: canManageSecrets } = useAsyncCheckPermissions( PermissionAction.FUNCTIONS_SECRET_WRITE, '*' ) const installed = isInstalled(installationStatus) const installError = hasInstallError(installationStatus) const uninstallError = hasUninstallError(installationStatus) const installInProgress = isInstalling(installationStatus) const uninstallInProgress = isUninstalling(installationStatus) const installDone = isInstallDone(installationStatus) const uninstallDone = isUninstallDone(installationStatus) // Detect if this is an upgrade (both old and new versions present) let oldVersion let newVersion if (installed) { // when installed we compare the installed version against the latest available oldVersion = schemaComment?.newVersion newVersion = latestAvailableVersion } else { // otherwise compare the old and new versions from the schema oldVersion = schemaComment?.oldVersion newVersion = schemaComment?.newVersion } const upgradeAvailable = !!(oldVersion && newVersion && oldVersion !== newVersion) const upgradeDone = latestAvailableVersion == schemaComment?.newVersion const { mutate: installStripeSync, isPending: isInstallRequested, error: installRequestError, reset: resetInstallError, } = useStripeSyncInstallMutation({ onSuccess: () => { toast.success( upgradeAvailable ? 'Stripe Sync upgrade started' : 'Stripe Sync installation started' ) setShouldShowInstallSheet(false) form.reset() setIsInstallInitiated(true) }, }) const { mutate: uninstallStripeSync, isPending: isUninstallRequested } = useStripeSyncUninstallMutation({ onSuccess: () => { toast.success('Stripe Sync uninstallation started') setShowUninstallModal(false) setIsUninstallInitiated(true) }, }) // Combine schema status with mutation/initiated states for UI const installing = (installInProgress || isInstallRequested || isInstallInitiated) && !timedOut const uninstalling = (uninstallInProgress || isUninstallRequested || isUninstallInitiated) && !timedOut const canInstall = checkCanInstall(installationStatus) && !installed && !installing const hasError = (uninstallError || installError) && ((!uninstalling && !installing) || timedOut) // Poll for schema changes during transitions useSchemasQuery( { projectRef: project?.ref, connectionString: project?.connectionString }, { refetchInterval: installing || uninstalling ? 5000 : false } ) const handleUninstall = useCallback(() => { if (!project?.ref) return uninstallStripeSync({ projectRef: project.ref, startTime: Date.now(), }) }, [project?.ref, uninstallStripeSync]) const handleOpenInstallSheet = useCallback(() => { resetInstallError() setShouldShowInstallSheet(true) }, [resetInstallError]) const handleCloseInstallSheet = (isOpen: boolean) => { if (isInstallRequested) return setShouldShowInstallSheet(isOpen) if (!isOpen) { form.reset() resetInstallError() } } // 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 successful completion // For errors, the flag is cleared when user manually retries (handleOpenInstallSheet) useEffect(() => { if (isInstallInitiated && installDone && upgradeDone && !installError) { setIsInstallInitiated(false) } }, [isInstallInitiated, installDone, upgradeDone, installError]) // Clear uninstall initiated flag once schema is removed or error useEffect(() => { if (isUninstallInitiated && uninstallDone) { setIsUninstallInitiated(false) } }, [isUninstallInitiated, uninstallDone]) return ( <> {isMarketplaceEnabled ? ( {hasError && ( )} {!installed && !uninstalling && !uninstallError ? ( ) : ( (installed || uninstalling || uninstallError) && ( ) )} ) : ( ) : null } status={ } actions={ !installed && !uninstalling && !uninstallError ? ( <> ) : installed || uninstalling || uninstallError ? ( <> ) : null } >
{ if (!project?.ref) return installStripeSync({ projectRef: project.ref, stripeSecretKey, startTime: Date.now(), }) })} className="overflow-auto grow px-0 flex flex-col" > {upgradeAvailable ? 'Upgrade' : 'Install'} Stripe Sync Engine

Configuration

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

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

This action cannot be undone.

) }