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
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
"use client";
import { ConfirmationDialog } from "@/components/ui/ConfirmationDialog";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { FormDescription } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import invariant from "tiny-invariant";
import { type Ecosystem, authOptions } from "../../../../types";
Expand All @@ -25,65 +38,212 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
} = useUpdateEcosystem({
onError: (error) => {
const message =
error instanceof Error ? error.message : "Failed to create ecosystem";
error instanceof Error ? error.message : "Failed to update ecosystem";
toast.error(message);
},
});

return (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-5 md:gap-2">
{authOptions.map((option) => (
<CheckboxWithLabel
key={option}
className={cn(
isPending &&
variables?.authOptions?.includes(option) &&
"animate-pulse",
"hover:cursor-pointer hover:text-foreground",
)}
>
<Checkbox
checked={ecosystem.authOptions.includes(option)}
onClick={() => {
if (ecosystem.authOptions.includes(option)) {
setMessageToConfirm({
title: `Are you sure you want to remove ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
description:
"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.",
authOptions: ecosystem.authOptions.filter(
(o) => o !== option,
),
});
} else {
setMessageToConfirm({
title: `Are you sure you want to add ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
description:
"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.",
authOptions: [...ecosystem.authOptions, option],
});
}
}}
/>
{option.slice(0, 1).toUpperCase() + option.slice(1)}
</CheckboxWithLabel>
))}
<ConfirmationDialog
open={!!messageToConfirm}
onOpenChange={(open) => {
if (!open) {
setMessageToConfirm(undefined);
}
}}
title={messageToConfirm?.title}
description={messageToConfirm?.description}
onSubmit={() => {
invariant(messageToConfirm, "Must have message for modal to be open");
updateEcosystem({
ecosystem,
authOptions: messageToConfirm.authOptions,
});
}}
/>
<div className="flex flex-col gap-8">
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-5 md:gap-2">
{authOptions.map((option) => (
<CheckboxWithLabel
key={option}
className={cn(
isPending &&
variables?.authOptions?.includes(option) &&
"animate-pulse",
"hover:cursor-pointer hover:text-foreground",
)}
>
<Checkbox
checked={ecosystem.authOptions?.includes(option)}
onClick={() => {
if (ecosystem.authOptions?.includes(option)) {
setMessageToConfirm({
title: `Are you sure you want to remove ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
description:
"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.",
authOptions: ecosystem.authOptions?.filter(
(o) => o !== option,
),
});
} else {
setMessageToConfirm({
title: `Are you sure you want to add ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
description:
"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.",
authOptions: [...ecosystem.authOptions, option],
});
}
}}
/>
{option.slice(0, 1).toUpperCase() + option.slice(1)}
</CheckboxWithLabel>
))}
<ConfirmationDialog
open={!!messageToConfirm}
onOpenChange={(open) => {
if (!open) {
setMessageToConfirm(undefined);
}
}}
title={messageToConfirm?.title}
description={messageToConfirm?.description}
onSubmit={() => {
invariant(
messageToConfirm,
"Must have message for modal to be open",
);
updateEcosystem({
...ecosystem,
authOptions: messageToConfirm.authOptions,
});
}}
/>
</div>
<CustomAuthOptionsForm ecosystem={ecosystem} />
</div>
);
}

function CustomAuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
const form = useForm({
defaultValues: {
customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url,
customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers,
},
});
const { fields, remove, append } = useFieldArray({
control: form.control,
name: "customHeaders",
});
const { mutateAsync: updateEcosystem, isPending } = useUpdateEcosystem({
onError: (error) => {
const message =
error instanceof Error ? error.message : "Failed to update ecosystem";
toast.error(message);
},
onSuccess: () => {
toast.success("Custom Auth Options updated");
},
Comment on lines +122 to +129
Copy link
Member

Choose a reason for hiding this comment

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

this can be improved to use toast.promise etc @MananTank or @kien-ngo maybe you can clean it up down the line - for now this is fine

});
return (
<div className="flex flex-col gap-4">
<h4 className="font-semibold text-2xl text-foreground">
Custom Auth Options
</h4>
<Card className="flex flex-col gap-4 p-4">
<div className="flex flex-col gap-4">
<Form {...form}>
<FormField
control={form.control}
name="customAuthEndpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Authentication Endpoint</FormLabel>
<FormDescription>
Enter the URL for your own authentication endpoint.{" "}
<a
className="underline"
href="https://portal.thirdweb.com/connect/in-app-wallet/custom-auth/configuration#generic-auth"
>
Learn more.
</a>
</FormDescription>
<FormControl>
<Input
{...field}
type="url"
placeholder="https://your-custom-auth-endpoint.com"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="customHeaders"
render={() => (
<FormItem>
<FormLabel>Headers</FormLabel>
<FormDescription>
Optional: Add headers for your authentication endpoint
</FormDescription>
<FormControl>
<div className="space-y-2">
{fields.map((item, index) => (
<div key={item.id} className="flex gap-2">
<Input
placeholder="Header Key"
{...form.register(`customHeaders.${index}.key`)}
/>
<Input
placeholder="Header Value"
{...form.register(`customHeaders.${index}.value`)}
/>
<Button
type="button"
variant="destructive"
onClick={() => remove(index)}
>
Remove
</Button>
</div>
))}
<Button
type="button"
variant="secondary"
onClick={() => append({ key: "", value: "" })}
>
Add Header
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<div className="flex justify-end">
<Button
disabled={isPending}
type="submit"
onClick={() => {
const customAuthEndpoint =
form.getValues("customAuthEndpoint");
let customAuthOptions:
| Ecosystem["customAuthOptions"]
| undefined = undefined;
if (customAuthEndpoint) {
try {
const url = new URL(customAuthEndpoint);
invariant(url.hostname, "Invalid URL");
} catch {
toast.error("Invalid URL");
return;
}
const customHeaders = form.getValues("customHeaders");
customAuthOptions = {
authEndpoint: {
url: customAuthEndpoint,
headers: customHeaders,
},
};
}
updateEcosystem({
...ecosystem,
customAuthOptions,
});
}}
>
{isPending ? "Saving..." : "Save"}
</Button>
</div>
</Form>
</div>
</Card>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function IntegrationPermissionsToggle({
onSubmit={() => {
invariant(messageToConfirm, "Must have message for modal to be open");
updateEcosystem({
ecosystem,
...ecosystem,
permission: messageToConfirm.permission,
});
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,30 @@ import {
useMutation,
useQueryClient,
} from "@tanstack/react-query";
import type { AuthOption, Ecosystem } from "../../../types";

type UpdateEcosystemParams = {
ecosystem: Ecosystem;
permission?: "PARTNER_WHITELIST" | "ANYONE";
authOptions?: AuthOption[];
};
import type { Ecosystem } from "../../../types";

export function useUpdateEcosystem(
options?: Omit<
UseMutationOptions<boolean, unknown, UpdateEcosystemParams>,
"mutationFn"
>,
options?: Omit<UseMutationOptions<boolean, unknown, Ecosystem>, "mutationFn">,
) {
const { onSuccess, ...queryOptions } = options || {};
const { isLoggedIn, user } = useLoggedInUser();
const queryClient = useQueryClient();

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

const res = await fetch(
`${params.ecosystem.url}/${params.ecosystem.id}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${user.jwt}`,
},
body: JSON.stringify({
permission: params.permission,
authOptions: params.authOptions,
}),
const res = await fetch(`${params.url}/${params.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${user.jwt}`,
},
);
body: JSON.stringify(params),
});

if (!res.ok) {
const body = await res.json();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export const authOptions = [
"coinbase",
"line",
] as const;
export type AuthOption = (typeof authOptions)[number];

export type Ecosystem = {
name: string;
Expand All @@ -22,6 +21,21 @@ export type Ecosystem = {
slug: string;
permission: "PARTNER_WHITELIST" | "ANYONE";
authOptions: (typeof authOptions)[number][];
customAuthOptions?: {
authEndpoint?: {
url: string;
headers?: { key: string; value: string }[];
};
jwt?: {
jwksUri: string;
aud: string;
};
};
smartAccountOptions?: {
chainIds: number[];
sponsorGas: boolean;
accountFactoryAddress: string;
};
url: string;
status: "active" | "requested" | "paymentFailed";
createdAt: string;
Expand Down
Loading