Skip to content

Commit 4d50549

Browse files
committed
feat: code review align and harden form validation
1 parent 2e29272 commit 4d50549

File tree

9 files changed

+104
-80
lines changed

9 files changed

+104
-80
lines changed

src/components/ecommerce/ecommerce-navigation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function EcommerceNavigation() {
1919

2020
return (
2121
<Tabs value={activeTab} className="w-full mb-8">
22-
<TabsList className="grid w-full grid-cols-3">
22+
<TabsList className="grid w-full grid-cols-2">
2323
<TabsTrigger value="manage" asChild>
2424
<Link href="/ecommerce/manage">Manage</Link>
2525
</TabsTrigger>

src/components/ecommerce/manage/blocks/client-form.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ interface EcommerceClientFormProps {
2222
onSubmit: (data: EcommerceClientFormValues) => void;
2323
isLoading?: boolean;
2424
defaultValues?: Partial<EcommerceClientFormValues>;
25-
submitButtonText?: string;
25+
submitButtonText: string;
2626
onCancel?: () => void;
2727
}
2828

2929
export function EcommerceClientForm({
3030
onSubmit,
3131
isLoading = false,
3232
defaultValues,
33-
submitButtonText = "Create Client ID",
33+
submitButtonText,
3434
onCancel,
3535
}: EcommerceClientFormProps) {
3636
const form = useForm<EcommerceClientFormValues>({
@@ -116,12 +116,7 @@ export function EcommerceClientForm({
116116
max="100"
117117
step="0.1"
118118
disabled={isLoading}
119-
value={field.value || ""}
120-
onChange={(e) =>
121-
field.onChange(
122-
e.target.value ? Number(e.target.value) : undefined,
123-
)
124-
}
119+
{...field}
125120
/>
126121
</FormControl>
127122
<FormMessage />
@@ -144,9 +139,7 @@ export function EcommerceClientForm({
144139
{isLoading ? (
145140
<>
146141
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
147-
{submitButtonText.includes("Create")
148-
? "Creating..."
149-
: "Updating..."}
142+
{submitButtonText}
150143
</>
151144
) : (
152145
submitButtonText

src/components/ecommerce/manage/blocks/create-client.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ export function CreateEcommerceClient() {
4747
<DialogHeader>
4848
<DialogTitle>Create New Ecommerce Client</DialogTitle>
4949
</DialogHeader>
50-
<EcommerceClientForm
51-
onSubmit={handleSubmit}
52-
isLoading={isLoading}
53-
submitButtonText="Create Client"
54-
onCancel={() => setIsOpen(false)}
55-
/>
50+
{isOpen && (
51+
<EcommerceClientForm
52+
onSubmit={handleSubmit}
53+
isLoading={isLoading}
54+
submitButtonText="Create Client"
55+
onCancel={() => setIsOpen(false)}
56+
/>
57+
)}
5658
</DialogContent>
5759
</Dialog>
5860
);

src/components/ecommerce/manage/blocks/delete-client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function DeleteEcommerceClient({
5959
</p>
6060
<p className="font-medium text-red-600">
6161
⚠️ This action cannot be undone. The client will be permanently
62-
removed and any integrations using it's ID will stop working.
62+
removed and any integrations using its ID will stop working.
6363
</p>
6464
</AlertDialogDescription>
6565
</AlertDialogHeader>

src/components/ecommerce/manage/blocks/edit-client.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,21 @@ export function EditEcommerceClient({
5252
<DialogHeader>
5353
<DialogTitle>Edit Ecommerce Client</DialogTitle>
5454
</DialogHeader>
55-
<EcommerceClientForm
56-
onSubmit={handleSubmit}
57-
isLoading={isLoading}
58-
defaultValues={{
59-
label: ecommerceClient.label,
60-
domain: ecommerceClient.domain,
61-
feeAddress: ecommerceClient.feeAddress || undefined,
62-
feePercentage: ecommerceClient.feePercentage
63-
? Number(ecommerceClient.feePercentage)
64-
: undefined,
65-
}}
66-
submitButtonText="Update Client"
67-
onCancel={() => setIsOpen(false)}
68-
/>
55+
{isOpen && (
56+
<EcommerceClientForm
57+
key={ecommerceClient.id}
58+
onSubmit={handleSubmit}
59+
isLoading={isLoading}
60+
defaultValues={{
61+
label: ecommerceClient.label,
62+
domain: ecommerceClient.domain,
63+
feeAddress: ecommerceClient.feeAddress || undefined,
64+
feePercentage: ecommerceClient?.feePercentage ?? undefined,
65+
}}
66+
submitButtonText="Update Client"
67+
onCancel={() => setIsOpen(false)}
68+
/>
69+
)}
6970
</DialogContent>
7071
</Dialog>
7172
);

src/components/ecommerce/manage/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@ export function EcommerceManage({
3838
return (
3939
<div className="flex flex-col items-start gap-3">
4040
{shouldCreateDefault ? (
41-
<CreateDefaultEcommerceClient />
41+
<>
42+
<p className="text-sm text-muted-foreground">
43+
Create a default client to get started by using Request Network
44+
Checkout
45+
</p>
46+
<CreateDefaultEcommerceClient />
47+
</>
4248
) : (
4349
<CreateEcommerceClient />
4450
)}

src/lib/schemas/ecommerce.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { isEthereumAddress } from "validator";
22
import { z } from "zod";
33

44
const feeValidation = (
5-
data: { feeAddress?: string; feePercentage?: number },
5+
data: { feeAddress?: string; feePercentage?: string },
66
ctx: z.RefinementCtx,
77
) => {
8-
const hasFeeAddress = data.feeAddress !== undefined && data.feeAddress !== "";
8+
const hasFeeAddress = data.feeAddress !== undefined;
99
const hasFeePercentage = data.feePercentage !== undefined;
1010

1111
if (hasFeeAddress && !hasFeePercentage) {
@@ -30,16 +30,21 @@ const baseEcommerceClientApiSchema = z.object({
3030
domain: z.string().url(),
3131
feeAddress: z
3232
.string()
33+
.transform((val) => (val === "" ? undefined : val))
3334
.refine((value) => {
34-
if (value === undefined || value === "") return true;
35-
35+
if (value === undefined) return true;
3636
return isEthereumAddress(value);
3737
}, "Invalid Ethereum address format")
3838
.optional(),
39-
feePercentage: z.coerce
40-
.number()
41-
.min(0, "Fee percentage must be at least 0")
42-
.max(100, "Fee percentage cannot exceed 100")
39+
feePercentage: z
40+
.string()
41+
.transform((val) => (val === "" ? undefined : val))
42+
.refine((value) => {
43+
if (value === undefined) return true;
44+
const num = Number(value);
45+
46+
return !Number.isNaN(num) && num >= 0 && num <= 100;
47+
}, "Fee percentage must be a number between 0 and 100")
4348
.optional(),
4449
});
4550

src/server/db/schema.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
pgTableCreator,
1111
text,
1212
timestamp,
13+
uniqueIndex,
1314
} from "drizzle-orm/pg-core";
1415

1516
export const createTable = pgTableCreator((name) => `easyinvoice_${name}`);
@@ -299,21 +300,30 @@ export const subscriptionPlanTable = createTable("subscription_plans", {
299300
createdAt: timestamp("created_at").defaultNow(),
300301
});
301302

302-
export const ecommerceClientTable = createTable("ecommerce_client", {
303-
id: text().primaryKey().notNull(),
304-
externalId: text().notNull(), // the API's id for the client ID
305-
rnClientId: text().notNull(), // the request API client ID
306-
userId: text()
307-
.notNull()
308-
.references(() => userTable.id, {
309-
onDelete: "cascade",
310-
}),
311-
label: text().notNull(),
312-
domain: text().notNull(),
313-
feeAddress: text(),
314-
feePercentage: text(),
315-
createdAt: timestamp("created_at").defaultNow(),
316-
});
303+
export const ecommerceClientTable = createTable(
304+
"ecommerce_client",
305+
{
306+
id: text().primaryKey().notNull(),
307+
externalId: text().notNull(), // the API's id for the client ID
308+
rnClientId: text().notNull(), // the request API client ID
309+
userId: text()
310+
.notNull()
311+
.references(() => userTable.id, {
312+
onDelete: "cascade",
313+
}),
314+
label: text().notNull(),
315+
domain: text().notNull(),
316+
feeAddress: text(),
317+
feePercentage: text(),
318+
createdAt: timestamp("created_at").defaultNow(),
319+
},
320+
(table) => ({
321+
userIdDomainIndex: uniqueIndex("ecommerce_client_user_id_domain_unique").on(
322+
table.userId,
323+
table.domain,
324+
),
325+
}),
326+
);
317327

318328
// Relationships
319329

src/server/routers/ecommerce.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,22 @@ export const ecommerceRouter = router({
1818
const { db, user } = ctx;
1919
try {
2020
const existingEcommerceClient =
21-
await db.query.ecommerceClientTable.findMany({
21+
await db.query.ecommerceClientTable.findFirst({
2222
where: and(
2323
eq(ecommerceClientTable.userId, user.id),
2424
eq(ecommerceClientTable.domain, input.domain),
2525
),
2626
});
2727

28-
if (existingEcommerceClient.length > 0) {
28+
if (existingEcommerceClient) {
2929
throw new Error("Ecommerce client for this domain already exists.");
3030
}
3131

3232
const response = await apiClient.post("v2/client-ids", {
3333
label: input.label,
3434
allowedDomains: [input.domain],
35-
...(input.feeAddress && input.feePercentage
36-
? {
37-
feeAddress: input.feeAddress,
38-
feePercentage: input.feePercentage,
39-
}
40-
: {}),
35+
feePercentage: input.feePercentage ?? null,
36+
feeAddress: input.feeAddress ?? null,
4137
});
4238

4339
if (!response.data.clientId) {
@@ -51,8 +47,8 @@ export const ecommerceRouter = router({
5147
externalId: response.data.id,
5248
rnClientId: response.data.clientId,
5349
label: input.label,
54-
feeAddress: input.feeAddress,
55-
feePercentage: input.feePercentage?.toString() ?? undefined,
50+
feeAddress: input.feeAddress ?? null,
51+
feePercentage: input.feePercentage ?? null,
5652
});
5753
} catch (error) {
5854
throw toTRPCError(error);
@@ -73,20 +69,31 @@ export const ecommerceRouter = router({
7369
});
7470

7571
if (!existingEcommerceClient) {
76-
throw new Error("Client ID for this user doesn't exist.");
72+
throw new Error("Client not found or doesn't belong to this user.");
73+
}
74+
75+
if (input.domain !== existingEcommerceClient.domain) {
76+
const conflictingClient =
77+
await db.query.ecommerceClientTable.findFirst({
78+
where: and(
79+
eq(ecommerceClientTable.userId, user.id),
80+
eq(ecommerceClientTable.domain, input.domain),
81+
not(eq(ecommerceClientTable.id, input.id)),
82+
),
83+
});
84+
85+
if (conflictingClient) {
86+
throw new Error("Another client already exists for this domain.");
87+
}
7788
}
7889

7990
await apiClient.put(
8091
`v2/client-ids/${existingEcommerceClient.externalId}`,
8192
{
8293
label: input.label,
8394
allowedDomains: [input.domain],
84-
...(input.feeAddress && input.feePercentage
85-
? {
86-
feeAddress: input.feeAddress,
87-
feePercentage: input.feePercentage,
88-
}
89-
: {}),
95+
feePercentage: input.feePercentage ?? null,
96+
feeAddress: input.feeAddress ?? null,
9097
},
9198
);
9299

@@ -95,8 +102,8 @@ export const ecommerceRouter = router({
95102
.set({
96103
label: input.label,
97104
domain: input.domain,
98-
feeAddress: input.feeAddress,
99-
feePercentage: input.feePercentage?.toString() ?? undefined,
105+
feeAddress: input.feeAddress ?? null,
106+
feePercentage: input.feePercentage ?? null,
100107
})
101108
.where(eq(ecommerceClientTable.id, input.id));
102109
} catch (error) {
@@ -131,13 +138,13 @@ export const ecommerceRouter = router({
131138
throw new Error("Client ID not found.");
132139
}
133140

134-
await apiClient.delete(
135-
`v2/client-ids/${existingEcommerceClient.externalId}`,
136-
);
137-
138141
await db
139142
.delete(ecommerceClientTable)
140143
.where(eq(ecommerceClientTable.id, existingEcommerceClient.id));
144+
145+
await apiClient.delete(
146+
`v2/client-ids/${existingEcommerceClient.externalId}`,
147+
);
141148
} catch (error) {
142149
throw toTRPCError(error);
143150
}

0 commit comments

Comments
 (0)