-
manifest.json
-
+
+
+ void copyManifest()}
+ >
+ Copy manifest
+
+ {showCopied && (
+
+ Manifest copied
+
+ )}
-
+ {copyFailed && (
+
+ Could not copy the manifest. Allow clipboard access and try again.
+
+ )}
+
+
+ View manifest
+
+
+
+
+
)
}
diff --git a/apps/sim/components/integrations/slack-search-setup-wizard.tsx b/apps/sim/components/integrations/slack-search-setup-wizard.tsx
index bbc10320c88..bc2f5ed4756 100644
--- a/apps/sim/components/integrations/slack-search-setup-wizard.tsx
+++ b/apps/sim/components/integrations/slack-search-setup-wizard.tsx
@@ -9,9 +9,9 @@ import {
ChipModalField,
ChipModalFooter,
ChipModalHeader,
- writeTextToClipboard,
} from '@sim/emcn'
import { SlackIcon } from '@/components/icons'
+import { SlackAppManifest } from '@/components/integrations/slack-app-manifest'
import {
SLACK_SEARCH_DEFAULT_DESCRIPTION,
SLACK_SEARCH_DEFAULT_NAME,
@@ -50,25 +50,10 @@ export function SlackSearchSetupWizard({
const [clientSecret, setClientSecret] = useState('')
const [signingSecret, setSigningSecret] = useState('')
const [botToken, setBotToken] = useState('')
- const [configurationCopied, setConfigurationCopied] = useState(false)
- const [copyError, setCopyError] = useState
(null)
- const error = prepare.error ?? oauth.error ?? connect.error ?? copyError
+ const error = prepare.error ?? oauth.error ?? connect.error
const busy = oauth.isPending || connect.isPending
const configuredAppId = appId ?? prepare.data?.existingApp?.appId
- async function copyConfiguration() {
- if (!prepare.data) throw new Error('Slack app configuration is not ready')
- setCopyError(null)
- try {
- await writeTextToClipboard(prepare.data.manifest)
- setConfigurationCopied(true)
- } catch {
- setCopyError(
- new Error('Could not copy the app configuration. Allow clipboard access and try again.')
- )
- }
- }
-
const shared = mode
? mode === 'shared'
: Boolean(
@@ -87,9 +72,9 @@ export function SlackSearchSetupWizard({
function advance() {
if (step === 'manifest') {
- setStep('credentials')
- } else if (step === 'credentials') {
setStep('token')
+ } else if (step === 'token') {
+ setStep('credentials')
} else {
connect.mutate(
{
@@ -203,7 +188,7 @@ export function SlackSearchSetupWizard({
: 'Create Slack app'
: step === 'credentials'
? 'Slack app credentials'
- : 'Connect installed Slack app'
+ : 'Install Slack app'
return (
{step === 'manifest' && (
-
- {configuredAppId
- ? configurationCopied
- ? 'Configuration copied. In Slack, replace the JSON under App Manifest and save.'
- : 'Copy the configuration, then replace the JSON under App Manifest in Slack.'
- : 'Create and install the app in Slack, then return here to add its credentials.'}
-
+
+
+
+ {configuredAppId
+ ? 'In your Slack app, open App Manifest, replace the JSON, and save changes.'
+ : 'Create Slack app opens Slack with this manifest already filled in. Select your workspace, review the configuration, and click Create.'}
+
+ {configuredAppId && (
+
+ Open app settings
+
+ )}
+
)}
{step === 'credentials' && (
<>
- Find these values under Basic Information in your Slack app.
+ Open Basic Information → App Credentials in the same Slack app. Copy these values,
+ then connect the app to Sim.
- Copy the Bot User OAuth Token from OAuth & Permissions in your installed Slack
- app. If Slack requests updated permissions, approve them there first.
+ Open OAuth & Permissions in Slack, choose Install to Workspace (or Reinstall to
+ Workspace), and approve access. Then copy the Bot User OAuth Token below.
void copyConfiguration() }]
- : [
- {
- custom: (
-
- {configuredAppId ? 'Open app settings' : 'Create app'}
-
- ),
- },
- ]
- : undefined
+ : undefined
}
primaryAdjacentAction={
step === 'manifest'
@@ -334,21 +315,20 @@ export function SlackSearchSetupWizard({
onClick: () => {
oauth.reset()
connect.reset()
- setStep(step === 'token' ? 'credentials' : 'manifest')
+ setStep(step === 'credentials' ? 'token' : 'manifest')
},
}
}
primaryAction={{
- label: busy ? 'Connecting…' : step === 'token' ? 'Connect app' : 'Continue',
+ label: busy ? 'Connecting…' : step === 'credentials' ? 'Connect app' : 'Continue',
onClick: advance,
disabled:
busy ||
- (step === 'manifest'
- ? Boolean(configuredAppId && !configurationCopied)
- : step === 'token'
- ? !botToken.trim()
- : !installationId &&
- (!clientId.trim() || !clientSecret.trim() || !signingSecret.trim())),
+ Boolean(prepare.error) ||
+ (step === 'token' && !botToken.trim()) ||
+ (step === 'credentials' &&
+ !installationId &&
+ (!clientId.trim() || !clientSecret.trim() || !signingSecret.trim())),
}}
/>
diff --git a/apps/sim/lib/api/contracts/knowledge/slack.ts b/apps/sim/lib/api/contracts/knowledge/slack.ts
index 6dd0bdb8885..ceaa3be8deb 100644
--- a/apps/sim/lib/api/contracts/knowledge/slack.ts
+++ b/apps/sim/lib/api/contracts/knowledge/slack.ts
@@ -1,6 +1,7 @@
import { z } from 'zod'
import { organizationIdSchema } from '@/lib/api/contracts/primitives'
import { defineRouteContract } from '@/lib/api/contracts/types'
+import { SLACK_APP_CREATION_URL_MAX_LENGTH } from '@/lib/integrations/slack-manifest'
export const slackSearchOrganizationQuerySchema = z.object({ organizationId: organizationIdSchema })
export const slackSearchInstallationSchema = z.object({
@@ -71,7 +72,7 @@ export const prepareSlackSearchContract = defineRouteContract({
existingApp: z
.object({ appId: z.string().min(1).max(200), teamId: z.string().min(1).max(200) })
.nullable(),
- createAppUrl: z.string().url().max(30_000),
+ createAppUrl: z.string().url().max(SLACK_APP_CREATION_URL_MAX_LENGTH),
}),
},
})
diff --git a/apps/sim/lib/integrations/slack-manifest.ts b/apps/sim/lib/integrations/slack-manifest.ts
new file mode 100644
index 00000000000..25e0ad65c08
--- /dev/null
+++ b/apps/sim/lib/integrations/slack-manifest.ts
@@ -0,0 +1,20 @@
+const SLACK_APP_NAME_MAX_LENGTH = 35
+export const SLACK_APP_CREATION_URL_MAX_LENGTH = 30_000
+
+/** Opens Slack's app creation flow with the generated manifest already filled in. */
+export function buildSlackAppCreationUrl(manifest: string): string {
+ return `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(manifest)}`
+}
+
+/** Validates a name used for both the Slack app and its bot user. */
+export function getSlackAppNameError(name: string): string | null {
+ const trimmedName = name.trim()
+ if (!trimmedName) return 'Enter an app name.'
+ if (trimmedName.length > SLACK_APP_NAME_MAX_LENGTH) {
+ return `Use ${SLACK_APP_NAME_MAX_LENGTH} characters or fewer.`
+ }
+ if (!/^[a-zA-Z0-9 ._-]+$/.test(trimmedName)) {
+ return 'Use letters, numbers, spaces, periods, hyphens, or underscores.'
+ }
+ return null
+}
diff --git a/apps/sim/lib/knowledge/application/slack-search/setup.ts b/apps/sim/lib/knowledge/application/slack-search/setup.ts
index fcc83c4cb08..57890df1601 100644
--- a/apps/sim/lib/knowledge/application/slack-search/setup.ts
+++ b/apps/sim/lib/knowledge/application/slack-search/setup.ts
@@ -14,6 +14,7 @@ import {
} from '@/lib/credential-groups/organization-slack-app'
import { configureSharedSlackMemberApp } from '@/lib/credential-groups/shared-slack-app'
import type { DbOrTx } from '@/lib/db/types'
+import { buildSlackAppCreationUrl } from '@/lib/integrations/slack-manifest'
import {
exchangeSlackBotAuthorization,
revokeSlackBotAuthorization,
@@ -98,7 +99,7 @@ export const prepareSlackSearchSetup = defineAuthorizedKnowledgeUseCase({
sharedAppId: sharedApp?.id ?? null,
manifest: JSON.stringify(manifest, null, 2),
existingApp: member.app,
- createAppUrl: `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(JSON.stringify(manifest))}`,
+ createAppUrl: buildSlackAppCreationUrl(JSON.stringify(manifest)),
}
},
})