diff --git a/apps/dashboard/src/@/api/insight/webhooks.ts b/apps/dashboard/src/@/api/insight/webhooks.ts new file mode 100644 index 00000000000..d75c27eb572 --- /dev/null +++ b/apps/dashboard/src/@/api/insight/webhooks.ts @@ -0,0 +1,157 @@ +"use server"; + +import { getAuthToken } from "app/(app)/api/lib/getAuthToken"; +import { THIRDWEB_INSIGHT_API_DOMAIN } from "constants/urls"; + +export interface WebhookResponse { + id: string; + name: string; + team_id: string; + project_id: string; + webhook_url: string; + webhook_secret: string; + filters: WebhookFilters; + suspended_at: string | null; + suspended_reason: string | null; + disabled: boolean; + created_at: string; + updated_at: string | null; +} + +export interface WebhookFilters { + "v1.events"?: { + chain_ids?: string[]; + addresses?: string[]; + signatures?: Array<{ + sig_hash: string; + abi?: string; + params?: Record; + }>; + }; + "v1.transactions"?: { + chain_ids?: string[]; + from_addresses?: string[]; + to_addresses?: string[]; + signatures?: Array<{ + sig_hash: string; + abi?: string; + params?: string[]; + }>; + }; +} + +interface CreateWebhookPayload { + name: string; + webhook_url: string; + filters: WebhookFilters; +} + +interface WebhooksListResponse { + data: WebhookResponse[]; +} + +interface WebhookSingleResponse { + data: WebhookResponse; +} + +interface TestWebhookPayload { + webhook_url: string; + type?: "event" | "transaction"; +} + +interface TestWebhookResponse { + success: boolean; +} + +export async function createWebhook( + payload: CreateWebhookPayload, + clientId: string, +): Promise { + const authToken = await getAuthToken(); + const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-client-id": clientId, + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to create webhook: ${errorText}`); + } + + return await response.json(); +} + +export async function getWebhooks( + clientId: string, +): Promise { + const authToken = await getAuthToken(); + const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, { + method: "GET", + headers: { + "x-client-id": clientId, + Authorization: `Bearer ${authToken}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to get webhooks: ${errorText}`); + } + + return await response.json(); +} + +export async function deleteWebhook( + webhookId: string, + clientId: string, +): Promise { + const authToken = await getAuthToken(); + const response = await fetch( + `${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/${webhookId}`, + { + method: "DELETE", + headers: { + "x-client-id": clientId, + Authorization: `Bearer ${authToken}`, + }, + }, + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to delete webhook: ${errorText}`); + } + + return await response.json(); +} + +export async function testWebhook( + payload: TestWebhookPayload, + clientId: string, +): Promise { + const authToken = await getAuthToken(); + const response = await fetch( + `${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/test`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-client-id": clientId, + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify(payload), + }, + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to test webhook: ${errorText}`); + } + + return await response.json(); +} diff --git a/apps/dashboard/src/@/components/blocks/SidebarLayout.tsx b/apps/dashboard/src/@/components/blocks/SidebarLayout.tsx index cc5bba9f020..fff84e60cbe 100644 --- a/apps/dashboard/src/@/components/blocks/SidebarLayout.tsx +++ b/apps/dashboard/src/@/components/blocks/SidebarLayout.tsx @@ -137,7 +137,7 @@ function RenderSidebarGroup(props: { } if ("separator" in link) { - return ; + return ; } return ( diff --git a/apps/dashboard/src/@/components/blocks/multi-select.tsx b/apps/dashboard/src/@/components/blocks/multi-select.tsx index 2824eaed728..7dbcb2a27fc 100644 --- a/apps/dashboard/src/@/components/blocks/multi-select.tsx +++ b/apps/dashboard/src/@/components/blocks/multi-select.tsx @@ -165,7 +165,9 @@ export const MultiSelect = forwardRef( {props.customTrigger || ( + + + +
+ + Create New Webhook + + +
+ +
+ + {/* Step indicator */} + + + {/* Step Content */} +
+ {currentStep === WebhookFormSteps.BasicInfo && ( + + )} + + {currentStep === WebhookFormSteps.FilterDetails && ( + + )} + + {currentStep === WebhookFormSteps.Review && ( + + )} +
+ + {/* Navigation Buttons */} +
+ {currentStep !== WebhookFormSteps.BasicInfo ? ( + + ) : ( +
+ )} + + {currentStep !== WebhookFormSteps.Review ? ( + + ) : ( + + )} +
+ + + + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/insight/webhooks/components/_components/BasicInfoStep.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/insight/webhooks/components/_components/BasicInfoStep.tsx new file mode 100644 index 00000000000..8f646df6b7d --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/insight/webhooks/components/_components/BasicInfoStep.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { cn } from "@/lib/utils"; +import type { UseFormReturn } from "react-hook-form"; +import type { WebhookFormValues } from "../_utils/webhook-types"; + +interface BasicInfoStepProps { + form: UseFormReturn; +} + +export default function BasicInfoStep({ form }: BasicInfoStepProps) { + return ( + <> +
+

Step 1: Basic Information

+

+ Provide webhook details and select filter type +

+
+ + ( + + + Name * + + + + + + + )} + /> + + ( + + + Webhook URL * + + + + + + + )} + /> + + ( + + + Filter Type * + + + { + const typedValue = value as "event" | "transaction"; + field.onChange(typedValue); + // Ensure the form state is updated immediately + form.setValue("filterType", typedValue, { + shouldValidate: true, + shouldDirty: true, + }); + }} + value={field.value || undefined} + className="grid grid-cols-1 gap-4 md:grid-cols-2" + > +
+