This commit is contained in:
Matt Linkous committed 2026-06-08 19:45:08 -05:00
1 parent 498d051d88
commit 0dcd46a52f
14 files changed
+71 -108

No files matched your search

@@ -4,9 +4,9 @@ import { toast } from 'sonner'
import { Button } from 'ui'
import type { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
import { useInstallOAuthIntegrationMutation } from '@/data/marketplace/install-oauth-integration-mutation'
import { useSecretsQuery } from '@/data/secrets/secrets-query'
import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
interface InstallOAuthIntegrationButtonProps {
integration: IntegrationDefinition
@@ -14,22 +14,15 @@ interface InstallOAuthIntegrationButtonProps {
export function InstallOAuthIntegrationButton({ integration }: InstallOAuthIntegrationButtonProps) {
const { ref: projectRef } = useParams()
const { data: organization } = useSelectedOrganizationQuery()
const requiresApiKeysCheck =
integration.installIdentificationMethod === 'secret_key_prefix' && !!integration.secretKeyPrefix
const clientId = integration.oauthClientId
const requiresEdgeFunctionSecretsCheck =
integration.installIdentificationMethod === 'edge_function_secret_name' &&
!!integration.edgeFunctionSecretName
console.log({ integration })
const { data: apiKeys, isLoading: isApiKeysLoading } = useAPIKeysQuery(
{ projectRef, reveal: false },
{ enabled: requiresApiKeysCheck }
)
const { data: edgeFunctionSecrets, isPending: isEdgeFunctionSecretsLoading } = useSecretsQuery(
{ projectRef },
{ enabled: requiresEdgeFunctionSecretsCheck }
const { data: authorizedApps, isLoading: isAuthorizedAppsLoading } = useAuthorizedAppsQuery(
{ slug: organization?.slug },
{ enabled: !!clientId && !!organization?.slug }
)
const { mutate: installOAuthIntegration, isPending: isInstalling } =
@@ -47,27 +40,16 @@ export function InstallOAuthIntegrationButton({ integration }: InstallOAuthInteg
},
})
const isLoading =
(requiresApiKeysCheck && isApiKeysLoading) ||
(requiresEdgeFunctionSecretsCheck && isEdgeFunctionSecretsLoading)
const isLoading = !!clientId && isAuthorizedAppsLoading
const isIntegrationInstalled = useMemo(() => {
if (!integration) return false
if (integration.installIdentificationMethod === 'secret_key_prefix') {
const prefix = integration.secretKeyPrefix
if (!prefix || isApiKeysLoading || !apiKeys) return false
return apiKeys.some((k) => k.type === 'secret' && k.name.startsWith(prefix))
}
if (integration.installIdentificationMethod === 'edge_function_secret_name') {
const secretName = integration.edgeFunctionSecretName
if (!secretName || isEdgeFunctionSecretsLoading || !edgeFunctionSecrets) return false
return edgeFunctionSecrets.some((secret) => secret.name === secretName)
}
return false
}, [apiKeys, edgeFunctionSecrets, integration, isApiKeysLoading, isEdgeFunctionSecretsLoading])
const isIntegrationConnected = useMemo(() => {
console.log({
authorizedApps,
clientId,
})
if (!clientId || !authorizedApps) return false
return authorizedApps.some((app) => app.client_id === clientId)
}, [authorizedApps, clientId])
const handleInstallClick = async () => {
if (!integration || !projectRef) return
@@ -78,9 +60,9 @@ export function InstallOAuthIntegrationButton({ integration }: InstallOAuthInteg
return (
<>
{isIntegrationInstalled ? (
{isIntegrationConnected ? (
<Button disabled type="outline" className="shrink-0">
Installed
Connected
</Button>
) : (
<Button
@@ -117,6 +117,7 @@ export type IntegrationDefinition = {
installIdentificationMethod?: InstallIdentificationMethod
secretKeyPrefix?: string
edgeFunctionSecretName?: string
oauthClientId?: string
listingId?: string
} & (
| { type: 'wrapper'; meta: WrapperMeta }
@@ -7,11 +7,10 @@ import {
isInstalled as checkIsInstalled,
findStripeSchema,
} from '@/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status'
import { type APIKey } from '@/data/api-keys/api-keys-query'
import { type DatabaseExtension } from '@/data/database-extensions/database-extensions-query'
import { type Schema } from '@/data/database/schemas-query'
import { type FDW } from '@/data/fdw/fdws-query'
import { type ProjectSecret } from '@/data/secrets/secrets-query'
import { type AuthorizedApp } from '@/data/oauth/authorized-apps-query'
export const isStripeSyncEngineInstalled = (schemas: Schema[]) => {
const stripeSchema = findStripeSchema(schemas)
@@ -21,28 +20,15 @@ export const isStripeSyncEngineInstalled = (schemas: Schema[]) => {
export const isOAuthInstalled = ({
integration,
apiKeys,
secrets,
authorizedApps,
}: {
integration: IntegrationDefinition
apiKeys: APIKey[]
secrets: ProjectSecret[]
authorizedApps: AuthorizedApp[]
}) => {
if (integration.installIdentificationMethod === 'secret_key_prefix') {
const prefix = integration.secretKeyPrefix
if (!prefix) return false
const clientId = integration.oauthClientId
if (!clientId) return false
return apiKeys.some((key) => key.type === 'secret' && key.name.startsWith(prefix))
}
if (integration.installIdentificationMethod === 'edge_function_secret_name') {
const secretName = integration.edgeFunctionSecretName
if (!secretName) return false
return secrets.some((secret) => secret.name === secretName)
}
return false
return authorizedApps.some((app) => app.client_id === clientId)
}
export const hasMatchingWrapper = ({ meta, wrappers }: { meta: WrapperMeta; wrappers: FDW[] }) => {
@@ -53,6 +53,7 @@ export const useAvailableIntegrations = () => {
installation_identification_method: installMethod,
secret_key_prefix: secretKeyPrefix,
edge_function_secret_name: edgeFunctionSecretName,
oauth_client_id: oauthClientId,
images,
content,
partner_name: authorName,
@@ -82,6 +83,7 @@ export const useAvailableIntegrations = () => {
installIdentificationMethod: installMethod ?? undefined,
secretKeyPrefix: secretKeyPrefix ?? undefined,
edgeFunctionSecretName: edgeFunctionSecretName ?? undefined,
oauthClientId: oauthClientId ?? undefined,
listingId: listingId ?? undefined,
author,
requiredExtensions: [],
@@ -7,16 +7,17 @@ import {
isStripeSyncEngineInstalled,
} from './Landing.utils'
import { useAvailableIntegrations } from './useAvailableIntegrations'
import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useSchemasQuery } from '@/data/database/schemas-query'
import { useFDWsQuery } from '@/data/fdw/fdws-query'
import { useSecretsQuery } from '@/data/secrets/secrets-query'
import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { EMPTY_ARR } from '@/lib/void'
export const useInstalledIntegrations = () => {
const { data: project } = useSelectedProjectQuery()
const { data: organization } = useSelectedOrganizationQuery()
const {
data: allIntegrations = EMPTY_ARR,
@@ -26,44 +27,21 @@ export const useInstalledIntegrations = () => {
isError: isErrorAvailableIntegrations,
} = useAvailableIntegrations()
const hasSecretKeyPrefixIntegration = useMemo(() => {
const hasOAuthIntegration = useMemo(() => {
return allIntegrations.some(
(integration) =>
integration.type === 'oauth' &&
integration.installIdentificationMethod === 'secret_key_prefix' &&
!!integration.secretKeyPrefix
)
}, [allIntegrations])
const hasEdgeFunctionSecretNameIntegration = useMemo(() => {
return allIntegrations.some(
(integration) =>
integration.type === 'oauth' &&
integration.installIdentificationMethod === 'edge_function_secret_name' &&
!!integration.edgeFunctionSecretName
(integration) => integration.type === 'oauth' && !!integration.oauthClientId
)
}, [allIntegrations])
const {
data: apiKeys = EMPTY_ARR,
error: apiKeysError,
isError: isErrorApiKeys,
isLoading: isApiKeysLoading,
isSuccess: isSuccessApiKeys,
} = useAPIKeysQuery(
{ projectRef: project?.ref, reveal: false },
{ enabled: hasSecretKeyPrefixIntegration }
)
const {
data: edgeFunctionSecrets = EMPTY_ARR,
error: edgeFunctionSecretsError,
isError: isErrorEdgeFunctionSecrets,
isLoading: isEdgeFunctionSecretsLoading,
isSuccess: isSuccessEdgeFunctionSecrets,
} = useSecretsQuery(
{ projectRef: project?.ref },
{ enabled: hasEdgeFunctionSecretNameIntegration }
data: authorizedApps = EMPTY_ARR,
error: authorizedAppsError,
isError: isErrorAuthorizedApps,
isLoading: isAuthorizedAppsLoading,
isSuccess: isSuccessAuthorizedApps,
} = useAuthorizedAppsQuery(
{ slug: organization?.slug },
{ enabled: hasOAuthIntegration && !!organization?.slug }
)
const {
@@ -115,41 +93,37 @@ export const useInstalledIntegrations = () => {
return hasRequiredExtensions({ integration, extensions })
}
if (integration.type === 'oauth') {
return isOAuthInstalled({ integration, apiKeys, secrets: edgeFunctionSecrets })
return isOAuthInstalled({ integration, authorizedApps })
}
return false
})
.sort((a, b) => a.name.localeCompare(b.name))
}, [allIntegrations, wrappers, extensions, schemas, isHooksEnabled, apiKeys, edgeFunctionSecrets])
}, [allIntegrations, wrappers, extensions, schemas, isHooksEnabled, authorizedApps])
const error =
fdwError ||
extensionsError ||
schemasError ||
availableIntegrationsError ||
(hasSecretKeyPrefixIntegration ? apiKeysError : null) ||
(hasEdgeFunctionSecretNameIntegration ? edgeFunctionSecretsError : null)
(hasOAuthIntegration ? authorizedAppsError : null)
const isLoading =
isSchemasLoading ||
isFDWLoading ||
isExtensionsLoading ||
isAvailableIntegrationsLoading ||
(hasSecretKeyPrefixIntegration && isApiKeysLoading) ||
(hasEdgeFunctionSecretNameIntegration && isEdgeFunctionSecretsLoading)
(hasOAuthIntegration && isAuthorizedAppsLoading)
const isError =
isErrorFDWs ||
isErrorExtensions ||
isErrorSchemas ||
isErrorAvailableIntegrations ||
(hasSecretKeyPrefixIntegration && isErrorApiKeys) ||
(hasEdgeFunctionSecretNameIntegration && isErrorEdgeFunctionSecrets)
(hasOAuthIntegration && isErrorAuthorizedApps)
const isSuccess =
isSuccessFDWs &&
isSuccessExtensions &&
isSuccessSchemas &&
isSuccessAvailableIntegrations &&
(!hasSecretKeyPrefixIntegration || isSuccessApiKeys) &&
(!hasEdgeFunctionSecretNameIntegration || isSuccessEdgeFunctionSecrets)
(!hasOAuthIntegration || isSuccessAuthorizedApps)
return {
// show all integrations at once instead of showing partial results
@@ -23,7 +23,11 @@ export const MarketplaceCard = ({ integration, isInstalled }: MarketplaceCardPro
<Card className="flex min-h-[168px] h-full flex-col gap-2.5 hover:border-stronger p-4">
<div className="flex items-start justify-between">
<IntegrationLogo integration={integration} size="h-9 w-9" />
{isInstalled && <Badge variant="success">Installed</Badge>}
{isInstalled && (
<Badge variant="success">
{integration.type === 'oauth' ? 'Connected' : 'Installed'}
</Badge>
)}
</div>
<div>
<div className="mb-1 text-sm font-medium">{integration.name}</div>
@@ -89,6 +89,7 @@ export const MarketplaceDetail = () => {
<MarketplaceDetailTopBar
title={integration.name}
isInstalled={isInstalled}
installedLabel={integration.type === 'oauth' ? 'Connected' : 'Installed'}
actions={
<>
{integration.docsUrl && (
@@ -79,7 +79,12 @@ export const MarketplaceDetailRail = ({ integration, isInstalled }: MarketplaceD
<RailGroup title="About">
<RailRow label="Type" value={typeLabel} />
<RailRow label="Built by" value={integration.author?.name || 'Supabase'} />
{isInstalled && <RailRow label="Status" value="Installed" />}
{isInstalled && (
<RailRow
label="Status"
value={integration.type === 'oauth' ? 'Connected' : 'Installed'}
/>
)}
</RailGroup>
{(docsUrl || siteUrl) && (
@@ -14,11 +14,13 @@ interface MarketplaceDetailTopBarProps {
title: string
actions?: ReactNode
isInstalled?: boolean
installedLabel?: string
}
export const MarketplaceDetailTopBar = ({
title,
isInstalled,
installedLabel = 'Installed',
actions,
}: MarketplaceDetailTopBarProps) => {
const { ref } = useParams()
@@ -35,7 +37,7 @@ export const MarketplaceDetailTopBar = ({
<BreadcrumbSeparator />
<BreadcrumbItem className="min-w-0">
<BreadcrumbPage className="block min-w-0 truncate">{title}</BreadcrumbPage>
{isInstalled && <Badge variant="success">Installed</Badge>}
{isInstalled && <Badge variant="success">{installedLabel}</Badge>}
</BreadcrumbItem>
</BreadcrumbList>
</div>
@@ -132,7 +132,7 @@ export const MarketplaceFeaturedHero = ({
</div>
{isActiveInstalled && (
<Badge variant="success" className="mt-2 @lg:mt-0 px-1.5 py-0 text-[10px]">
Installed
{active.type === 'oauth' ? 'Connected' : 'Installed'}
</Badge>
)}
</div>
@@ -253,7 +253,7 @@ export const MarketplaceIndex = () => {
<>
{featured.length > 0 && (
<MarketplaceFeaturedHero
integrations={featured}
integrations={featured.slice(0, 1)}
installedIds={installedIds}
categoryOptions={categoryOptions}
/>
@@ -67,7 +67,11 @@ export const MarketplaceListRow = ({ integration, isInstalled }: MarketplaceList
</TableCell>
<TableCell className="w-24">
{isInstalled ? <Badge variant="success">Installed</Badge> : null}
{isInstalled ? (
<Badge variant="success">
{integration.type === 'oauth' ? 'Connected' : 'Installed'}
</Badge>
) : null}
</TableCell>
</TableRow>
)
@@ -11,6 +11,7 @@ export type AuthorizedAppsVariables = {
export type AuthorizedApp = {
id: string
app_id: string
client_id: string | null
icon: string | null
name: string
website: string
+1
View File
@@ -30,6 +30,7 @@ export type Database = MergeDeep<
website_url: string
documentation_url: string
listing_logo: string
oauth_client_id: string | null
}
}
}