Skip to content

Commit 0f4e662

Browse files
feat: add custom auth options to ecosystem settings (#5075)
## Problem solved ![CleanShot 2024-10-18 at 14.59.26.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/GGPGC1k4LEcpDtkOsVBe/73287a0a-c367-4a6e-9897-5df7c2935df6.png) <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the `Ecosystem` type and its related components by adding support for custom authentication options, including headers and endpoints, and updating the mutation logic to accommodate these changes. ### Detailed summary - Added `customAuthOptions` to the `Ecosystem` type. - Updated `useUpdateEcosystem` to accept `Ecosystem` directly. - Modified mutation logic to handle the new `Ecosystem` structure. - Introduced `CustomAuthOptionsForm` for managing custom authentication endpoints and headers. - Enhanced UI components to reflect new functionality. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent e67a300 commit 0f4e662

File tree

4 files changed

+240
-81
lines changed

4 files changed

+240
-81
lines changed

apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx

Lines changed: 214 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
"use client";
22
import { ConfirmationDialog } from "@/components/ui/ConfirmationDialog";
3+
import { Button } from "@/components/ui/button";
4+
import { Card } from "@/components/ui/card";
35
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
6+
import {
7+
Form,
8+
FormControl,
9+
FormField,
10+
FormItem,
11+
FormLabel,
12+
FormMessage,
13+
} from "@/components/ui/form";
14+
import { FormDescription } from "@/components/ui/form";
15+
import { Input } from "@/components/ui/input";
416
import { Skeleton } from "@/components/ui/skeleton";
517
import { cn } from "@/lib/utils";
618
import { useState } from "react";
19+
import { useFieldArray, useForm } from "react-hook-form";
720
import { toast } from "sonner";
821
import invariant from "tiny-invariant";
922
import { type Ecosystem, authOptions } from "../../../../types";
@@ -25,65 +38,212 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
2538
} = useUpdateEcosystem({
2639
onError: (error) => {
2740
const message =
28-
error instanceof Error ? error.message : "Failed to create ecosystem";
41+
error instanceof Error ? error.message : "Failed to update ecosystem";
2942
toast.error(message);
3043
},
3144
});
3245

