diff --git a/apps/desktop/e2e/source-connect.spec.ts b/apps/desktop/e2e/source-connect.spec.ts
index 27d701ff0cd..cae4b8f02be 100644
--- a/apps/desktop/e2e/source-connect.spec.ts
+++ b/apps/desktop/e2e/source-connect.spec.ts
@@ -110,7 +110,9 @@ test('source authorization returns to its desktop screen and refreshes live', as
const ok = attempts.get(state) === session && url.searchParams.has('code')
attempts.delete(state)
if (ok) installed = true
- redirect(`/credential-groups/slack-complete?state=${state}&ok=${ok}`)
+ const reason =
+ url.searchParams.get('error') === 'session_expired' ? '&reason=signin_required' : ''
+ redirect(`/credential-groups/slack-complete?state=${state}&ok=${ok}${reason}`)
return
}
if (
@@ -224,7 +226,7 @@ test('source authorization returns to its desktop screen and refreshes live', as
if (path === '/provider') {
const state = url.searchParams.get('state') ?? ''
response.end(
- `AuthorizeCancel`
+ `AuthorizeCancelSession expired`
)
return
}
@@ -389,6 +391,15 @@ test('source authorization returns to its desktop screen and refreshes live', as
await expect(page.getByLabel('Enrollment error')).toHaveText('')
expect(page.url()).toBe(`${origin}/home`)
})
+ await check('browser sign-in failures retain recovery guidance on desktop', async () => {
+ await page.getByRole('button', { name: 'Connect Slack' }).click()
+ await expect.poll(async () => (await opened()).length).toBe(8)
+ await external.goto((await opened())[7])
+ await external.getByRole('link', { name: 'Session expired' }).click()
+ await expect(page.getByLabel('Connection')).toHaveText('error')
+ await expect(page.getByRole('alert')).toContainText('Sign in to Sim in your browser')
+ expect(page.url()).toBe(`${origin}/home`)
+ })
await page.screenshot({ path: test.info().outputPath('source-connect-desktop.png') })
} finally {
mkdirSync(dirname(reportPath), { recursive: true })
diff --git a/apps/sim/app/api/knowledge/slack/oauth/callback/route.test.ts b/apps/sim/app/api/knowledge/slack/oauth/callback/route.test.ts
index 055aa4daaf0..e70ab6b34d8 100644
--- a/apps/sim/app/api/knowledge/slack/oauth/callback/route.test.ts
+++ b/apps/sim/app/api/knowledge/slack/oauth/callback/route.test.ts
@@ -87,7 +87,7 @@ describe('Slack OAuth callback', () => {
const response = await GET(request('state=state&code=code'))
expect(response.status).toBe(303)
expect(response.headers.get('location')).toBe(
- 'https://www.sim.ai/credential-groups/slack-complete?state=state&ok=false'
+ 'https://www.sim.ai/credential-groups/slack-complete?state=state&ok=false&reason=signin_required'
)
expect(m.authenticate).not.toHaveBeenCalled()
expect(m.complete).not.toHaveBeenCalled()
diff --git a/apps/sim/app/api/knowledge/slack/oauth/callback/route.ts b/apps/sim/app/api/knowledge/slack/oauth/callback/route.ts
index 24b5a5bc9e0..3b7c0983c9f 100644
--- a/apps/sim/app/api/knowledge/slack/oauth/callback/route.ts
+++ b/apps/sim/app/api/knowledge/slack/oauth/callback/route.ts
@@ -77,6 +77,8 @@ export const GET = withRouteHandler(async (request) => {
const url = new URL('/credential-groups/slack-complete', getBaseUrl())
url.searchParams.set('state', callbackState)
url.searchParams.set('ok', 'false')
+ if (error instanceof InternalUnauthenticatedError)
+ url.searchParams.set('reason', 'signin_required')
return NextResponse.redirect(url, {
status: 303,
headers: { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' },
diff --git a/apps/sim/app/credential-groups/complete/completion-handoff.tsx b/apps/sim/app/credential-groups/complete/completion-handoff.tsx
index cb61d3e275e..f4baa4dcd7c 100644
--- a/apps/sim/app/credential-groups/complete/completion-handoff.tsx
+++ b/apps/sim/app/credential-groups/complete/completion-handoff.tsx
@@ -17,7 +17,7 @@ export function CredentialGroupCompletionHandoff({
completionId,
failure,
}: CredentialGroupCompletionHandoffProps) {
- const started = useRef(false)
+ const started = useRef(false)
useEffect(() => {
if (started.current) return
started.current = true
diff --git a/apps/sim/app/credential-groups/slack-complete/page.tsx b/apps/sim/app/credential-groups/slack-complete/page.tsx
index 3389f1abfaf..7dc492e13fb 100644
--- a/apps/sim/app/credential-groups/slack-complete/page.tsx
+++ b/apps/sim/app/credential-groups/slack-complete/page.tsx
@@ -17,6 +17,7 @@ export default async function SlackCompletePage({ searchParams }: SlackCompleteP
const scalar = (key: string) =>
typeof params[key] === 'string' && params[key].length <= 512 ? params[key] : undefined
const ok = params.ok === 'true'
+ const signInRequired = !ok && params.reason === 'signin_required'
const mode = params.mode === 'managed' ? 'managed' : 'search'
const organizationId = scalar('organizationId')
return (
@@ -25,7 +26,9 @@ export default async function SlackCompletePage({ searchParams }: SlackCompleteP
description={
ok
? 'Your connection is ready. You can return to Sim.'
- : 'Authorization did not complete. Return to Sim and try connecting again.'
+ : signInRequired
+ ? 'Sign in to Sim in your browser, then return to Sim and restart Slack setup.'
+ : 'Authorization did not complete. Return to Sim and try connecting again.'
}
>
- Return to Sim
+ {signInRequired ? 'Sign in to Sim' : 'Return to Sim'}
)
diff --git a/apps/sim/app/credential-groups/slack-complete/slack-completion.tsx b/apps/sim/app/credential-groups/slack-complete/slack-completion.tsx
index 87f8ec9bc0f..bc64579622c 100644
--- a/apps/sim/app/credential-groups/slack-complete/slack-completion.tsx
+++ b/apps/sim/app/credential-groups/slack-complete/slack-completion.tsx
@@ -23,7 +23,7 @@ export function SlackCompletion({
credentialGroupId,
slackBotCredentialId,
}: SlackCompletionProps) {
- const started = useRef(false)
+ const started = useRef(false)
useEffect(() => {
if (started.current || !state) return
started.current = true
diff --git a/apps/sim/app/desktop/connect/source-connect-launcher.tsx b/apps/sim/app/desktop/connect/source-connect-launcher.tsx
index e95a1dd549a..b12cd7ad3ff 100644
--- a/apps/sim/app/desktop/connect/source-connect-launcher.tsx
+++ b/apps/sim/app/desktop/connect/source-connect-launcher.tsx
@@ -14,7 +14,7 @@ interface SourceConnectLauncherProps {
}
export function SourceConnectLauncher({ requestId, state, port }: SourceConnectLauncherProps) {
- const started = useRef(false)
+ const started = useRef(false)
const [error, setError] = useState(null)
useEffect(() => {
if (started.current) return
diff --git a/apps/sim/lib/desktop/source-browser.ts b/apps/sim/lib/desktop/source-browser.ts
index 4f787a69f7f..efe9a127b83 100644
--- a/apps/sim/lib/desktop/source-browser.ts
+++ b/apps/sim/lib/desktop/source-browser.ts
@@ -180,7 +180,11 @@ export async function finishDesktopSourceBrowser(
return false
sessionStorage.removeItem(STORAGE_KEY)
const url = new URL(buildConnectCompletePath(context.state, context.port), window.location.origin)
- if (completion.error) url.searchParams.set('error', 'connection_failed')
+ if (completion.error)
+ url.searchParams.set(
+ 'error',
+ completion.error === 'signin_required' ? 'signin_required' : 'connection_failed'
+ )
else if (context.github) {
try {
const result = await requestJson(readGitHubSearchSetupContract, { query: context.github })
diff --git a/apps/sim/lib/desktop/source-connect.ts b/apps/sim/lib/desktop/source-connect.ts
index 9d245b4039d..2a23c63c963 100644
--- a/apps/sim/lib/desktop/source-connect.ts
+++ b/apps/sim/lib/desktop/source-connect.ts
@@ -40,7 +40,9 @@ export async function connectDesktopSource(
new Error(
result?.error === 'cancelled' || result?.error === 'superseded'
? 'Connection canceled. You can try again.'
- : 'Connection did not complete. Try connecting again.'
+ : result?.error === 'signin_required'
+ ? 'Sign in to Sim in your browser, then try connecting again.'
+ : 'Connection did not complete. Try connecting again.'
)
)
}
diff --git a/apps/sim/scripts/fixtures/desktop-source-connect.tsx b/apps/sim/scripts/fixtures/desktop-source-connect.tsx
index 875ca8c3742..52028aded79 100644
--- a/apps/sim/scripts/fixtures/desktop-source-connect.tsx
+++ b/apps/sim/scripts/fixtures/desktop-source-connect.tsx
@@ -65,7 +65,7 @@ function SourceConnectFixture() {
}
function BrowserLauncher() {
- const started = useRef(false)
+ const started = useRef(false)
const [error, setError] = useState('')
useEffect(() => {
if (started.current) return
@@ -99,6 +99,7 @@ const content =
mode='search'
ok={params.get('ok') === 'true'}
state={params.get('state') ?? undefined}
+ reason={params.get('reason') ?? undefined}
/>
) : (