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} /> ) : (