From 18cc0230586bceaef732ad6222a04e17d357189f Mon Sep 17 00:00:00 2001 From: Marcus Farrell Date: Wed, 3 Dec 2025 15:10:28 -0800 Subject: [PATCH 1/4] Settings pages update --- .../[slug]/(ee)/settings/billing/layout.tsx | 11 ++ .../(ee)/settings/billing/payment-methods.tsx | 4 +- .../(ee)/settings/billing/plan-usage.tsx | 4 +- .../(ee)/settings/security/audit-logs.tsx | 12 +- .../[slug]/(ee)/settings/security/layout.tsx | 11 ++ .../(ee)/settings/security/page-client.tsx | 4 +- .../[slug]/(ee)/settings/security/saml.tsx | 64 +++--- .../[slug]/(ee)/settings/security/scim.tsx | 181 ++++++++--------- .../(basic-layout)/integrations/layout.tsx | 22 +++ .../(basic-layout)/integrations/page.tsx | 15 +- .../[slug]/settings/(basic-layout)/layout.tsx | 3 +- .../(basic-layout)/notifications/layout.tsx | 21 ++ .../notifications/page-client.tsx | 184 ++++++++++++------ .../settings/(basic-layout)/page-client.tsx | 4 +- .../[slug]/settings/(basic-layout)/page.tsx | 10 +- .../account/settings/security/page-client.tsx | 8 +- .../security/request-set-password.tsx | 24 +-- .../settings/security/update-password.tsx | 66 +++---- apps/web/ui/account/delete-account.tsx | 10 +- .../ui/account/update-default-workspace.tsx | 16 +- apps/web/ui/account/upload-avatar.tsx | 21 +- apps/web/ui/account/user-id.tsx | 15 +- .../web/ui/layout/sidebar/app-sidebar-nav.tsx | 57 +++++- apps/web/ui/shared/password-requirements.tsx | 4 + apps/web/ui/workspaces/delete-workspace.tsx | 10 +- apps/web/ui/workspaces/upload-logo.tsx | 19 +- packages/ui/src/form.tsx | 12 +- 27 files changed, 506 insertions(+), 306 deletions(-) create mode 100644 apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/layout.tsx create mode 100644 apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/layout.tsx create mode 100644 apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/layout.tsx create mode 100644 apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/notifications/layout.tsx diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/layout.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/layout.tsx new file mode 100644 index 00000000000..72053141f13 --- /dev/null +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/layout.tsx @@ -0,0 +1,11 @@ +import { PageContent } from "@/ui/layout/page-content"; +import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper"; +import { ReactNode } from "react"; + +export default function BillingLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-methods.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-methods.tsx index 5e817fd9671..0c143f2fc80 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-methods.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-methods.tsx @@ -45,7 +45,7 @@ export default function PaymentMethods() { } return ( -
+

Payment methods

