Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/tidy-paws-feel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/service-utils": patch
---

Update engineCloud service config
27 changes: 27 additions & 0 deletions apps/dashboard/src/@/api/x402/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Project } from "@/api/project/projects";

type X402Fee = {
feeRecipient: string;
feeBps: number;
};
Comment on lines +3 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Export the X402Fee type for external consumers.

The X402Fee type is referenced by components like X402FeeConfig.tsx (line 26-29) which define their own inline version of this type. Exporting it would enable type reuse and ensure consistency across the codebase.

Apply this diff:

-type X402Fee = {
+export type X402Fee = {
   feeRecipient: string;
   feeBps: number;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type X402Fee = {
feeRecipient: string;
feeBps: number;
};
export type X402Fee = {
feeRecipient: string;
feeBps: number;
};
🤖 Prompt for AI Agents
In apps/dashboard/src/@/api/x402/config.ts around lines 3 to 6, the X402Fee type
is currently declared but not exported; update the declaration to export the
type (e.g., add the export keyword: export type X402Fee = { feeRecipient:
string; feeBps: number; }) so external consumers can import and reuse it, and
then update any files (like X402FeeConfig.tsx) to import this exported type
instead of redefining it locally.


/**
* Extract x402 fee configuration from project's engineCloud service
*/
export function getX402Fees(project: Project): X402Fee {
const engineCloudService = project.services.find(
(service) => service.name === "engineCloud",
);

if (!engineCloudService) {
return {
feeRecipient: "",
feeBps: 0,
};
}

return {
feeRecipient: engineCloudService.x402FeeRecipient || "",
feeBps: engineCloudService.x402FeeBPS || 0,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { Project } from "@/api/project/projects";
import { SettingsCard } from "@/components/blocks/SettingsCard";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { updateProjectClient } from "@/hooks/useApi";
import {
type ApiKeyPayConfigValidationSchema,
apiKeyPayConfigValidationSchema,
} from "@/schema/validations";

interface X402FeeConfigProps {
project: Project;
fees: {
feeRecipient: string;
feeBps: number;
};
projectWalletAddress?: string;
}

export const X402FeeConfig: React.FC<X402FeeConfigProps> = (props) => {
const form = useForm<ApiKeyPayConfigValidationSchema>({
resolver: zodResolver(apiKeyPayConfigValidationSchema),
values: {
developerFeeBPS: props.fees.feeBps ? props.fees.feeBps / 100 : 0,
payoutAddress: props.fees.feeRecipient ?? "",
},
});

const updateFeeMutation = useMutation({
mutationFn: async (values: {
payoutAddress: string;
developerFeeBPS: number;
}) => {
// Find and update the engineCloud service
const newServices = props.project.services.map((service) => {
if (service.name === "engineCloud") {
return {
...service,
x402FeeBPS: values.developerFeeBPS,
x402FeeRecipient: values.payoutAddress,
};
}
return service;
});

// Update the project with the new services configuration
await updateProjectClient(
{
projectId: props.project.id,
teamId: props.project.teamId,
},
{
services: newServices,
},
);
},
});

const handleSubmit = form.handleSubmit(
({ payoutAddress, developerFeeBPS }) => {
updateFeeMutation.mutate(
{
developerFeeBPS: developerFeeBPS ? developerFeeBPS * 100 : 0,
payoutAddress,
},
{
onError: (err) => {
toast.error("Failed to update fee configuration");
console.error(err);
},
onSuccess: () => {
toast.success("Fee configuration updated");
},
},
);
},
(errors) => {
console.log(errors);
},
);

return (
<Form {...form}>
<form autoComplete="off" onSubmit={handleSubmit}>
<SettingsCard
bottomText="Fees are sent to recipient address"
errorText={form.getFieldState("payoutAddress").error?.message}
noPermissionText={undefined}
saveButton={{
disabled: !form.formState.isDirty,
isPending: updateFeeMutation.isPending,
type: "submit",
}}
>
<div>
<h3 className="font-semibold text-xl tracking-tight">
Fee Sharing
</h3>
<p className="mt-1.5 mb-4 text-foreground text-sm">
thirdweb collects a 0.3% service fee on x402 transactions. You may
set your own developer fee in addition to this fee.
</p>

<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<FormField
control={form.control}
name="payoutAddress"
render={({ field }) => (
<FormItem>
<FormLabel>Recipient address</FormLabel>
<FormControl>
<div className="flex flex-col gap-2 sm:flex-row">
<Input
{...field}
className="sm:flex-1"
placeholder="0x..."
/>
{props.projectWalletAddress && (
<Button
onClick={() => {
if (!props.projectWalletAddress) {
return;
}

form.setValue(
"payoutAddress",
props.projectWalletAddress,
{
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
},
);
}}
size="sm"
type="button"
variant="outline"
>
Use Project Wallet
</Button>
)}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="developerFeeBPS"
render={({ field }) => (
<FormItem>
<FormLabel>Fee amount</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<Input {...field} placeholder="0.5" type="number" />
<span className="text-muted-foreground text-sm">%</span>
</div>
</FormControl>
</FormItem>
)}
/>
Comment on lines +160 to +174
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Display validation errors for the fee amount field.

The payoutAddress field shows validation errors (line 100), but the developerFeeBPS field has no error display. Users won't see validation messages (e.g., "Developer fee must be between 0 and 100") if they enter an invalid value.

Add error display for the fee field:

              <FormField
                control={form.control}
                name="developerFeeBPS"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Fee amount</FormLabel>
                    <FormControl>
                      <div className="flex items-center gap-2">
                        <Input {...field} placeholder="0.5" type="number" />
                        <span className="text-muted-foreground text-sm">%</span>
                      </div>
                    </FormControl>
+                   <FormMessage />
                  </FormItem>
                )}
              />

Note: You'll need to import FormMessage from @/components/ui/form.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<FormField
control={form.control}
name="developerFeeBPS"
render={({ field }) => (
<FormItem>
<FormLabel>Fee amount</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<Input {...field} placeholder="0.5" type="number" />
<span className="text-muted-foreground text-sm">%</span>
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="developerFeeBPS"
render={({ field }) => (
<FormItem>
<FormLabel>Fee amount</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<Input {...field} placeholder="0.5" type="number" />
<span className="text-muted-foreground text-sm">%</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/configuration/X402FeeConfig.tsx
around lines 160 to 174, the developerFeeBPS FormField is missing display of
validation errors; update the FormField render to include FormMessage below the
FormControl (same pattern used for payoutAddress) so form errors for
developerFeeBPS show to the user, and ensure FormMessage is imported from
"@/components/ui/form" at the top of the file.

</div>
</div>
</SettingsCard>
</form>
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { getTeamBySlug } from "@/api/team/get-team";
import { getX402Fees } from "@/api/x402/config";
import { getProjectWallet } from "@/lib/server/project-wallet";
import { loginRedirect } from "@/utils/redirects";
import { X402FeeConfig } from "./X402FeeConfig";

export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
}) {
const params = await props.params;
const { team_slug, project_slug } = await props.params;

const [team, project, authToken] = await Promise.all([
getTeamBySlug(team_slug),
getProject(team_slug, project_slug),
getAuthToken(),
]);

const authToken = await getAuthToken();
if (!authToken) {
loginRedirect(
`/team/${params.team_slug}/${params.project_slug}/x402/configuration`,
);
loginRedirect(`/team/${team_slug}/${project_slug}/x402/configuration`);
}

if (!team) {
redirect("/team");
}

const project = await getProject(params.team_slug, params.project_slug);
if (!project) {
redirect(`/team/${params.team_slug}`);
redirect(`/team/${team_slug}`);
}

const projectWallet = await getProjectWallet(project);

const fees = getX402Fees(project);

return (
<div className="flex flex-col gap-6">
<div className="rounded-lg border border-border bg-card p-8 text-center">
<h2 className="text-2xl font-semibold">Coming Soon</h2>
<p className="mt-2 text-muted-foreground">
x402 payments configuration will be available soon.
</p>
</div>
<X402FeeConfig
fees={fees}
project={project}
projectWalletAddress={projectWallet?.address}
/>
</div>
);
}
2 changes: 2 additions & 0 deletions packages/service-utils/src/core/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ export type ProjectService =
encryptedAdminKey?: string | null;
encryptedWalletAccessToken?: string | null;
projectWalletAddress?: string | null;
x402FeeBPS?: number | null;
x402FeeRecipient?: string | null;
}
| ProjectBundlerService
| ProjectEmbeddedWalletsService;
Expand Down
Loading