3346
return (
34-
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-5 md:gap-2">
35-
{authOptions.map((option) => (
36-
<CheckboxWithLabel
37-
key={option}
38-
className={cn(
39-
isPending &&
40-
variables?.authOptions?.includes(option) &&
41-
"animate-pulse",
42-
"hover:cursor-pointer hover:text-foreground",
43-
)}
44-
>
45-
<Checkbox
46-
checked={ecosystem.authOptions.includes(option)}
47-
onClick={() => {
48-
if (ecosystem.authOptions.includes(option)) {
49-
setMessageToConfirm({
50-
title: `Are you sure you want to remove ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
51-
description:
52-
"Users will no longer be able to log into your ecosystem using this option. Any users that previously used this option will be unable to log in.",
53-
authOptions: ecosystem.authOptions.filter(
54-
(o) => o !== option,
55-
),
56-
});
57-
} else {
58-
setMessageToConfirm({
59-
title: `Are you sure you want to add ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
60-
description:
61-
"Users will be able to log into your ecosystem using this option. If you later remove this option users that used it will no longer be able to log in.",
62-
authOptions: [...ecosystem.authOptions, option],
63-
});
64-
}
65-
}}
66-
/>
67-
{option.slice(0, 1).toUpperCase() + option.slice(1)}
68-
</CheckboxWithLabel>
69-
))}
70-
<ConfirmationDialog
71-
open={!!messageToConfirm}
72-
onOpenChange={(open) => {
73-
if (!open) {
74-
setMessageToConfirm(undefined);
75-
}
76-
}}
77-
title={messageToConfirm?.title}
78-
description={messageToConfirm?.description}
79-
onSubmit={() => {
80-
invariant(messageToConfirm, "Must have message for modal to be open");
81-
updateEcosystem({
82-
ecosystem,
83-
authOptions: messageToConfirm.authOptions,
84-
});
85-
}}
86-
/>
47+
<div className="flex flex-col gap-8">
48+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-5 md:gap-2">
49+
{authOptions.map((option) => (
50+
<CheckboxWithLabel
51+
key={option}
52+
className={cn(
53+
isPending &&
54+
variables?.authOptions?.includes(option) &&
55+
"animate-pulse",
56+
"hover:cursor-pointer hover:text-foreground",
57+
)}
58+
>
59+
<Checkbox
60+
checked={ecosystem.authOptions?.includes(option)}
61+
onClick={() => {
62+
if (ecosystem.authOptions?.includes(option)) {
63+
setMessageToConfirm({
64+
title: `Are you sure you want to remove ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
65+
description:
66+
"Users will no longer be able to log into your ecosystem using this option. Any users that previously used this option will be unable to log in.",
67+
authOptions: ecosystem.authOptions?.filter(
68+
(o) => o !== option,
69+
),
70+
});
71+
} else {
72+
setMessageToConfirm({
73+
title: `Are you sure you want to add ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
74+
description:
75+
"Users will be able to log into your ecosystem using this option. If you later remove this option users that used it will no longer be able to log in.",
76+
authOptions: [...ecosystem.authOptions, option],
77+
});
78+
}
79+
}}
80+
/>
81+
{option.slice(0, 1).toUpperCase() + option.slice(1)}
82+
</CheckboxWithLabel>
83+
))}
84+
<ConfirmationDialog
85+
open={!!messageToConfirm}
86+
onOpenChange={(open) => {
87+
if (!open) {
88+
setMessageToConfirm(undefined);
89+
}
90+
}}
91+
title={messageToConfirm?.title}
92+
description={messageToConfirm?.description}
93+
onSubmit={() => {
94+
invariant(
95+
messageToConfirm,
96+
"Must have message for modal to be open",
97+
);
98+
updateEcosystem({
99+
...ecosystem,
100+
authOptions: messageToConfirm.authOptions,
101+
});
102+
}}
103+
/>
104+
</div>
105+
<CustomAuthOptionsForm ecosystem={ecosystem} />
106+
</div>
107+
);
108+
}
109+
110+
function CustomAuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
111+
const form = useForm({
112+
defaultValues: {
113+
customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url,
114+
customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers,
115+
},
116+
});
117+
const { fields, remove, append } = useFieldArray({
118+
control: form.control,
119+
name: "customHeaders",
120+
});
121+
const { mutateAsync: updateEcosystem, isPending } = useUpdateEcosystem({
122+
onError: (error) => {
123+
const message =
124+
error instanceof Error ? error.message : "Failed to update ecosystem";
125+
toast.error(message);
126+
},
127+
onSuccess: () => {
128+
toast.success("Custom Auth Options updated");
129+
},
130+
});
131+
return (
132+
<div className="flex flex-col gap-4">
133+
<h4 className="font-semibold text-2xl text-foreground">
134+
Custom Auth Options
135+
</h4>
136+
<Card className="flex flex-col gap-4 p-4">
137+
<div className="flex flex-col gap-4">
138+
<Form {...form}>
139+
<FormField
140+
control={form.control}
141+
name="customAuthEndpoint"
142+
render={({ field }) => (
143+
<FormItem>
144+
<FormLabel>Authentication Endpoint</FormLabel>
145+
<FormDescription>
146+
Enter the URL for your own authentication endpoint.{" "}
147+
<a
148+
className="underline"
149+
href="https://portal.thirdweb.com/connect/in-app-wallet/custom-auth/configuration#generic-auth"
150+
>
151+
Learn more.
152+
</a>
153+
</FormDescription>
154+
<FormControl>
155+
<Input
156+
{...field}
157+
type="url"
158+
placeholder="https://your-custom-auth-endpoint.com"
159+
/>
160+
</FormControl>
161+
<FormMessage />
162+
</FormItem>
163+
)}
164+
/>
165+
<FormField
166+
control={form.control}
167+
name="customHeaders"
168+
render={() => (
169+
<FormItem>
170+
<FormLabel>Headers</FormLabel>
171+
<FormDescription>
172+
Optional: Add headers for your authentication endpoint
173+
</FormDescription>
174+
<FormControl>
175+
<div className="space-y-2">
176+
{fields.map((item, index) => (
177+
<div key={item.id} className="flex gap-2">
178+
<Input
179+
placeholder="Header Key"
180+
{...form.register(`customHeaders.${index}.key`)}
181+
/>
182+
<Input
183+
placeholder="Header Value"
184+
{...form.register(`customHeaders.${index}.value`)}
185+
/>
186+
<Button
187+
type="button"
188+
variant="destructive"
189+
onClick={() => remove(index)}
190+
>
191+
Remove
192+
</Button>
193+
</div>
194+
))}
195+
<Button
196+
type="button"
197+
variant="secondary"
198+
onClick={() => append({ key: "", value: "" })}
199+
>
200+
Add Header
201+
</Button>
202+
</div>
203+
</FormControl>
204+
<FormMessage />
205+
</FormItem>
206+
)}
207+
/>
208+
209+
<div className="flex justify-end">
210+
<Button
211+
disabled={isPending}
212+
type="submit"
213+
onClick={() => {
214+
const customAuthEndpoint =
215+
form.getValues("customAuthEndpoint");
216+
let customAuthOptions:
217+
| Ecosystem["customAuthOptions"]
218+
| undefined = undefined;
219+
if (customAuthEndpoint) {
220+
try {
221+
const url = new URL(customAuthEndpoint);
222+
invariant(url.hostname, "Invalid URL");
223+
} catch {
224+
toast.error("Invalid URL");
225+
return;
226+
}
227+
const customHeaders = form.getValues("customHeaders");
228+
customAuthOptions = {
229+
authEndpoint: {
230+
url: customAuthEndpoint,
231+
headers: customHeaders,
232+
},
233+
};
234+
}
235+
updateEcosystem({
236+
...ecosystem,
237+
customAuthOptions,
238+
});
239+
}}
240+
>
241+
{isPending ? "Saving..." : "Save"}
242+
</Button>
243+
</div>
244+
</Form>
245+
</div>
246+
</Card>
87247
</div>
88248
);
89249
}

apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/integration-permissions-toggle.client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function IntegrationPermissionsToggle({
8989
onSubmit={() => {
9090
invariant(messageToConfirm, "Must have message for modal to be open");
9191
updateEcosystem({
92-
ecosystem,
92+
...ecosystem,
9393
permission: messageToConfirm.permission,
9494
});
9595
}}

apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/hooks/use-update-ecosystem.ts

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,30 @@ import {
44
useMutation,
55
useQueryClient,
66
} from "@tanstack/react-query";
7-
import type { AuthOption, Ecosystem } from "../../../types";
8-
9-
type UpdateEcosystemParams = {
10-
ecosystem: Ecosystem;
11-
permission?: "PARTNER_WHITELIST" | "ANYONE";
12-
authOptions?: AuthOption[];
13-
};
7+
import type { Ecosystem } from "../../../types";
148

159
export function useUpdateEcosystem(
16-
options?: Omit<
17-
UseMutationOptions<boolean, unknown, UpdateEcosystemParams>,
18-
"mutationFn"
19-
>,
10+
options?: Omit<UseMutationOptions<boolean, unknown, Ecosystem>, "mutationFn">,
2011
) {
2112
const { onSuccess, ...queryOptions } = options || {};
2213
const { isLoggedIn, user } = useLoggedInUser();
2314
const queryClient = useQueryClient();
2415

2516
return useMutation({
2617
// Returns true if the update was successful
27-
mutationFn: async (params: UpdateEcosystemParams): Promise<boolean> => {
18+
mutationFn: async (params: Ecosystem): Promise<boolean> => {
2819
if (!isLoggedIn || !user?.jwt) {
2920
throw new Error("Please login to update this ecosystem");
3021
}
3122

32-
const res = await fetch(
33-
`${params.ecosystem.url}/${params.ecosystem.id}`,
34-
{
35-
method: "PATCH",
36-
headers: {
37-
"Content-Type": "application/json",
38-
Authorization: `Bearer ${user.jwt}`,
39-
},
40-
body: JSON.stringify({
41-
permission: params.permission,
42-
authOptions: params.authOptions,
43-
}),
23+
const res = await fetch(`${params.url}/${params.id}`, {
24+
method: "PATCH",
25+
headers: {
26+
"Content-Type": "application/json",
27+
Authorization: `Bearer ${user.jwt}`,
4428
},
45-
);
29+
body: JSON.stringify(params),
30+
});
4631

4732
if (!res.ok) {
4833
const body = await res.json();

apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export const authOptions = [
1313
"coinbase",
1414
"line",
1515
] as const;
16-
export type AuthOption = (typeof authOptions)[number];
1716

1817
export type Ecosystem = {
1918
name: string;
@@ -22,6 +21,21 @@ export type Ecosystem = {
2221
slug: string;
2322
permission: "PARTNER_WHITELIST" | "ANYONE";
2423
authOptions: (typeof authOptions)[number][];
24+
customAuthOptions?: {
25+
authEndpoint?: {
26+
url: string;
27+
headers?: { key: string; value: string }[];
28+
};
29+
jwt?: {
30+
jwksUri: string;
31+
aud: string;
32+
};
33+
};
34+
smartAccountOptions?: {
35+
chainIds: number[];
36+
sponsorGas: boolean;
37+
accountFactoryAddress: string;
38+
};
2539
url: string;
2640
status: "active" | "requested" | "paymentFailed";
2741
createdAt: string;

0 commit comments

Comments
 (0)