diff --git a/apps/dashboard/src/@/components/ui/radio-group.tsx b/apps/dashboard/src/@/components/ui/radio-group.tsx index 9274cfdbc67..16f89169a40 100644 --- a/apps/dashboard/src/@/components/ui/radio-group.tsx +++ b/apps/dashboard/src/@/components/ui/radio-group.tsx @@ -51,12 +51,12 @@ const RadioGroupItemButton = React.forwardRef< -
+
{/* Show on checked */} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemPermissionsPage.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemPermissionsPage.tsx index 544a93c25e1..1712304568b 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemPermissionsPage.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemPermissionsPage.tsx @@ -10,7 +10,7 @@ export function EcosystemPermissionsPage({ const { ecosystem } = useEcosystem({ slug: params.slug }); return ( -
+
{ecosystem?.permission === "PARTNER_WHITELIST" && ( diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/AddPartnerDialogButton.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/AddPartnerDialogButton.tsx index 655eb3bcac6..40cd3c701c6 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/AddPartnerDialogButton.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/AddPartnerDialogButton.tsx @@ -20,7 +20,7 @@ export function AddPartnerDialogButton(props: { return ( - diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx index 7cd2fd5df03..54c4770531d 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx @@ -1,8 +1,8 @@ "use client"; -import { ConfirmationDialog } from "@/components/ui/ConfirmationDialog"; +import { MultiNetworkSelector } from "@/components/blocks/NetworkSelectors"; +import { SettingsCard } from "@/components/blocks/SettingsCard"; import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; +import { Checkbox } from "@/components/ui/checkbox"; import { Form, FormControl, @@ -13,111 +13,114 @@ import { } from "@/components/ui/form"; import { FormDescription } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; +import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; -import { useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { PlusIcon } from "lucide-react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; +import { getSocialIcon } from "thirdweb/wallets/in-app"; +import { + DEFAULT_ACCOUNT_FACTORY_V0_6, + DEFAULT_ACCOUNT_FACTORY_V0_7, +} from "thirdweb/wallets/smart"; import invariant from "tiny-invariant"; +import { z } from "zod"; import { type Ecosystem, authOptions } from "../../../../types"; import { useUpdateEcosystem } from "../../hooks/use-update-ecosystem"; -export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) { - const [messageToConfirm, setMessageToConfirm] = useState< - | { - title: string; - description: string; - authOptions: typeof ecosystem.authOptions; - } - | undefined - >(); - const { - mutateAsync: updateEcosystem, - variables, - isPending, - } = useUpdateEcosystem({ - onError: (error) => { - const message = - error instanceof Error ? error.message : "Failed to update ecosystem"; - toast.error(message); - }, - }); +type AuthOptionsFormData = { + authOptions: string[]; + useCustomAuth: boolean; + customAuthEndpoint: string; + customHeaders: { key: string; value: string }[]; + useSmartAccount: boolean; + chainIds: number[]; + sponsorGas: boolean; + accountFactoryType: "v0.6" | "v0.7" | "custom"; + customAccountFactoryAddress: string; +}; - return ( -
-
- {authOptions.map((option) => ( - - { - 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)} - - ))} - { - 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, - }); - }} - /> -
- -
- ); -} - -function CustomAuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) { - const form = useForm({ +export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) { + const form = useForm({ defaultValues: { - customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url, - customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers, + authOptions: ecosystem.authOptions || [], + useCustomAuth: !!ecosystem.customAuthOptions, + customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url || "", + customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers || [], + useSmartAccount: !!ecosystem.smartAccountOptions, + chainIds: ecosystem.smartAccountOptions?.chainIds || [], + sponsorGas: ecosystem.smartAccountOptions?.sponsorGas || false, + accountFactoryType: + ecosystem.smartAccountOptions?.accountFactoryAddress === + DEFAULT_ACCOUNT_FACTORY_V0_7 + ? "v0.7" + : ecosystem.smartAccountOptions?.accountFactoryAddress === + DEFAULT_ACCOUNT_FACTORY_V0_6 + ? "v0.6" + : "custom", + customAccountFactoryAddress: + ecosystem.smartAccountOptions?.accountFactoryAddress || "", }, + resolver: zodResolver( + z + .object({ + authOptions: z.array(z.string()), + useCustomAuth: z.boolean(), + customAuthEndpoint: z.string().optional(), + customHeaders: z + .array( + z.object({ + key: z.string(), + value: z.string(), + }), + ) + .optional(), + useSmartAccount: z.boolean(), + chainIds: z.array(z.number()), + sponsorGas: z.boolean(), + accountFactoryType: z.enum(["v0.6", "v0.7", "custom"]), + customAccountFactoryAddress: z.string().optional(), + }) + .refine( + (data) => { + if (data.useSmartAccount && data.chainIds.length === 0) { + return false; + } + return true; + }, + { + message: "Please select at least one chain for smart accounts", + path: ["chainIds"], + }, + ) + .refine( + (data) => { + if (data.useCustomAuth && !data.customAuthEndpoint) { + return false; + } + return true; + }, + { + message: "Please enter a valid custom auth endpoint", + path: ["customAuthEndpoint"], + }, + ), + ), }); const { fields, remove, append } = useFieldArray({ control: form.control, name: "customHeaders", }); + const { mutateAsync: updateEcosystem, isPending } = useUpdateEcosystem({ onError: (error) => { const message = @@ -125,126 +128,375 @@ function CustomAuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) { toast.error(message); }, onSuccess: () => { - toast.success("Custom Auth Options updated"); + toast.success("Ecosystem options updated"); }, }); + + const onSubmit = (data: AuthOptionsFormData) => { + let customAuthOptions: Ecosystem["customAuthOptions"] | null = null; + if (data.useCustomAuth && data.customAuthEndpoint) { + try { + const url = new URL(data.customAuthEndpoint); + invariant(url.hostname, "Invalid URL"); + customAuthOptions = { + authEndpoint: { + url: data.customAuthEndpoint, + headers: data.customHeaders, + }, + }; + } catch { + toast.error("Invalid Custom Auth URL"); + return; + } + } + + let smartAccountOptions: Ecosystem["smartAccountOptions"] | null = null; + if (data.useSmartAccount) { + let accountFactoryAddress: string; + switch (data.accountFactoryType) { + case "v0.6": + accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_6; + break; + case "v0.7": + accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_7; + break; + case "custom": + accountFactoryAddress = data.customAccountFactoryAddress; + break; + } + + smartAccountOptions = { + chainIds: data.chainIds, + sponsorGas: data.sponsorGas, + accountFactoryAddress, + }; + } + + updateEcosystem({ + ...ecosystem, + authOptions: data.authOptions as (typeof authOptions)[number][], + customAuthOptions, + smartAccountOptions, + }); + }; + return ( -
-

- Custom Auth Options -

- -
-
- ( - - Authentication Endpoint - - Enter the URL for your own authentication endpoint.{" "} - - Learn more. - - - - - - - - )} - /> - ( - - Headers - - Optional: Add headers for your authentication endpoint - - -
- {fields.map((item, index) => ( -
- - + { + console.log(errors); + })} + className="flex flex-col gap-8" + > + +
+ {authOptions.map((option) => ( + { + const isChecked = field.value?.includes(option); + return ( + + +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {option} - +

+ {option.slice(0, 1).toUpperCase() + option.slice(1)} +

+
+ + { + return checked + ? field.onChange([...field.value, option]) + : field.onChange( + field.value?.filter( + (value) => value !== option, + ), + ); + }} + /> +
- ))} -
+ + + + ( + + + + + + )} + /> + + {form.watch("useCustomAuth") && ( +
+ ( + + Authentication Endpoint + + Enter the URL for your own authentication endpoint.{" "} + - Add Header - -
- - -
- )} - /> + Learn more. + + + + + + + + )} + /> + ( + + Headers + + Optional: Add headers for your authentication endpoint + + +
+ {fields.map((item, index) => ( +
+ + + +
+ ))} + +
+
+ +
+ )} + /> +
+ )} +
-
- + /> + + + )} + /> + {form.watch("useSmartAccount") && ( +
+ ( + + Supported Chains + + Select the chains you want to support for smart accounts + + +
+ +
+
+ +
+ )} + /> + ( + + + + +
+ Sponsor Gas + + Enable gas sponsorship for smart accounts + +
+
+ )} + /> + ( + + Account Factory + + + Choose a default account factory or select custom to enter + your own address + + + + )} + /> + {form.watch("accountFactoryType") === "custom" && ( + ( + + Custom Account Factory Address + + + + + Enter your own smart account factory contract address + + + + )} + /> + )}
- -
- -
+ )} + + + ); } diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/auth-options-section.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/auth-options-section.tsx index 734f3ce740e..50bf1bf6125 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/auth-options-section.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/auth-options-section.tsx @@ -1,4 +1,3 @@ -import { Skeleton } from "@/components/ui/skeleton"; import type { Ecosystem } from "../../../../types"; import { AuthOptionsForm, @@ -8,18 +7,6 @@ import { export function AuthOptionsSection({ ecosystem }: { ecosystem?: Ecosystem }) { return (
-
-

- Authentication Options -

- {ecosystem ? ( -

- Configure the authentication options your ecosystem supports. -

- ) : ( - - )} -
{ecosystem ? ( ) : ( diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/ecosystem-partners-section.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/ecosystem-partners-section.tsx index ef4b0e40628..02481ebeb7b 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/ecosystem-partners-section.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/ecosystem-partners-section.tsx @@ -6,24 +6,24 @@ export function EcosystemPartnersSection({ ecosystem, }: { ecosystem: Ecosystem }) { return ( -
-
-
-

+
+
+
+

Ecosystem partners -

-

+

+ +

Configure apps that can use your wallet. Creating a partner will generate a unique partner ID that can access your ecosystem.
{" "} You will need to generate at least one partner ID for use in your own app.

-
-
+
); } diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/integration-permissions-section.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/integration-permissions-section.tsx index e55389b4c98..c6257c99071 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/integration-permissions-section.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/integration-permissions-section.tsx @@ -1,4 +1,3 @@ -import { Skeleton } from "@/components/ui/skeleton"; import type { Ecosystem } from "../../../../types"; import { IntegrationPermissionsToggle, @@ -9,26 +8,26 @@ export function IntegrationPermissionsSection({ ecosystem, }: { ecosystem?: Ecosystem }) { return ( -
-
-

- Integration permissions -

+
+

+ Integration Permissions +

+ + {ecosystem && ( +

+ {ecosystem.permission === "PARTNER_WHITELIST" + ? "Your ecosystem has an allowlist. Only preset partners can add your wallet to their app." + : "Your ecosystem is public. Anyone can add your wallet to their app."} +

+ )} + +
{ecosystem ? ( -

- {ecosystem.permission === "PARTNER_WHITELIST" - ? "Your ecosystem has an allowlist. Only preset partners can add your wallet to their app." - : "Your ecosystem is public. Anyone can add your wallet to their app."} -

+ ) : ( - + )} -
- {ecosystem ? ( - - ) : ( - - )} -
+ +
); } diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/partners-table.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/partners-table.tsx index 95ce105019e..bc4f56aa0ae 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/partners-table.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/server/partners-table.tsx @@ -83,32 +83,32 @@ function PartnerRow(props: { isDeleting && "animate-pulse", )} > - + {props.partner.name} - + {props.partner.allowlistedDomains.map((domain) => (
{domain}
))}
- + {props.partner.allowlistedBundleIds.map((domain) => (
{domain}
))}
- + -
+
- {props.partner.id} + {props.partner.id}
- -
+ +
elements so they can be themed -export function getWalletIcon(provider: string) { +export function getSocialIcon(provider: AuthOption | ({} & string)) { switch (provider) { case "google": return googleIconUri; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx index 30da7985b21..7ab67e79c15 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx @@ -4,7 +4,7 @@ import { shortenAddress } from "../../../../../utils/address.js"; import type { Profile } from "../../../../../wallets/in-app/core/authentication/types.js"; import { fontSize, iconSize } from "../../../../core/design-system/index.js"; import { useSocialProfiles } from "../../../../core/social/useSocialProfiles.js"; -import { getWalletIcon } from "../../../../core/utils/walletIcon.js"; +import { getSocialIcon } from "../../../../core/utils/walletIcon.js"; import { useProfiles } from "../../../hooks/wallets/useProfiles.js"; import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js"; import { Img } from "../../components/Img.js"; @@ -149,7 +149,7 @@ function LinkedProfile({ ) : ( { handleGuestLogin(); }} @@ -483,7 +483,7 @@ export const ConnectWalletSocialOptions = ( {props.isLinking && ( { handleWalletLogin(); }}