Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/docs/providers/communications/slack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,17 @@ adds or updates Roomote's required capabilities, scopes, events, redirect URLs,
and callback URLs, validates the result, and then applies it. The token is not
stored.

Roomote records the Slack integration version separately from the Roomote
release. Installations created before version tracking have an unknown version
and show **Upgrade available** until Roomote can confirm their manifest. Running
**Upgrade app** clears the indicator immediately when the exported manifest is
already current or when Slack applies an update without new permissions.

If the update changes permissions, Roomote shows **Reinstall in Slack**. Finish
that approval step before testing the new capabilities because Slack does not
add scopes to existing installations automatically.
that approval step to clear the upgrade indicator and before testing the new
capabilities because Slack does not add scopes to existing installations
automatically. Saving credentials or validating a configuration token does not
mark the installation current.

### Alternative: env vars or an existing app

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 29 additions & 11 deletions apps/web/src/components/settings/SlackManifestUpdateDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
'use client';

import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { isSlackManifestUpgradeRequired } from '@roomote/types';

import { useTRPC } from '@/trpc/client';
import { useConnectSlack, useSlackInstallation } from '@/hooks/slack';
import { SETTINGS_PATHS } from '@/lib/settings';
import {
Button,
Badge,
Dialog,
DialogContent,
DialogDescription,
Expand All @@ -24,6 +26,7 @@ import {

export function SlackManifestUpdateDialog() {
const trpc = useTRPC();
const queryClient = useQueryClient();
const slackInstallation = useSlackInstallation();
const connectSlack = useConnectSlack(SETTINGS_PATHS.comms);
const [open, setOpen] = useState(false);
Expand All @@ -32,14 +35,18 @@ export function SlackManifestUpdateDialog() {

const updateManifest = useMutation(
trpc.slack.updateAppManifest.mutationOptions({
onSuccess: (result) => {
onSuccess: async (result) => {
setConfigToken('');

if (!result.success) {
toast.error(result.error);
return;
}

await queryClient.invalidateQueries({
queryKey: trpc.slack.installation.queryKey(),
});

if (!result.changed) {
toast.success('Slack app is already up to date');
setOpen(false);
Expand All @@ -63,6 +70,10 @@ export function SlackManifestUpdateDialog() {
return null;
}

const upgradeRequired = isSlackManifestUpgradeRequired(
slackInstallation.data.manifestVersion,
);

const handleOpenChange = (nextOpen: boolean) => {
if (updateManifest.isPending || connectSlack.isPending) return;
setOpen(nextOpen);
Expand All @@ -83,15 +94,22 @@ export function SlackManifestUpdateDialog() {

return (
<>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setOpen(true)}
>
<RefreshCw />
Update app
</Button>
<div className="flex items-center gap-2">
{upgradeRequired ? (
<Badge variant="warning" className="hidden sm:inline-flex">
Upgrade available
</Badge>
) : null}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setOpen(true)}
>
<RefreshCw />
{upgradeRequired ? 'Upgrade app' : 'Update app'}
</Button>
</div>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent size="md">
<DialogHeader>
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/lib/server/__tests__/slack-oauth-state.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions apps/web/src/lib/server/slack-oauth-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type SignedSlackInstallStatePayload = {
version: typeof SIGNED_STATE_VERSION;
mode: 'install';
redirectPath: string;
manifestAppId?: string;
manifestVersion?: number;
issuedAt: number;
};

Expand All @@ -25,6 +27,8 @@ type DecodedSlackOAuthState =
| {
mode: 'install';
redirectPath: string;
manifestAppId?: string;
manifestVersion?: number;
}
| {
mode: 'link_account';
Expand Down Expand Up @@ -82,13 +86,20 @@ async function createSignedSlackState(

export async function createSignedSlackInstallState({
redirectPath,
manifestAppId,
manifestVersion,
}: {
redirectPath?: string | null;
manifestAppId?: string;
manifestVersion?: number;
}): Promise<string> {
return createSignedSlackState({
version: SIGNED_STATE_VERSION,
mode: 'install',
redirectPath: normalizeSlackOAuthRedirectPath(redirectPath),
...(manifestAppId && manifestVersion
? { manifestAppId, manifestVersion }
: {}),
issuedAt: Date.now(),
} satisfies SignedSlackInstallStatePayload);
}
Expand Down Expand Up @@ -155,9 +166,25 @@ async function decodeSignedSlackState(
}

if (payload.mode === 'install') {
const manifestAppId = payload.manifestAppId;
const manifestVersion = payload.manifestVersion;
if (
(manifestAppId !== undefined || manifestVersion !== undefined) &&
(typeof manifestAppId !== 'string' ||
manifestAppId.length === 0 ||
typeof manifestVersion !== 'number' ||
!Number.isInteger(manifestVersion) ||
manifestVersion < 1)
) {
return null;
}

return {
mode: 'install',
redirectPath,
...(manifestAppId && manifestVersion
? { manifestAppId, manifestVersion }
: {}),
};
}

Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/lib/slack-app-manifest.client.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading