Skip to content

Commit 2e263af

Browse files
bchapuisclaude
andcommitted
Unify bot creation dialogs and detail pages across all providers
All four bot create dialogs (Slack, WhatsApp, Telegram, Discord) now follow a consistent pattern: dedicated name step, one credential step per external page, webhook configuration after creation, and setup. - Add dedicated name step to Slack, Telegram, and Discord dialogs - Add webhook step to WhatsApp dialog with Callback URL and Verify Token - Generate WhatsApp verifyToken at bot creation time (not trigger time) - Update WhatsApp webhook verification to fall back to bot metadata - Align button text, placeholders, spinner labels, and help copy - Condense Slack detail page setup instructions from 6 to 4 steps - Reorder WhatsApp detail page instructions (webhook config first) - Standardize link labels and trigger phrasing across detail pages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 51d8e9b commit 2e263af

10 files changed

Lines changed: 399 additions & 215 deletions

apps/api/src/routes/bots.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ function buildMetadata(
158158
return JSON.stringify({
159159
phoneNumberId: data.phoneNumberId,
160160
wabaId: data.wabaId ?? null,
161+
verifyToken: crypto.randomUUID(),
161162
});
162163
case "slack":
163164
return JSON.stringify({
@@ -430,7 +431,7 @@ botRoutes.get("/:id/webhook-info", async (c) => {
430431

431432
const apiHost = new URL(c.req.url).origin;
432433

433-
// Find verify token from trigger metadata
434+
// Find verify token from trigger metadata, then fall back to bot metadata
434435
const triggers = await getBotTriggersByBot(db, id);
435436
let verifyToken: string | null = null;
436437
for (const t of triggers) {
@@ -446,6 +447,12 @@ botRoutes.get("/:id/webhook-info", async (c) => {
446447
break;
447448
}
448449
}
450+
if (!verifyToken && bot.metadata) {
451+
const botMeta = JSON.parse(bot.metadata) as Record<string, string>;
452+
if (botMeta.verifyToken) {
453+
verifyToken = botMeta.verifyToken;
454+
}
455+
}
449456

450457
const response: GetBotWebhookInfoResponse = {
451458
webhookUrl: `${apiHost}/${bot.provider}/webhook/${id}`,

apps/api/src/routes/whatsapp-webhook.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,23 @@ whatsappWebhook.get("/webhook/:whatsappAccountId", async (c) => {
3333
}
3434

3535
const db = createDatabase(c.env.DB);
36-
// Find verify token from trigger metadata
36+
// Find verify token from trigger metadata, then fall back to bot metadata
3737
const triggers = await getBotTriggersByBot(db, whatsappAccountId);
38+
let expectedToken: string | undefined;
3839
const firstMeta = triggers[0]?.botTrigger.metadata
3940
? (JSON.parse(triggers[0].botTrigger.metadata) as {
4041
verifyToken?: string;
4142
})
4243
: null;
43-
const expectedToken = firstMeta?.verifyToken;
44+
expectedToken = firstMeta?.verifyToken;
45+
46+
if (!expectedToken) {
47+
const bot = await getBotById(db, whatsappAccountId);
48+
const botMeta = bot?.metadata
49+
? (JSON.parse(bot.metadata) as { verifyToken?: string })
50+
: null;
51+
expectedToken = botMeta?.verifyToken;
52+
}
4453

4554
if (!expectedToken || token !== expectedToken) {
4655
return c.json({ error: "Verification token mismatch" }, 403);

apps/app/src/components/workflow/widgets/input/discord-bot-create-dialog.tsx

Lines changed: 92 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,32 @@ import {
1212
import { Input } from "@/components/ui/input";
1313
import { Label } from "@/components/ui/label";
1414
import { Spinner } from "@/components/ui/spinner";
15+
import { getApiBaseUrl } from "@/config/api";
1516
import { createDiscordBot } from "@/services/bot-service";
1617

17-
import { DiscordBotSetupInfo } from "./discord-setup-info";
18+
import { CopyableValue } from "./copyable-value";
1819

19-
type Step = "application" | "bot-token" | "webhook" | "command" | "invite";
20+
type Step =
21+
| "name"
22+
| "application"
23+
| "bot-token"
24+
| "webhook"
25+
| "command"
26+
| "invite";
2027

2128
const STEP_TITLES: Record<Step, string> = {
22-
application: "Create a Discord Application",
29+
name: "Create a Discord Bot",
30+
application: "Application Info",
2331
"bot-token": "Bot Token",
2432
webhook: "Interactions Endpoint",
2533
command: "Slash Command",
2634
invite: "Add Bot to Server",
2735
};
2836

2937
const STEP_DESCRIPTIONS: Record<Step, string> = {
38+
name: "Choose a display name to identify this Discord bot in Dafthunk.",
3039
application:
31-
"Create a new application in the Discord Developer Portal, then copy the Application ID and Public Key from the General Information page.",
40+
"Copy the Application ID and Public Key from the General Information page in the Discord Developer Portal.",
3241
"bot-token":
3342
"Copy the token from the Bot page in the Discord Developer Portal.",
3443
webhook:
@@ -54,7 +63,7 @@ export function DiscordBotCreateDialog({
5463
showCommandStep = true,
5564
}: DiscordBotCreateDialogProps) {
5665
const { organization } = useAuth();
57-
const [step, setStep] = useState<Step>("application");
66+
const [step, setStep] = useState<Step>("name");
5867
const [name, setName] = useState("");
5968
const [applicationId, setApplicationId] = useState("");
6069
const [publicKey, setPublicKey] = useState("");
@@ -65,7 +74,7 @@ export function DiscordBotCreateDialog({
6574
const [createdBotId, setCreatedBotId] = useState<string | null>(null);
6675

6776
const resetForm = () => {
68-
setStep("application");
77+
setStep("name");
6978
setName("");
7079
setApplicationId("");
7180
setPublicKey("");
@@ -116,15 +125,14 @@ export function DiscordBotCreateDialog({
116125
? `https://discord.com/developers/applications/${applicationId}/bot`
117126
: "https://discord.com/developers/applications";
118127

128+
const webhookUrl = createdBotId
129+
? `${getApiBaseUrl().replace(/\/$/, "")}/discord/webhook/${createdBotId}`
130+
: "";
131+
119132
const inviteUrl = applicationId
120133
? `https://discord.com/oauth2/authorize?client_id=${applicationId}&scope=bot+applications.commands&permissions=2048`
121134
: null;
122135

123-
const canAdvanceToToken =
124-
name.trim() !== "" &&
125-
applicationId.trim() !== "" &&
126-
publicKey.trim() !== "";
127-
128136
return (
129137
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
130138
<DialogContent className="max-w-[450px]">
@@ -134,7 +142,7 @@ export function DiscordBotCreateDialog({
134142
</DialogTitle>
135143
<DialogDescription className="text-sm text-muted-foreground mt-1">
136144
{STEP_DESCRIPTIONS[step]}
137-
{step === "application" && (
145+
{(step === "application" || step === "webhook") && (
138146
<>
139147
{" "}
140148
<a
@@ -148,10 +156,24 @@ export function DiscordBotCreateDialog({
148156
</a>
149157
</>
150158
)}
159+
{step === "bot-token" && (
160+
<>
161+
{" "}
162+
<a
163+
href={botSettingsUrl}
164+
target="_blank"
165+
rel="noopener noreferrer"
166+
className="text-primary hover:underline inline-flex items-center gap-0.5"
167+
>
168+
Open Discord Developer Portal
169+
<ExternalLink className="w-2.5 h-2.5" />
170+
</a>
171+
</>
172+
)}
151173
</DialogDescription>
152174
</div>
153175

154-
{step === "application" && (
176+
{step === "name" && (
155177
<div className="space-y-3">
156178
<div className="space-y-1.5">
157179
<Label htmlFor="discord-name">Name</Label>
@@ -162,10 +184,27 @@ export function DiscordBotCreateDialog({
162184
placeholder="My Discord Bot"
163185
/>
164186
<p className="text-xs text-muted-foreground">
165-
A display name for this bot in Dafthunk.
187+
A display name for this bot in Dafthunk. This is not visible to
188+
your Discord users.
166189
</p>
167190
</div>
168191

192+
<div className="flex justify-end gap-2 pt-1">
193+
<Button type="button" variant="outline" onClick={handleClose}>
194+
Cancel
195+
</Button>
196+
<Button
197+
onClick={() => setStep("application")}
198+
disabled={name.trim() === ""}
199+
>
200+
Next
201+
</Button>
202+
</div>
203+
</div>
204+
)}
205+
206+
{step === "application" && (
207+
<div className="space-y-3">
169208
<div className="space-y-1.5">
170209
<Label htmlFor="discord-app-id">Application ID</Label>
171210
<Input
@@ -176,15 +215,9 @@ export function DiscordBotCreateDialog({
176215
/>
177216
<p className="text-xs text-muted-foreground">
178217
Copy from the{" "}
179-
<a
180-
href={generalInfoUrl}
181-
target="_blank"
182-
rel="noopener noreferrer"
183-
className="text-primary hover:underline inline-flex items-center gap-0.5"
184-
>
218+
<span className="font-medium text-foreground">
185219
General Information
186-
<ExternalLink className="w-2.5 h-2.5" />
187-
</a>{" "}
220+
</span>{" "}
188221
page in the Discord Developer Portal.
189222
</p>
190223
</div>
@@ -199,26 +232,26 @@ export function DiscordBotCreateDialog({
199232
/>
200233
<p className="text-xs text-muted-foreground">
201234
Copy from the same{" "}
202-
<a
203-
href={generalInfoUrl}
204-
target="_blank"
205-
rel="noopener noreferrer"
206-
className="text-primary hover:underline inline-flex items-center gap-0.5"
207-
>
235+
<span className="font-medium text-foreground">
208236
General Information
209-
<ExternalLink className="w-2.5 h-2.5" />
210-
</a>{" "}
237+
</span>{" "}
211238
page. Used to verify interaction signatures.
212239
</p>
213240
</div>
214241

215242
<div className="flex justify-end gap-2 pt-1">
216-
<Button type="button" variant="outline" onClick={handleClose}>
217-
Cancel
243+
<Button
244+
type="button"
245+
variant="outline"
246+
onClick={() => setStep("name")}
247+
>
248+
Back
218249
</Button>
219250
<Button
220251
onClick={() => setStep("bot-token")}
221-
disabled={!canAdvanceToToken}
252+
disabled={
253+
applicationId.trim() === "" || publicKey.trim() === ""
254+
}
222255
>
223256
Next
224257
</Button>
@@ -235,20 +268,12 @@ export function DiscordBotCreateDialog({
235268
type="password"
236269
value={botToken}
237270
onChange={(e) => setBotToken(e.target.value)}
238-
placeholder="••••••••"
271+
placeholder="Paste your bot token here"
239272
/>
240273
<p className="text-xs text-muted-foreground">
241274
Copy the token from the{" "}
242-
<a
243-
href={botSettingsUrl}
244-
target="_blank"
245-
rel="noopener noreferrer"
246-
className="text-primary hover:underline inline-flex items-center gap-0.5"
247-
>
248-
Bot
249-
<ExternalLink className="w-2.5 h-2.5" />
250-
</a>{" "}
251-
page in the Discord Developer Portal.
275+
<span className="font-medium text-foreground">Bot</span> page in
276+
the Discord Developer Portal.
252277
</p>
253278
</div>
254279

@@ -277,10 +302,10 @@ export function DiscordBotCreateDialog({
277302
{isSubmitting ? (
278303
<>
279304
<Spinner className="h-4 w-4 mr-1" />
280-
Creating...
305+
Connecting...
281306
</>
282307
) : (
283-
"Create Bot"
308+
"Next"
284309
)}
285310
</Button>
286311
</div>
@@ -296,12 +321,27 @@ export function DiscordBotCreateDialog({
296321
<span className="font-medium">{name}</span>
297322
</div>
298323

299-
{createdBotId && (
300-
<DiscordBotSetupInfo
301-
botId={createdBotId}
302-
applicationId={applicationId}
303-
/>
304-
)}
324+
<div className="space-y-2 text-sm">
325+
<div className="space-y-1">
326+
<p className="font-medium text-foreground">
327+
Interactions Endpoint URL
328+
</p>
329+
<CopyableValue value={webhookUrl} />
330+
<p className="text-muted-foreground text-xs">
331+
Paste this as the Interactions Endpoint URL in the{" "}
332+
<a
333+
href={generalInfoUrl}
334+
target="_blank"
335+
rel="noopener noreferrer"
336+
className="text-primary hover:underline inline-flex items-center gap-0.5"
337+
>
338+
General Information
339+
<ExternalLink className="w-2.5 h-2.5" />
340+
</a>{" "}
341+
page of your Discord application.
342+
</p>
343+
</div>
344+
</div>
305345

306346
<div className="flex justify-end">
307347
<Button

0 commit comments

Comments
 (0)