shadimage
Billingsdk
blockbilling-settings-2source: ok · hit

Billing Settings 2

A comprehensive billing settings component with tabs for general, payment methods, invoices, and usage limits

npx shadcn add "https://billingsdk.com/r/billing-settings-2.json"
free

Where it goes

settings-form

Use cases

dashboard

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
currency-codes

Registry dependencies

buttoncardinputlabelselectswitchutils

Files2 · source cached 10/9/2026, 10:50:33 AM

src/registry/billingsdk/billing-settings-2.tsx
"use client";

import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { useState, useMemo } from "react";
import currencyCodes from "currency-codes";

// Define types for our props
export interface FeatureToggle {
  id: string;
  label: string;
  description: string;
  enabled: boolean;
  onToggle: (enabled: boolean) => void;
}

export interface InputField {
  id: string;
  name: string;
  value?: string;
  defaultValue?: string;
  placeholder: string;
  onChange: (value: string) => void;
  label: string;
  helperText?: string;
  type?: "text" | "email" | "tel" | "url" | "number";
  required?: boolean;
  validation?: {
    minLength?: number;
    maxLength?: number;
    pattern?: RegExp;
    customValidator?: (value: string) => string | null;
  };
}

export interface ValidationError {
  field: string;
  message: string;
}

export interface BillingSettings2Props {
  className?: string;
  title?: string;
  features?: FeatureToggle[];
  inputFields?: InputField[];
  onSave?: () => void;
  onCancel?: () => void;
  saveButtonText?: string;
  cancelButtonText?: string;
  currencies?: string[]; // Array of currency codes to show (e.g., ['USD', 'EUR', 'GBP'])
  currencyOptions?: { value: string; label: string }[]; // Override for custom currency options
  defaultCurrency?: string;
  onCurrencyChange?: (value: string) => void;
  enableValidation?: boolean;
  currencyRequired?: boolean;
}

// Validation helper functions
const validateEmail = (email: string): string | null => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    return "Please enter a valid email address";
  }
  return null;
};

const validateField = (value: string, field: InputField): string | null => {
  // Check required validation
  if (field.required && !value.trim()) {
    return `${field.label} is required`;
  }

  // Skip other validations if field is empty and not required
  if (!value.trim() && !field.required) {
    return null;
  }

  // Check email validation
  if (field.type === "email") {
    return validateEmail(value);
  }

  // Check custom validation
  if (field.validation?.customValidator) {
    return field.validation.customValidator(value);
  }

  // Check pattern validation
  if (field.validation?.pattern && !field.validation.pattern.test(value)) {
    return `Invalid ${field.label.toLowerCase()} format`;
  }

  // Check length validations
  if (
    field.validation?.minLength &&
    value.length < field.validation.minLength
  ) {
    return `${field.label} must be at least ${field.validation.minLength} characters`;
  }

  if (
    field.validation?.maxLength &&
    value.length > field.validation.maxLength
  ) {
    return `${field.label} must be no more than ${field.validation.maxLength} characters`;
  }

  return null;
};

