diff --git a/apps/dashboard/src/@/api/universal-bridge/developer.ts b/apps/dashboard/src/@/api/universal-bridge/developer.ts new file mode 100644 index 00000000000..d5d918071c8 --- /dev/null +++ b/apps/dashboard/src/@/api/universal-bridge/developer.ts @@ -0,0 +1,148 @@ +"use server"; +import { getAuthToken } from "app/api/lib/getAuthToken"; + +const UB_BASE_URL = process.env.NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST; + +type Webhook = { + url: string; + label: string; + active: boolean; + createdAt: string; + id: string; + secret: string; + version?: number; // TODO (UB) make this mandatory after migration +}; + +export async function getWebhooks(props: { + clientId: string; +}) { + const authToken = await getAuthToken(); + const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "x-client-id-override": props.clientId, + Authorization: `Bearer ${authToken}`, + }, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text); + } + + const json = await res.json(); + return json.data as Array; +} + +export async function createWebhook(props: { + clientId: string; + version?: number; + url: string; + label: string; + secret?: string; +}) { + const authToken = await getAuthToken(); + const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, { + method: "POST", + body: JSON.stringify({ + url: props.url, + label: props.label, + version: props.version, + secret: props.secret, + }), + headers: { + "Content-Type": "application/json", + "x-client-id-override": props.clientId, + Authorization: `Bearer ${authToken}`, + }, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text); + } + + return; +} + +export async function deleteWebhook(props: { + clientId: string; + webhookId: string; +}) { + const authToken = await getAuthToken(); + const res = await fetch( + `${UB_BASE_URL}/v1/developer/webhooks/${props.webhookId}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "x-client-id-override": props.clientId, + Authorization: `Bearer ${authToken}`, + }, + }, + ); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text); + } + + return; +} + +export type Fee = { + feeRecipient: string; + feeBps: number; + createdAt: string; + updatedAt: string; +}; + +export async function getFees(props: { + clientId: string; +}) { + const authToken = await getAuthToken(); + const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "x-client-id-override": props.clientId, + Authorization: `Bearer ${authToken}`, + }, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text); + } + + const json = await res.json(); + return json.data as Fee; +} + +export async function updateFee(props: { + clientId: string; + feeRecipient: string; + feeBps: number; +}) { + const authToken = await getAuthToken(); + const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "x-client-id-override": props.clientId, + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify({ + feeRecipient: props.feeRecipient, + feeBps: props.feeBps, + }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text); + } + + return; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/universal-bridge/settings/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/universal-bridge/settings/page.tsx index 1b7b793aba7..a0314e44909 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/universal-bridge/settings/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/universal-bridge/settings/page.tsx @@ -1,5 +1,6 @@ import { getProject } from "@/api/projects"; import { getTeamBySlug } from "@/api/team"; +import { getFees } from "@/api/universal-bridge/developer"; import { redirect } from "next/navigation"; import { PayConfig } from "../../../../../../../components/pay/PayConfig"; @@ -24,5 +25,32 @@ export default async function Page(props: { redirect(`/team/${team_slug}`); } - return ; + let fees = await getFees({ + clientId: project.publishableKey, + }).catch(() => { + return { + feeRecipient: "", + feeBps: 0, + createdAt: "", + updatedAt: "", + }; + }); + + if (!fees) { + fees = { + feeRecipient: "", + feeBps: 0, + createdAt: "", + updatedAt: "", + }; + } + + return ( + + ); } diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/universal-bridge/webhooks/components/webhooks.client.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/universal-bridge/webhooks/components/webhooks.client.tsx index 357c7d508e2..b4d1ef499db 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/universal-bridge/webhooks/components/webhooks.client.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/universal-bridge/webhooks/components/webhooks.client.tsx @@ -1,10 +1,15 @@ "use client"; -import { payServerProxy } from "@/actions/proxies"; +import { + createWebhook, + deleteWebhook, + getWebhooks, +} from "@/api/universal-bridge/developer"; import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; import { CopyTextButton } from "@/components/ui/CopyTextButton"; import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button } from "@/components/ui/button"; +import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -44,58 +49,24 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { formatDistanceToNow } from "date-fns"; import { PlusIcon, TrashIcon } from "lucide-react"; -import { type PropsWithChildren, useState } from "react"; +import { type PropsWithChildren, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import { randomPrivateKey } from "thirdweb/wallets"; import { shortenString } from "utils/usedapp-external"; import { z } from "zod"; -type Webhook = { - url: string; - label: string; - active: boolean; - createdAt: string; - id: string; - secret: string; -}; - type PayWebhooksPageProps = { - /** - * @deprecated - remove after migration - */ clientId: string; - // switching to projectId for lookup, but have to send both during migration - projectId: string; - teamId: string; }; export function PayWebhooksPage(props: PayWebhooksPageProps) { const webhooksQuery = useQuery({ queryKey: ["webhooks", props.clientId], queryFn: async () => { - const res = await payServerProxy({ - method: "GET", - pathname: "/webhooks/get-all", - searchParams: { - /** - * @deprecated - remove after migration - */ - clientId: props.clientId, - // switching to projectId for lookup, but have to send both during migration - projectId: props.projectId, - teamId: props.teamId, - }, - headers: { - "Content-Type": "application/json", - }, + return await getWebhooks({ + clientId: props.clientId, }); - - if (!res.ok) { - throw new Error(); - } - - const json = res.data as { result: Array }; - return json.result; }, }); @@ -107,11 +78,7 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) { return (

No webhooks configured yet.

- +
diff --git a/apps/dashboard/src/components/settings/ApiKeys/validations.ts b/apps/dashboard/src/components/settings/ApiKeys/validations.ts index 49b524180cc..9ce385b6e3e 100644 --- a/apps/dashboard/src/components/settings/ApiKeys/validations.ts +++ b/apps/dashboard/src/components/settings/ApiKeys/validations.ts @@ -114,6 +114,13 @@ export const apiKeyEmbeddedWalletsValidationSchema = z.object({ export const apiKeyPayConfigValidationSchema = z.object({ payoutAddress: payoutAddressValidation, + developerFeeBPS: z + .string() + .transform((val) => Number(val)) + .refine((val) => val >= 0 && val <= 100, { + message: "Developer fee must be between 0 and 100", + }) + .optional(), }); export type ApiKeyEmbeddedWalletsValidationSchema = z.infer<