From f7aa3e62396f088e958d5c73a17f7ae717750886 Mon Sep 17 00:00:00 2001 From: MananTank Date: Tue, 12 Aug 2025 21:41:49 +0000 Subject: [PATCH] [BLD-80] Dashboard: in-app wallet settings page UI improvements (#7843) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on enhancing the UI components and improving the country selection functionality in the dashboard application. It includes updates to transitions, checkbox indicators, and country names for better clarity and usability. ### Detailed summary - Updated transition duration in `DynamicHeight.tsx`. - Added `MinusIcon` for indeterminate state in `checkbox.tsx`. - Corrected country names in `utils.ts` for several countries. - Improved structure and styling of the `CountrySelector` component. - Added `ChevronDownIcon` and `Button` for toggling tiers in `country-selector.tsx`. - Refactored `Fieldset` and `FieldsetWithDescription` components for better layout and usability in settings forms. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit - New Features - Checkbox now supports an indeterminate state with a clear visual indicator. - Wallet settings sections gain per-section Save buttons with loading spinners. - SMS country selection adds collapsible tier cards, partial-selection counters, and improved country tiles with clear selection states. - Refactor - Unified fieldset layouts with consistent headers, descriptions, dividers, and footers. - Branding settings reorganized into a responsive two‑column layout with improved flow. - Country tier rendering moved to modular TierCard components. - Bug Fixes - Corrected several country display names (e.g., South Korea, North Macedonia, Eswatini, Saint Lucia). - Style - Adjusted a height-transition timing/easing for dynamic-height animations. --- .../src/@/components/ui/DynamicHeight.tsx | 4 +- .../src/@/components/ui/checkbox.tsx | 8 +- .../wallets/settings/components/index.tsx | 249 +++++++++++------- .../sms-country-select/country-selector.tsx | 220 +++++++++------- .../components/sms-country-select/utils.ts | 16 +- 5 files changed, 299 insertions(+), 198 deletions(-) diff --git a/apps/dashboard/src/@/components/ui/DynamicHeight.tsx b/apps/dashboard/src/@/components/ui/DynamicHeight.tsx index d2bb3b91e78..4835281db51 100644 --- a/apps/dashboard/src/@/components/ui/DynamicHeight.tsx +++ b/apps/dashboard/src/@/components/ui/DynamicHeight.tsx @@ -17,9 +17,7 @@ export function DynamicHeight(props: { boxSizing: "border-box", height: height ? `${height}px` : "auto", overflow: "hidden", - transition: - props.transition || - "height 210ms cubic-bezier(0.175, 0.885, 0.32, 1.1)", + transition: props.transition || "height 250ms ease", }} >
- + {props.checked === "indeterminate" ? ( + + ) : ( + + )} )); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/index.tsx index ee84c438bd0..30194d44594 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/index.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/index.tsx @@ -3,7 +3,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@tanstack/react-query"; import type { ProjectEmbeddedWalletsService } from "@thirdweb-dev/service-utils"; import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react"; -import Link from "next/link"; import type React from "react"; import { useState } from "react"; import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form"; @@ -240,11 +239,7 @@ export const InAppWalletSettingsUI: React.FC< return (
- + {/* Branding */} - + {/* Authentication */} -
+
+ +
+ } + > -
+
-
+
- -
- -
); @@ -302,14 +306,22 @@ function BrandingFieldset(props: { teamSlug: string; requiredPlan: Team["billingPlan"]; client: ThirdwebClient; + isUpdating: boolean; }) { return ( -
- + + +
+ } + > +
- +
- {/* Application Image */} - ( - - Application Image URL - - Logo that will display in the emails sent to users.{" "} -
The image must be squared with - recommended size of 72x72 px. -
- - { - props.form.setValue("branding.applicationImageUrl", uri, { - shouldDirty: true, - shouldTouch: true, - }); - }} - uri={props.form.watch("branding.applicationImageUrl")} - /> - - -
- )} - /> +
+ ( + +
+ Application Image URL + + Logo that will display in the emails sent to users.{" "} +
The image must be squared + with recommended size of 72x72 px. +
+ + +
+ + + { + props.form.setValue("branding.applicationImageUrl", uri, { + shouldDirty: true, + shouldTouch: true, + }); + }} + uri={props.form.watch("branding.applicationImageUrl")} + /> + +
+ )} + /> - {/* Application Name */} - ( - - Application Name - - Name that will be displayed in the emails sent to users.{" "} -
Defaults to your API Key's - name. -
- - - - -
- )} - /> + {/* Application Name */} + ( + + Application Name + + Name that will be displayed in the emails sent to users.{" "} +
Defaults to your API Key's + name. +
+ + + + +
+ )} + /> +
- + ); } @@ -418,7 +435,7 @@ function AppImageFormControl(props: {
{ @@ -514,14 +531,13 @@ function JSONWebTokenFields(props: { description={ <> Optionally allow users to authenticate with a custom JWT.{" "} - Learn more - + } switchId="authentication-switch" @@ -606,14 +622,13 @@ function AuthEndpointFields(props: { <> Optionally allow users to authenticate with any arbitrary payload that you provide.{" "} - Learn more - + } switchId="auth-endpoint-switch" @@ -743,10 +758,21 @@ function AuthEndpointFieldsContent(props: { function NativeAppsFieldset(props: { form: UseFormReturn; + isUpdating: boolean; }) { const { form } = props; return ( -
+
+ +
+ } + > {props.children}
; + return ( +
+ {props.children} +
+ ); +} + +function Fieldset(props: { + legend: string; + children: React.ReactNode; + footer?: React.ReactNode; +}) { + return ( +
+ +
+ {/* put inside div to remove default styles on legend */} +
+ {props.legend} +
+ + {props.children} +
+
+ {props.footer} +
+ ); } -function Fieldset(props: { legend: string; children: React.ReactNode }) { +function FieldsetWithDescription(props: { + legend: string; + children: React.ReactNode; + footer?: React.ReactNode; + description: React.ReactNode; +}) { return ( - -
- {/* put inside div to remove default styles on legend */} -
- {props.legend} -
+
+ +
+ {/* put inside div to remove default styles on legend */} +
+ + {props.legend} + +

{props.description}

+
- {props.children} -
-
+ {props.children} +
+
+ {props.footer} +
); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/sms-country-select/country-selector.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/sms-country-select/country-selector.tsx index a8dbbf57552..da376e2c3b0 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/sms-country-select/country-selector.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/sms-country-select/country-selector.tsx @@ -1,6 +1,8 @@ /** biome-ignore-all lint/a11y/useSemanticElements: EXPECTED */ -import { CheckIcon, MinusIcon } from "lucide-react"; +import { CheckIcon, ChevronDownIcon } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import type { SMSCountryTiers } from "../../api/sms"; @@ -31,17 +33,6 @@ export default function CountrySelector({ return tierCountries.every((country) => isCountrySelected(country)); }; - // Check if some countries in a tier are selected - const isTierIndeterminate = (tier: string) => { - const tierCountries = countryTiers[tier as keyof typeof countryTiers]; - const selectedInTier = tierCountries.filter((country) => - isCountrySelected(country), - ); - return ( - selectedInTier.length > 0 && selectedInTier.length < tierCountries.length - ); - }; - // Toggle a tier selection const toggleTier = (tier: string) => { const tierCountries = countryTiers[tier as keyof typeof countryTiers]; @@ -81,101 +72,144 @@ export default function CountrySelector({ onChange(newSelected); }; - // Get selected countries count for a tier - const getSelectedCountInTier = (tier: string) => { - const tierCountries = countryTiers[tier as keyof typeof countryTiers]; - return tierCountries.filter((country) => isCountrySelected(country)).length; - }; + return ( +
+ {Object.entries(countryTiers).map(([tier, tierCountries], index) => { + const selectedTierCountries = tierCountries.filter((country) => + isCountrySelected(country), + ); + + return ( + toggleTier(tier)} + tierCountries={tierCountries} + selectedTierCountries={selectedTierCountries} + onToggleCountry={toggleCountry} + /> + ); + })} +
+ ); +} + +function TierCard(props: { + tier: string; + tierIndex: number; + onToggleTier: () => void; + tierCountries: string[]; + selectedTierCountries: string[]; + onToggleCountry: (country: string) => void; +}) { + const { + tier, + tierIndex, + onToggleTier, + tierCountries: countries, + selectedTierCountries: selectedCountries, + onToggleCountry, + } = props; + + const [isExpanded, setIsExpanded] = useState(true); + const isPartiallySelected = + selectedCountries.length > 0 && selectedCountries.length < countries.length; + const isTierFullySelected = selectedCountries.length === countries.length; return ( -
- {Object.entries(countryTiers).map(([tier, countries], index) => ( -
-
-
- toggleTier(tier)} - /> - {isTierIndeterminate(tier) && ( - - )} -
- - - {tierPricing[tier as keyof typeof tierPricing]} +
+ {/* header */} +
+ {/* left */} +
+ + + + {isPartiallySelected && ( + + ({selectedCountries.length}/{countries.length}) -
+ )} +
-
+ - {countries.map((country) => ( -
+ + +
+
+ + {/* body */} + {isExpanded && ( +
+ {countries.map((country) => { + const isSelected = selectedCountries.includes(country); + return ( +
+ {isSelected && } + + ); + })}
- ))} + )}
); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/sms-country-select/utils.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/sms-country-select/utils.ts index 89943daa541..b43d8694c12 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/sms-country-select/utils.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/sms-country-select/utils.ts @@ -117,14 +117,14 @@ export const countryNames: Record = { KG: "Kyrgyzstan", KH: "Cambodia", KI: "Kiribati", - KN: "St Kitts and Nevis", - KR: "Korea Republic of", + KN: "Saint Kitts and Nevis", + KR: "South Korea", KW: "Kuwait", KY: "Cayman Islands", KZ: "Kazakhstan", LA: "Laos PDR", LB: "Lebanon", - LC: "St Lucia", + LC: "Saint Lucia", LI: "Liechtenstein", LK: "Sri Lanka", LR: "Liberia", @@ -139,7 +139,7 @@ export const countryNames: Record = { ME: "Montenegro", MG: "Madagascar", MH: "Marshall Islands", - MK: "Macedonia", + MK: "North Macedonia", ML: "Mali", MM: "Myanmar", MN: "Mongolia", @@ -172,14 +172,14 @@ export const countryNames: Record = { PH: "Philippines", PK: "Pakistan", PL: "Poland", - PM: "St Pierre and Miquelon", + PM: "Saint Pierre and Miquelon", PR: "Puerto Rico", PS: "Palestinian Territory", PT: "Portugal", PW: "Palau", PY: "Paraguay", QA: "Qatar", - RE: "Reunion/Mayotte", + RE: "Réunion", RO: "Romania", RS: "Serbia", RW: "Rwanda", @@ -200,7 +200,7 @@ export const countryNames: Record = { ST: "Sao Tome and Principe", SV: "El Salvador", SY: "Syria", - SZ: "Swaziland", + SZ: "Eswatini", TC: "Turks and Caicos Islands", TD: "Chad", TG: "Togo", @@ -217,7 +217,7 @@ export const countryNames: Record = { UG: "Uganda", US: "United States", UY: "Uruguay", - VC: "St Vincent Grenadines", + VC: "Saint Vincent and the Grenadines", VE: "Venezuela", VG: "Virgin Islands, British", VI: "Virgin Islands, U.S.",