export function BillingSettings2({
  className,
  title = "Billing Settings",
  features = [
    {
      id: "auto-renewal",
      label: "Auto-Renewal",
      description: "Automatically renew your subscription",
      enabled: true,
      onToggle: () => {},
    },
    {
      id: "invoice-emails",
      label: "Invoice Emails",
      description: "Receive emails when invoices are generated",
      enabled: true,
      onToggle: () => {},
    },
    {
      id: "promotional-emails",
      label: "Promotional Emails",
      description: "Receive occasional updates about new features and offers",
      enabled: true,
      onToggle: () => {},
    },
  ],
  inputFields = [
    {
      id: "fullName",
      name: "fullName",
      defaultValue: "",
      placeholder: "John Doe",
      onChange: () => {},
      label: "Full Name",
      type: "text",
      required: true,
    },
    {
      id: "billingEmail",
      name: "billingEmail",
      defaultValue: "",
      placeholder: "user@example.com",
      onChange: () => {},
      label: "Billing Email",
      helperText: "Invoices will be sent to this email address",
      type: "email",
      required: true,
    },
    {
      id: "taxId",
      name: "taxId",
      defaultValue: "",
      placeholder: "EU123456789",
      onChange: () => {},
      label: "Tax ID (Optional)",
      helperText: "For VAT or other tax purposes",
      type: "text",
    },
  ],
  onSave = () => {},
  onCancel = () => {},
  saveButtonText = "Save Changes",
  cancelButtonText = "Cancel",
  currencies, // Array of specific currency codes to show
  currencyOptions, // Custom currency options override
  defaultCurrency = "USD",
  onCurrencyChange = () => {},
  enableValidation = true,
  currencyRequired = true,
}: BillingSettings2Props) {
  const [validationErrors, setValidationErrors] = useState<ValidationError[]>(
    [],
  );
  const [currencyError, setCurrencyError] = useState<string | null>(null);

  // Generate currency options from currency-codes package
  const generatedCurrencyOptions = useMemo(() => {
    // If custom currencyOptions are provided, use them
    if (currencyOptions) {
      return currencyOptions;
    }

    // Get all currency data
    const allCurrencies = currencyCodes.data;

    // If specific currencies are requested, filter to those
    if (currencies && currencies.length > 0) {
      return currencies
        .map((code) => {
          const currency = allCurrencies.find(
            (c) => c.code === code.toUpperCase(),
          );
          return currency
            ? {
                value: currency.code.toLowerCase(),
                label: `${currency.code} - ${currency.currency}`,
              }
            : null;
        })
        .filter(Boolean) as { value: string; label: string }[];
    }

    // Return all currencies if no specific ones requested
    return allCurrencies
      .filter((currency) => currency.code && currency.currency) // Filter out invalid entries
      .map((currency) => ({
        value: currency.code.toLowerCase(),
        label: `${currency.code} - ${currency.currency}`,
      }))
      .sort((a, b) => a.label.localeCompare(b.label)); // Sort alphabetically
  }, [currencies, currencyOptions]);

  // Normalize the defaultCurrency to lowercase to match generated options
  const normalizedDefaultCurrency = defaultCurrency?.toLowerCase();

  // Validate all fields
  const validateAllFields = (): boolean => {
    if (!enableValidation) return true;

    const errors: ValidationError[] = [];
    let hasCurrencyError = false;

    // Validate input fields
    inputFields.forEach((field) => {
      const value =
        field.value !== undefined ? field.value : field.defaultValue || "";
      const error = validateField(value, field);
      if (error) {
        errors.push({ field: field.id, message: error });
      }
    });

    // Validate currency if required
    if (currencyRequired && !normalizedDefaultCurrency) {
      setCurrencyError("Please select a currency");
      hasCurrencyError = true;
    } else {
      setCurrencyError(null);
    }

    setValidationErrors(errors);
    return errors.length === 0 && !hasCurrencyError;
  };

  // Handle save with validation
  const handleSave = () => {
    if (validateAllFields()) {
      onSave();
    }
  };

  // Get error for a specific field
  const getFieldError = (fieldId: string): string | undefined => {
    return validationErrors.find((error) => error.field === fieldId)?.message;
  };

  // Clear validation error for a specific field
  const clearFieldError = (fieldId: string) => {
    setValidationErrors((prev) =>
      prev.filter((error) => error.field !== fieldId),
    );
  };

  // Clear currency error
  const clearCurrencyError = () => {
    setCurrencyError(null);
  };

  // Enhanced input change handler that clears validation errors
  const handleInputChange = (
    fieldId: string,
    value: string,
    originalOnChange: (value: string) => void,
  ) => {
    // Clear the validation error for this field when user starts typing
    clearFieldError(fieldId);
    // Call the original onChange handler
    originalOnChange(value);
  };

  // Enhanced currency change handler
  const handleCurrencyChange = (
    value: string,
    originalOnChange: (value: string) => void,
  ) => {
    // Clear currency error when user makes a selection
    clearCurrencyError();
    // Call the original onChange handler
    originalOnChange(value);
  };
  return (
    <Card className={cn("mx-auto max-w-2xl", className)}>
      <CardHeader>
        <CardTitle className="text-lg">{title}</CardTitle>
      </CardHeader>
      <CardContent className="space-y-6">
        <p className="text-muted-foreground text-sm">
          Manage your billing preferences and settings
        </p>

        <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
          {inputFields.map((field) => {
            const error = getFieldError(field.id);
            return (
              <div key={field.id} className="space-y-2">
                <Label htmlFor={field.id}>
                  {field.label}
                  {field.required && (
                    <span className="ml-1 text-red-500">*</span>
                  )}
                </Label>
                <Input
                  id={field.id}
                  name={field.name}
                  {...(field.value !== undefined
                    ? { value: field.value }
                    : { defaultValue: field.defaultValue })}
                  placeholder={field.placeholder}
                  onChange={(e) =>
                    handleInputChange(field.id, e.target.value, field.onChange)
                  }
                  type={field.type || "text"}
                  aria-describedby={
                    field.helperText ? `${field.id}-help` : undefined
                  }
                  className={error ? "border-red-500 focus:border-red-500" : ""}
                />
                {error ? (
                  <p className="text-xs text-red-500">{error}</p>
                ) : field.helperText ? (
                  <p
                    id={`${field.id}-help`}
                    className="text-muted-foreground text-xs"
                  >
                    {field.helperText}
                  </p>
                ) : null}
              </div>
            );
          })}

          <div className="min-w-0 space-y-2">
            <Label id="currency-label">
              Currency
              {currencyRequired && <span className="ml-1 text-red-500">*</span>}
            </Label>
            <Select
              value={normalizedDefaultCurrency}
              onValueChange={(value) =>
                handleCurrencyChange(value, onCurrencyChange)
              }
            >
              <SelectTrigger
                aria-labelledby="currency-label"
                className={cn(
                  "w-[280px] flex-shrink-0",
                  currencyError ? "border-red-500 focus:border-red-500" : "",
                )}
              >
                <SelectValue
                  placeholder="Select currency"
                  className="truncate overflow-hidden text-ellipsis whitespace-nowrap"
                />
              </SelectTrigger>
              <SelectContent className="max-h-[200px] w-[280px]">
                {generatedCurrencyOptions.map((option) => (
                  <SelectItem
                    key={option.value}
                    value={option.value}
                    className="w-full"
                  >
                    <span
                      className="block w-full truncate"
                      title={option.label}
                    >
                      {option.label}
                    </span>
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            {currencyError && (
              <p className="text-xs text-red-500">{currencyError}</p>
            )}
          </div>
        </div>

        <div className="space-y-4">
          {features.map((feature) => (
            <div
              key={feature.id}
              className="flex items-center justify-between rounded-lg border p-4"
            >
              <div>
                <div className="font-medium">{feature.label}</div>
                <div className="text-muted-foreground text-sm">
                  {feature.description}
                </div>
              </div>
              <Switch
                aria-label={feature.label}
                checked={feature.enabled}
                onCheckedChange={feature.onToggle}
              />
            </div>
          ))}
        </div>

        <div className="flex items-center justify-end gap-3 pt-2">
          <Button variant="outline" onClick={onCancel}>
            {cancelButtonText}
          </Button>
          <Button
            onClick={handleSave}
            disabled={
              enableValidation &&
              (validationErrors.length > 0 || !!currencyError)
            }
          >
            {saveButtonText}
          </Button>
        </div>
      </CardContent>
    </Card>
  );
}
src/registry/billingsdk/demo/billing-settings-2-demo.tsx
"use client";

import { BillingSettings2 } from "@/components/billingsdk/billing-settings-2";
import { useState } from "react";

export function BillingSettings2Demo() {
  const [inputValues, setInputValues] = useState({
    fullName: "",
    billingEmail: "",
    taxId: "",
  });

  const [featureToggles, setFeatureToggles] = useState({
    autoRenewal: true,
    invoiceEmails: true,
    promotionalEmails: false,
  });

  const [selectedCurrency, setSelectedCurrency] = useState("usd");

  const handleInputChange = (field: string, value: string) => {
    setInputValues((prev) => ({
      ...prev,
      [field]: value,
    }));
  };

  // Handler wrappers
  const createInputChangeHandler = (field: string) => (value: string) => {
    handleInputChange(field, value);
  };

  const createCurrencyChangeHandler =
    (setCurrency: (value: string) => void) => (value: string) => {
      console.log("Currency changed to:", value);
      setCurrency(value);
      // Add: updatePricing(value), savePreference(value), etc.
    };

  const handleFeatureToggle = (feature: string, enabled: boolean) => {
    setFeatureToggles((prev) => ({
      ...prev,
      [feature]: enabled,
    }));
  };

  const handleSave = () => {
    // Validation passed if we reach here
    alert("Settings saved successfully!");
    console.log("Input values:", inputValues);
    console.log("Feature toggles:", featureToggles);
    console.log("Selected currency:", selectedCurrency);
  };

  const handleCancel = () => {
    alert("Changes cancelled!");
  };

  return (
    <div className="p-6">
      <BillingSettings2
        title="Custom Billing Settings"
        inputFields={[
          {
            id: "fullName",
            name: "fullName",
            value: inputValues.fullName,
            placeholder: "Enter your full name",
            onChange: createInputChangeHandler("fullName"),
            label: "Full Name",
            type: "text",
            required: true,
            validation: {
              minLength: 2,
              maxLength: 50,
            },
          },
          {
            id: "billingEmail",
            name: "billingEmail",
            value: inputValues.billingEmail,
            placeholder: "user@example.com",
            onChange: createInputChangeHandler("billingEmail"),
            label: "Billing Email",
            helperText: "Invoices will be sent to this email address",
            type: "email",
            required: true,
          },
          {
            id: "taxId",
            name: "taxId",
            value: inputValues.taxId,
            placeholder: "EU123456789",
            onChange: createInputChangeHandler("taxId"),
            label: "Tax ID (Optional)",
            helperText: "For VAT or other tax purposes",
            type: "text",
            validation: {
              pattern: /^[A-Z]{2}\d{8,12}$/,
              customValidator: (value: string) => {
                if (value && !/^[A-Z]{2}\d{8,12}$/.test(value)) {
                  return "Tax ID should be in format: XX followed by 8-12 digits (e.g., EU123456789)";
                }
                return null;
              },
            },
          },
        ]}
        features={[
          {
            id: "auto-renewal",
            label: "Auto-Renewal",
            description: "Automatically renew your subscription",
            enabled: featureToggles.autoRenewal,
            onToggle: (enabled) => handleFeatureToggle("autoRenewal", enabled),
          },
          {
            id: "invoice-emails",
            label: "Invoice Emails",
            description: "Receive emails when invoices are generated",
            enabled: featureToggles.invoiceEmails,
            onToggle: (enabled) =>
              handleFeatureToggle("invoiceEmails", enabled),
          },
          {
            id: "promotional-emails",
            label: "Promotional Emails",
            description:
              "Receive occasional updates about new features and offers",
            enabled: featureToggles.promotionalEmails,
            onToggle: (enabled) =>
              handleFeatureToggle("promotionalEmails", enabled),
          },
        ]}
        // CURRENCY: Shows all 180+ currencies by default
        // currencies={['USD', 'EUR', 'GBP']} // Specific currencies
        // currencyOptions={[{value: 'btc', label: 'Bitcoin'}]} // Custom

        defaultCurrency={selectedCurrency}
        onCurrencyChange={createCurrencyChangeHandler(setSelectedCurrency)}
        onSave={handleSave}
        onCancel={handleCancel}
        saveButtonText="Save Preferences"
        cancelButtonText="Discard Changes"
      />

      {/* 
			USAGE EXAMPLES:
			- Specific: currencies={['USD', 'EUR']}
			- Custom: currencyOptions={[{value: 'btc', label: 'Bitcoin'}]}
			- All: (no currency props) = 180+ currencies
			- Handler: onCurrencyChange={(c) => updatePricing(c)}
			*/}
    </div>
  );
}

More from Billingsdk

Browse all components in this library →