@@ -63,7 +63,7 @@ export default function PaymentMethods() { /> )}
-
+
{regularPaymentMethods ? ( regularPaymentMethods.length > 0 ? ( regularPaymentMethods.map((paymentMethod) => ( diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx index 0ba9ca824ec..cd3866e6d84 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx @@ -124,7 +124,7 @@ export default function PlanUsage() { }, [usage, usageLimit, linksUsage, linksLimit, totalLinks]); return ( -
+

@@ -206,7 +206,7 @@ export default function PlanUsage() { href={`/${slug}/settings/people`} />

-
+
-
-
-

Audit Logs

+
+
+
+

Audit Logs

Workspace partner and payout history

-
+
{!canExportAuditLogs && ( -
+
Audit logs are available on the{" "} + {children} + + ); +} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/page-client.tsx index e0f652eb154..d961a22893b 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/page-client.tsx @@ -6,10 +6,10 @@ import { SCIM } from "./scim"; export default function WorkspaceSecurityClient() { return ( - <> +
- +
); } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/saml.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/saml.tsx index 9ab7dc6ba69..90424166a84 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/saml.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/saml.tsx @@ -110,19 +110,21 @@ export function SAML() { return ( <> {configured ? : } -
-
-
-

SAML Single Sign-On

+
+
+
+

+ SAML Single Sign-On +

Set up SAML Single Sign-On (SSO) to allow your team to sign in to{" "} {process.env.NEXT_PUBLIC_APP_NAME} with your identity provider.

-
-
-
+
+
+
{data.logo || (
)} @@ -197,40 +199,40 @@ export function SAML() { )}
-
-
-
- - {workspaceData?.ssoEnforcedAt && ( - - - {ssoEmailDomain} - - )} +
+
+ + {workspaceData?.ssoEnforcedAt && ( + + + {ssoEmailDomain} + + )} +
+
-
- diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/scim.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/scim.tsx index e0f0c402c00..8b85d34c3cb 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/scim.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/scim.tsx @@ -52,113 +52,118 @@ export function SCIM() { return ( <> - - {configured && } -
-
-
-

Directory Sync

+ {configured ? : } +
+
+
+

+ Directory Sync +

Automatically provision and deprovision users from your identity provider.

-
-
- {data.logo || ( -
- )} -
- {data.title ? ( -

{data.title}

- ) : ( -
+
+
+
+ {data.logo || ( +
)} - {data.description ? ( -

{data.description}

+
+ {data.title ? ( +

{data.title}

+ ) : ( +
+ )} + {data.description ? ( +

+ {data.description} +

+ ) : ( +
+ )} +
+
+
+ {loading ? ( +
+ ) : configured ? ( + + + +
+ } + align="end" + openPopover={openPopover} + setOpenPopover={setOpenPopover} + > + + ) : ( -
+
-
- {loading ? ( -
- ) : configured ? ( - - - -
- } - align="end" - openPopover={openPopover} - setOpenPopover={setOpenPopover} - > - - - ) : ( -
- diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/layout.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/layout.tsx new file mode 100644 index 00000000000..32e15c7aa1c --- /dev/null +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/layout.tsx @@ -0,0 +1,22 @@ +import { PageContent } from "@/ui/layout/page-content"; +import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper"; +import { ReactNode } from "react"; + +export default function IntegrationsLayout({ + children, +}: { + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/page.tsx index f10caab3f13..499efb10287 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/page.tsx @@ -3,18 +3,5 @@ import { IntegrationsList } from "./integrations-list"; export const revalidate = 300; // 5 minutes export default function IntegrationsPage() { - return ( -
-
-

- Integrations -

-

- Use Dub with your existing favorite tools with our seamless - integrations. -

-
- -
- ); + return ; } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/layout.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/layout.tsx index 8ee3029679e..386dc73f091 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/layout.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/layout.tsx @@ -1,4 +1,3 @@ -import SettingsLayout from "@/ui/layout/settings-layout"; import { ReactNode } from "react"; // TODO: Move remaining (basic-layout) pages out and get them using PageContent instead @@ -7,5 +6,5 @@ export default function WorkspaceSettingsLayout({ }: { children: ReactNode; }) { - return {children}; + return <>{children}; } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/notifications/layout.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/notifications/layout.tsx new file mode 100644 index 00000000000..c9226824064 --- /dev/null +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/notifications/layout.tsx @@ -0,0 +1,21 @@ +import { PageContent } from "@/ui/layout/page-content"; +import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper"; +import { ReactNode } from "react"; + +export default function NotificationsLayout({ + children, +}: { + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/notifications/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/notifications/page-client.tsx index 1f99f15c64b..83e0bfc2c3d 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/notifications/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/notifications/page-client.tsx @@ -5,8 +5,10 @@ import useWorkspace from "@/lib/swr/use-workspace"; import { notificationTypes } from "@/lib/zod/schemas/workspaces"; import { Switch, useOptimisticUpdate } from "@dub/ui"; import { Globe, Hyperlink, Msgs, UserPlus } from "@dub/ui/icons"; -import { DollarSign, ShieldAlert, Trophy } from "lucide-react"; +import { isClickOnInteractiveChild } from "@dub/utils"; +import { DollarSign, Trophy } from "lucide-react"; import { useAction } from "next-safe-action/hooks"; +import React from "react"; import { z } from "zod"; type PreferenceType = z.infer; @@ -16,7 +18,7 @@ export default function NotificationsSettingsPageClient() { const { id: workspaceId } = useWorkspace(); const { executeAsync } = useAction(updateNotificationPreference); - const notifications = [ + const workspaceNotifications = [ { type: "domainConfigurationUpdates", icon: Globe, @@ -30,6 +32,9 @@ export default function NotificationsSettingsPageClient() { description: "Monthly summary email of your top 5 links by usage & total links created.", }, + ]; + + const partnerProgramNotifications = [ { type: "newPartnerApplication", icon: UserPlus, @@ -57,13 +62,13 @@ export default function NotificationsSettingsPageClient() { description: "Alert when a new message is received from a partner in your partner program.", }, - { - type: "fraudEventsSummary", - icon: ShieldAlert, - title: "Daily Fraud events summary", - description: - "Daily summary email of unresolved fraud events detected in your partner program.", - }, + // { + // type: "fraudEventsSummary", + // icon: ShieldAlert, + // title: "Daily Fraud events summary", + // description: + // "Daily summary email of unresolved fraud events detected in your partner program.", + // }, ]; const { @@ -100,64 +105,119 @@ export default function NotificationsSettingsPageClient() { }; }; - return ( -
-
-

- Workspace Notifications -

-

- Adjust your personal notification preferences and choose which updates - you want to receive. These settings will only be applied to your - personal account. -

-
-
- {notifications.map(({ type, icon: Icon, title, description }) => ( -
-
-
-
- -
+ const renderNotificationItem = ({ + type, + icon: Icon, + title, + description, + isLast, + }: { + type: string; + icon: React.ComponentType<{ className?: string }>; + title: string; + description: string; + isLast?: boolean; + }) => { + const handleRowClick = (e: React.MouseEvent) => { + if (isClickOnInteractiveChild(e) || !preferences || isLoading) return; + + const newValue = !preferences[type]; + update( + () => + handleUpdate({ + type, + value: newValue, + currentPreferences: preferences, + }), + { + ...preferences, + [type]: newValue, + }, + ); + }; + + return ( +
+
+
+
+ +
+
+
+ {title}
-
-
-
{title}
-
-
- - {description} - -
+
+ {description}
- { - if (!preferences) return; - - update( - () => - handleUpdate({ - type, - value: checked, - currentPreferences: preferences, - }), - { - ...preferences, - [type]: checked, - }, - ); - }} - />
- ))} + { + if (!preferences) return; + + update( + () => + handleUpdate({ + type, + value: checked, + currentPreferences: preferences, + }), + { + ...preferences, + [type]: checked, + }, + ); + }} + /> +
+ {!isLast &&
}
+ ); + }; + + const renderSection = ({ + title, + notifications, + }: { + title: string; + notifications: Array<{ + type: string; + icon: React.ComponentType<{ className?: string }>; + title: string; + description: string; + }>; + }) => ( +
+
+

{title}

+
+
+ {notifications.map((notification, index) => + renderNotificationItem({ + ...notification, + isLast: index === notifications.length - 1, + }), + )} +
+
+ ); + + return ( +
+ {renderSection({ + title: "Workspace", + notifications: workspaceNotifications, + })} + {renderSection({ + title: "Partner program", + notifications: partnerProgramNotifications, + })}
); } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/page-client.tsx index 60b34eeaf6d..fbfb1902695 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/page-client.tsx @@ -22,7 +22,7 @@ export default function WorkspaceSettingsClient() { const { update } = useSession(); return ( - <> +
- +
); } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/page.tsx index 290e38c0ddd..6692b7794f9 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/page.tsx @@ -1,5 +1,13 @@ +import { PageContent } from "@/ui/layout/page-content"; +import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper"; import WorkspaceSettingsClient from "./page-client"; export default function WorkspaceSettings() { - return ; + return ( + + + + + + ); } diff --git a/apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx index c5b4f976297..63a75e8dfdc 100644 --- a/apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx @@ -11,12 +11,12 @@ export default function SecurityPageClient() { if (loading) { return ( -
-
-

Password

+
+
+

Password

-
+
diff --git a/apps/web/app/app.dub.co/(dashboard)/account/settings/security/request-set-password.tsx b/apps/web/app/app.dub.co/(dashboard)/account/settings/security/request-set-password.tsx index 5ea36275f7b..07e61c28828 100644 --- a/apps/web/app/app.dub.co/(dashboard)/account/settings/security/request-set-password.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/account/settings/security/request-set-password.tsx @@ -37,9 +37,9 @@ export const RequestSetPassword = () => { }; return ( -
-
-

Password

+
+
+

Password

{user?.provider && ( <> @@ -58,14 +58,16 @@ export const RequestSetPassword = () => { You can set a password to use with your Dub account.

-
-
); diff --git a/apps/web/app/app.dub.co/(dashboard)/account/settings/security/update-password.tsx b/apps/web/app/app.dub.co/(dashboard)/account/settings/security/update-password.tsx index 79c57150452..c38878bc956 100644 --- a/apps/web/app/app.dub.co/(dashboard)/account/settings/security/update-password.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/account/settings/security/update-password.tsx @@ -1,20 +1,28 @@ "use client"; import { updatePasswordSchema } from "@/lib/zod/schemas/auth"; -import { Button, Input, Label, Tooltip } from "@dub/ui"; -import { useForm } from "react-hook-form"; +import { PasswordRequirements } from "@/ui/shared/password-requirements"; +import { Button, Input, Label } from "@dub/ui"; +import { FormProvider, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; // Allow the user to update their existing password export const UpdatePassword = () => { +import { zodResolver } from "@hookform/resolvers/zod"; + +// Allow the user to update their existing password +export const UpdatePassword = () => { + const form = useForm>({ + resolver: zodResolver(updatePasswordSchema), + }); const { register, handleSubmit, setError, formState: { isSubmitting, isDirty, errors }, reset, - } = useForm>(); + } = form; const onSubmit = handleSubmit(async (data) => { try { @@ -41,17 +49,18 @@ export const UpdatePassword = () => { return ( -
-
-

Password

-

- Manage your account password on {process.env.NEXT_PUBLIC_APP_NAME}. -

-
-
+
+
+
+

Password

+

+ Manage your account password on {process.env.NEXT_PUBLIC_APP_NAME} + . +

+
{ type="password" {...register("newPassword", { required: true })} /> - - {errors.newPassword?.message} - + + +
-
-
- -

- Password requirements -

-
-
-
diff --git a/apps/web/ui/account/delete-account.tsx b/apps/web/ui/account/delete-account.tsx index 0bf8a0dd5b3..c45779b7bde 100644 --- a/apps/web/ui/account/delete-account.tsx +++ b/apps/web/ui/account/delete-account.tsx @@ -7,19 +7,19 @@ export default function DeleteAccountSection() { useDeleteAccountModal(); return ( -
+
-
-

Delete Account

+
+

Delete Account

Permanently delete your {process.env.NEXT_PUBLIC_APP_NAME} account, all of your workspaces, links and their respective stats. This action cannot be undone - please proceed with caution.

-
+
-
+