From 1d9bf793fa168e656e40cd203100abe3d9e4f00c Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:09:28 +0600 Subject: [PATCH 1/6] feat: add LicensePage component with activation functionality and related types --- package.json | 1 + src/components/LicensePage.stories.tsx | 302 +++++++++++++++++ src/components/license-page.tsx | 453 +++++++++++++++++++++++++ src/index.ts | 1 + 4 files changed, 757 insertions(+) create mode 100644 src/components/LicensePage.stories.tsx create mode 100644 src/components/license-page.tsx diff --git a/package.json b/package.json index b047fe2..a0c6a85 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@wordpress/dataviews": "^11.3.0", + "@wordpress/hooks": "^4.39.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "input-otp": "^1.4.2", diff --git a/src/components/LicensePage.stories.tsx b/src/components/LicensePage.stories.tsx new file mode 100644 index 0000000..e2f1aca --- /dev/null +++ b/src/components/LicensePage.stories.tsx @@ -0,0 +1,302 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "storybook/test"; +import { LicensePage, type LicenseStatus } from "./license-page"; +import React, { useState } from "react"; + +const DefaultHeaderImage = () => ( + + + + + + + + + + +); + +const meta = { + title: "Components/LicensePage", + component: LicensePage, + parameters: { layout: "padded" }, + tags: ["autodocs"], + args: { + onActivate: fn(), + onDeactivate: fn(), + onRefresh: fn(), + onLicenseKeyChange: fn(), + }, + argTypes: { + licenseKey: { control: "text" }, + loading: { control: "boolean" }, + error: { control: "text" }, + pluginName: { control: "text" }, + hookNamespace: { control: "text" }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +// --- Inactive state (no license) --- + +export const Inactive: Story = { + args: { + licenseKey: "", + status: null, + loading: false, + error: "", + pluginName: "MyPlugin Pro", + headerImage: , + }, +}; + +export const InactiveWithKey: Story = { + args: { + licenseKey: "abc123-def456-ghi789-jkl012", + status: null, + loading: false, + error: "", + pluginName: "MyPlugin Pro", + headerImage: , + }, +}; + +// --- Active state --- + +const activeStatus: LicenseStatus = { + is_valid: true, + expiry_days: 120, + data: { + key: "abc123-def456-ghi789-jkl012-mno345", + remaining: 3, + activation_limit: 5, + }, +}; + +export const Active: Story = { + args: { + licenseKey: "abc123-def456-ghi789-jkl012-mno345", + status: activeStatus, + loading: false, + error: "", + pluginName: "MyPlugin Pro", + headerImage: , + }, +}; + +// --- Active with perpetual license --- + +const perpetualStatus: LicenseStatus = { + is_valid: true, + expiry_days: 0, + data: { + key: "abc123-def456-ghi789-jkl012-mno345", + remaining: 10, + activation_limit: 25, + }, +}; + +export const ActivePerpetual: Story = { + args: { + licenseKey: "abc123-def456-ghi789-jkl012-mno345", + status: perpetualStatus, + loading: false, + error: "", + pluginName: "MyPlugin Pro", + headerImage: , + }, +}; + +// --- Loading state --- + +export const Loading: Story = { + args: { + licenseKey: "", + status: null, + loading: true, + error: "", + pluginName: "MyPlugin Pro", + headerImage: , + }, +}; + +// --- Error state --- + +export const WithError: Story = { + args: { + licenseKey: "invalid-key", + status: null, + loading: false, + error: "The license key you entered is invalid. Please check and try again.", + pluginName: "MyPlugin Pro", + headerImage: , + }, +}; + +// --- Without header image --- + +export const WithoutHeaderImage: Story = { + args: { + licenseKey: "", + status: null, + loading: false, + error: "", + pluginName: "wePos", + }, +}; + +// --- Custom labels --- + +export const CustomLabels: Story = { + args: { + licenseKey: "", + status: null, + loading: false, + error: "", + pluginName: "Starter Plugin", + headerImage: , + labels: { + title: "Activation", + activationTitle: "Product Activation", + activateButton: "Activate Now", + licenseKeyPlaceholder: "Paste your license key...", + }, + }, +}; + +// --- Interactive playground --- + +const InteractiveTemplate = () => { + const [licenseKey, setLicenseKey] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [status, setStatus] = useState(null); + + const handleActivate = async () => { + setLoading(true); + setError(""); + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1500)); + + if (licenseKey === "invalid") { + setError("Invalid license key. Please try again."); + setLoading(false); + return; + } + + setStatus({ + is_valid: true, + expiry_days: 365, + data: { + key: licenseKey, + remaining: 4, + activation_limit: 5, + }, + }); + setLoading(false); + }; + + const handleDeactivate = async () => { + setLoading(true); + setError(""); + await new Promise((resolve) => setTimeout(resolve, 1000)); + setStatus(null); + setLicenseKey(""); + setLoading(false); + }; + + const handleRefresh = async () => { + setLoading(true); + await new Promise((resolve) => setTimeout(resolve, 800)); + setLoading(false); + }; + + return ( + } + /> + ); +}; + +export const Interactive: Story = { + render: () => , + args: { + licenseKey: "", + status: null, + loading: false, + error: "", + }, + parameters: { + docs: { + description: { + story: + "A fully interactive demo with simulated API calls. Type any key to activate (type 'invalid' to see error state).", + }, + }, + }, +}; diff --git a/src/components/license-page.tsx b/src/components/license-page.tsx new file mode 100644 index 0000000..1375a55 --- /dev/null +++ b/src/components/license-page.tsx @@ -0,0 +1,453 @@ +import { useMemo, useState, type HTMLAttributes, type ReactNode } from "react"; +import { doAction } from "@wordpress/hooks"; +import { Calendar, Eye, EyeOff, Info, KeyRound, RefreshCw } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Spinner } from "@/components/ui/spinner"; +import { + Modal, + ModalHeader, + ModalTitle, + ModalDescription, + ModalFooter, +} from "@/components/ui/modal"; + +export interface LicenseStatus { + is_valid: boolean; + expiry_days?: number; + data?: { + key?: string; + remaining?: number; + activation_limit?: number; + }; +} + +export interface LicensePageLabels { + title?: string; + activationTitle?: string; + activationDescription?: string; + activateLicenseHeading?: string; + activateLicenseSubheading?: string; + licenseKeyLabel?: string; + licenseKeyPlaceholder?: string; + activateButton?: string; + deactivateButton?: string; + refreshButton?: string; + activeStatus?: string; + maskedKeyInfo?: string; + activationsRemainingTitle?: string; + usageLabel?: string; + outOfLabel?: string; + activationInfoUnavailable?: string; + licenseExpiryTitle?: string; + perpetualLicenseMessage?: string; + expiryMessage?: (date: string, days: number) => string; + expiryInfoUnavailable?: string; + deactivateConfirmTitle?: string; + deactivateConfirmMessage?: string; + cancelButton?: string; + confirmDeactivateButton?: string; + showKeyLabel?: string; + hideKeyLabel?: string; +} + +export interface LicensePageProps extends HTMLAttributes { + /** Current license key value */ + licenseKey: string; + /** Callback when the license key input changes */ + onLicenseKeyChange: (key: string) => void; + /** Current license status data */ + status: LicenseStatus | null; + /** Whether a license operation is in progress */ + loading: boolean; + /** Current error message, empty string if no error */ + error: string; + /** Called when user clicks the Activate button */ + onActivate: () => void; + /** Called when user confirms deactivation */ + onDeactivate: () => void; + /** Called when user clicks the Refresh button */ + onRefresh: () => void; + /** Custom header image/illustration rendered in the top-right of the card */ + headerImage?: ReactNode; + /** Namespace for WordPress hooks (doAction). Defaults to 'plugin_ui' */ + hookNamespace?: string; + /** Plugin name used in default label strings. Defaults to 'Plugin' */ + pluginName?: string; + /** Override default label strings for i18n or customization */ + labels?: LicensePageLabels; + /** Format a date string for display (e.g., using dateI18n from @wordpress/date) */ + formatDate?: (date: Date) => string; +} + +const maskKey = (key: string): string => { + if (!key) return ""; + if (key.length <= 8) return "*".repeat(key.length); + return key.substring(0, 18) + "*".repeat(Math.max(10, key.length - 18)); +}; + +const getUsagePercentage = ( + limit: number, + remaining: number, +): number => { + return Math.min(100, Math.round(((limit - remaining) / (limit || 1)) * 100)); +}; + +const defaultFormatDate = (date: Date): string => { + return date.toLocaleDateString(undefined, { + year: "numeric", + month: "long", + day: "numeric", + }); +}; + +export function LicensePage({ + licenseKey, + onLicenseKeyChange, + status, + loading, + error, + onActivate, + onDeactivate, + onRefresh, + headerImage, + hookNamespace = "plugin_ui", + pluginName = "Plugin", + labels: labelOverrides = {}, + formatDate = defaultFormatDate, + className, + ...props +}: LicensePageProps) { + const [showKey, setShowKey] = useState(false); + const [showDeactivateModal, setShowDeactivateModal] = useState(false); + + const hasActive = useMemo(() => Boolean(status?.is_valid), [status]); + + const labels = useMemo(() => { + const defaults: Required = { + title: "License", + activationTitle: "License Activation", + activationDescription: `Activate ${pluginName} with your license key for automatic updates and expert support from your dashboard.`, + activateLicenseHeading: "Activate License", + activateLicenseSubheading: + "Enter your license key to activate your software", + licenseKeyLabel: `${pluginName} License Key`, + licenseKeyPlaceholder: "Enter your key here", + activateButton: "Activate License", + deactivateButton: "Deactivate License", + refreshButton: "Refresh", + activeStatus: "Active", + maskedKeyInfo: "Your license key is masked for security purposes", + activationsRemainingTitle: "Activations Remaining", + usageLabel: "Usage", + outOfLabel: "out of", + activationInfoUnavailable: "Activation information not available.", + licenseExpiryTitle: "License Expiry", + perpetualLicenseMessage: `Your ${pluginName} license is perpetual and will never expire.`, + expiryMessage: (date: string, days: number) => + `Your ${pluginName} license expires on ${date} (${days} days left)`, + expiryInfoUnavailable: "Expiry information not available.", + deactivateConfirmTitle: + "Are you sure you want to deactivate the license key?", + deactivateConfirmMessage: + "Deactivating will disable updates and support until you activate again.", + cancelButton: "Cancel", + confirmDeactivateButton: "Yes, Deactivate", + showKeyLabel: "Show key", + hideKeyLabel: "Hide key", + }; + return { ...defaults, ...labelOverrides }; + }, [labelOverrides, pluginName]); + + const maskedOrReal = showKey ? licenseKey : maskKey(licenseKey); + + const handleActivate = () => { + doAction(`${hookNamespace}_license_action`, { + action: "activate", + licenseKey, + }); + onActivate(); + }; + + const handleDeactivate = () => { + setShowDeactivateModal(false); + doAction(`${hookNamespace}_license_action`, { + action: "deactivate", + }); + onDeactivate(); + }; + + const handleRefresh = () => { + doAction(`${hookNamespace}_license_action`, { + action: "refresh", + }); + onRefresh(); + }; + + const expiryDate = + typeof status?.expiry_days === "number" && status.expiry_days > 0 + ? new Date(Date.now() + status.expiry_days * 24 * 60 * 60 * 1000) + : null; + + return ( +
+

{labels.title}

+ +
+ {loading && ( +
+
+ +
+
+ )} + + + {/* Header section */} +
+
+
+
+

{labels.activationTitle}

+ {hasActive && ( + + {labels.activeStatus} + + )} +
+

+ {labels.activationDescription} +

+
+ {headerImage && ( +
+ {headerImage} +
+ )} +
+
+ + {/* Body section */} +
+ {/* Activate heading (shown when not active) */} + {!hasActive && ( +
+
+ +
+
+

+ {labels.activateLicenseHeading} +

+ + {labels.activateLicenseSubheading} + +
+
+ )} + + {/* License key label */} +
+ {labels.licenseKeyLabel} +
+ + {/* License key input */} +
+ onLicenseKeyChange(e.target.value)} + disabled={loading || hasActive} + className={cn( + "pr-10", + error && "border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20" + )} + /> + +
+ + {/* Error message */} + {error && ( +

{error}

+ )} + + {/* Masked key info */} + {!error && hasActive && ( +
+ + {labels.maskedKeyInfo} +
+ )} + + {/* Action buttons */} +
+ {hasActive ? ( + <> + + + + ) : ( + + )} +
+
+
+ + {/* Status cards (shown when active) */} + {hasActive && ( +
+ {/* Activations Remaining card */} + +
+
+ +
+
+ {labels.activationsRemainingTitle} +
+
+ {typeof status?.data?.remaining !== "undefined" && + typeof status?.data?.activation_limit !== "undefined" ? ( + <> +
+ {labels.usageLabel} +
+
+
+
+
+
+ {getUsagePercentage( + status.data.activation_limit, + status.data.remaining, + )} + % +
+
+
+ {`${status.data.activation_limit - status.data.remaining} ${labels.outOfLabel} ${status.data.activation_limit}`} +
+ + ) : ( +
+ {labels.activationInfoUnavailable} +
+ )} + + + {/* License Expiry card */} + +
+
+ +
+
+ {labels.licenseExpiryTitle} +
+
+ {typeof status?.expiry_days === "number" ? ( + status.expiry_days <= 0 ? ( +
+ {labels.perpetualLicenseMessage} +
+ ) : ( +
+ +
+
+ ) + ) : ( +
+ {labels.expiryInfoUnavailable} +
+ )} + +
+ )} +
+ + {/* Deactivation confirmation modal */} + setShowDeactivateModal(false)} + size="sm" + > + + {labels.deactivateConfirmTitle} + + + {labels.deactivateConfirmMessage} + + +
+ + +
+
+
+
+ ); +} diff --git a/src/index.ts b/src/index.ts index 12dd9b9..5e7fe78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -260,6 +260,7 @@ export { export { TopBar, type TopBarProps } from './components/top-bar' export { CrownIcon, type CrownIconProps } from './components/crown-icon' export { ButtonToggleGroup, type ButtonToggleGroupProps, type ButtonToggleGroupItem } from './components/button-toggle-group' +export { LicensePage, type LicensePageProps, type LicensePageLabels, type LicenseStatus } from './components/license-page' // Settings (schema-driven settings page) export { From 947c47614d36d6642b17173f64f88f44eeb9ee45 Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:46:14 +0600 Subject: [PATCH 2/6] feat: replace modal with alert dialog for license deactivation confirmation and improve UI elements --- src/components/LicensePage.stories.tsx | 163 +++++++++++++++---------- src/components/license-page.tsx | 111 ++++++++--------- 2 files changed, 152 insertions(+), 122 deletions(-) diff --git a/src/components/LicensePage.stories.tsx b/src/components/LicensePage.stories.tsx index e2f1aca..b13f61a 100644 --- a/src/components/LicensePage.stories.tsx +++ b/src/components/LicensePage.stories.tsx @@ -5,67 +5,104 @@ import React, { useState } from "react"; const DefaultHeaderImage = () => ( - - - - - - - - - + width="122" + height="83" + viewBox="0 0 122 83" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + + + + + + + + + + + + + + + + + + + + ); const meta = { @@ -226,7 +263,6 @@ const InteractiveTemplate = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [status, setStatus] = useState(null); - const handleActivate = async () => { setLoading(true); setError(""); @@ -251,13 +287,14 @@ const InteractiveTemplate = () => { setLoading(false); }; - const handleDeactivate = async () => { + const handleDeactivate = async (closeDialog: () => void) => { setLoading(true); setError(""); await new Promise((resolve) => setTimeout(resolve, 1000)); setStatus(null); setLicenseKey(""); setLoading(false); + closeDialog(); }; const handleRefresh = async () => { diff --git a/src/components/license-page.tsx b/src/components/license-page.tsx index 1375a55..abc7497 100644 --- a/src/components/license-page.tsx +++ b/src/components/license-page.tsx @@ -1,4 +1,5 @@ import { useMemo, useState, type HTMLAttributes, type ReactNode } from "react"; +import { RawHTML } from "@wordpress/element"; import { doAction } from "@wordpress/hooks"; import { Calendar, Eye, EyeOff, Info, KeyRound, RefreshCw } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -7,12 +8,16 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { - Modal, - ModalHeader, - ModalTitle, - ModalDescription, - ModalFooter, -} from "@/components/ui/modal"; + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogAction, + AlertDialogCancel, +} from "@/components/ui/alert-dialog"; export interface LicenseStatus { is_valid: boolean; @@ -66,8 +71,8 @@ export interface LicensePageProps extends HTMLAttributes { error: string; /** Called when user clicks the Activate button */ onActivate: () => void; - /** Called when user confirms deactivation */ - onDeactivate: () => void; + /** Called when user confirms deactivation. Receives `closeDialog` to dismiss the confirmation when ready. */ + onDeactivate: (closeDialog: () => void) => void; /** Called when user clicks the Refresh button */ onRefresh: () => void; /** Custom header image/illustration rendered in the top-right of the card */ @@ -121,7 +126,7 @@ export function LicensePage({ ...props }: LicensePageProps) { const [showKey, setShowKey] = useState(false); - const [showDeactivateModal, setShowDeactivateModal] = useState(false); + const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false); const hasActive = useMemo(() => Boolean(status?.is_valid), [status]); @@ -172,11 +177,10 @@ export function LicensePage({ }; const handleDeactivate = () => { - setShowDeactivateModal(false); doAction(`${hookNamespace}_license_action`, { action: "deactivate", }); - onDeactivate(); + onDeactivate(() => setDeactivateDialogOpen(false)); }; const handleRefresh = () => { @@ -193,7 +197,7 @@ export function LicensePage({ return (

{labels.title}

@@ -213,14 +217,14 @@ export function LicensePage({
-

{labels.activationTitle}

+

{labels.activationTitle}

{hasActive && ( {labels.activeStatus} )}
-

+

{labels.activationDescription}

@@ -241,7 +245,7 @@ export function LicensePage({
-

+

{labels.activateLicenseHeading}

@@ -283,7 +287,7 @@ export function LicensePage({ {/* Error message */} {error && ( -

{error}

+

{error}

)} {/* Masked key info */} @@ -298,14 +302,32 @@ export function LicensePage({
{hasActive ? ( <> - + + + + + + + + {labels.deactivateConfirmTitle} + + + {labels.deactivateConfirmMessage} + + + + + {labels.cancelButton} + + + {labels.confirmDeactivateButton} + + + +
- {/* Deactivation confirmation modal */} - setShowDeactivateModal(false)} - size="sm" - > - - {labels.deactivateConfirmTitle} - - - {labels.deactivateConfirmMessage} - - -
- - -
-
-
); } From 8e0824b1bf807010200bdb4176706b4dd3769d63 Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:54:39 +0600 Subject: [PATCH 3/6] feat: add License component with activation functionality and update exports --- ...ensePage.stories.tsx => License.stories.tsx} | 10 +++++----- .../{license-page.tsx => license.tsx} | 17 ++++++++--------- src/index.ts | 2 +- 3 files changed, 14 insertions(+), 15 deletions(-) rename src/components/{LicensePage.stories.tsx => License.stories.tsx} (98%) rename src/components/{license-page.tsx => license.tsx} (97%) diff --git a/src/components/LicensePage.stories.tsx b/src/components/License.stories.tsx similarity index 98% rename from src/components/LicensePage.stories.tsx rename to src/components/License.stories.tsx index b13f61a..4f1a839 100644 --- a/src/components/LicensePage.stories.tsx +++ b/src/components/License.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { fn } from "storybook/test"; -import { LicensePage, type LicenseStatus } from "./license-page"; +import { License, type LicenseStatus } from "./license"; import React, { useState } from "react"; const DefaultHeaderImage = () => ( @@ -106,8 +106,8 @@ const DefaultHeaderImage = () => ( ); const meta = { - title: "Components/LicensePage", - component: LicensePage, + title: "Components/License", + component: License, parameters: { layout: "padded" }, tags: ["autodocs"], args: { @@ -123,7 +123,7 @@ const meta = { pluginName: { control: "text" }, hookNamespace: { control: "text" }, }, -} satisfies Meta; +} satisfies Meta; export default meta; @@ -304,7 +304,7 @@ const InteractiveTemplate = () => { }; return ( - { +export interface LicenseProps extends HTMLAttributes { /** Current license key value */ licenseKey: string; /** Callback when the license key input changes */ @@ -82,7 +81,7 @@ export interface LicensePageProps extends HTMLAttributes { /** Plugin name used in default label strings. Defaults to 'Plugin' */ pluginName?: string; /** Override default label strings for i18n or customization */ - labels?: LicensePageLabels; + labels?: LicenseLabels; /** Format a date string for display (e.g., using dateI18n from @wordpress/date) */ formatDate?: (date: Date) => string; } @@ -108,7 +107,7 @@ const defaultFormatDate = (date: Date): string => { }); }; -export function LicensePage({ +export function License({ licenseKey, onLicenseKeyChange, status, @@ -124,14 +123,14 @@ export function LicensePage({ formatDate = defaultFormatDate, className, ...props -}: LicensePageProps) { +}: LicenseProps) { const [showKey, setShowKey] = useState(false); const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false); const hasActive = useMemo(() => Boolean(status?.is_valid), [status]); const labels = useMemo(() => { - const defaults: Required = { + const defaults: Required = { title: "License", activationTitle: "License Activation", activationDescription: `Activate ${pluginName} with your license key for automatic updates and expert support from your dashboard.`, @@ -206,7 +205,7 @@ export function LicensePage({ {loading && (
- +
)} diff --git a/src/index.ts b/src/index.ts index 5e7fe78..57d5be4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -260,7 +260,7 @@ export { export { TopBar, type TopBarProps } from './components/top-bar' export { CrownIcon, type CrownIconProps } from './components/crown-icon' export { ButtonToggleGroup, type ButtonToggleGroupProps, type ButtonToggleGroupItem } from './components/button-toggle-group' -export { LicensePage, type LicensePageProps, type LicensePageLabels, type LicenseStatus } from './components/license-page' +export { License, type LicenseProps, type LicenseLabels, type LicenseStatus } from './components/license' // Settings (schema-driven settings page) export { From 62fc56c7ab1163e8139de92ce4ab3e4963d05024 Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:07:58 +0600 Subject: [PATCH 4/6] feat: enhance AlertDialog for license deactivation with improved loading state handling and styling --- src/components/license.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/license.tsx b/src/components/license.tsx index 77d9763..800981e 100644 --- a/src/components/license.tsx +++ b/src/components/license.tsx @@ -301,26 +301,27 @@ export function License({
{hasActive ? ( <> - + { if (!loading) setDeactivateDialogOpen(open); }}> - + {labels.deactivateConfirmTitle} - + {labels.deactivateConfirmMessage} - + {labels.cancelButton} {labels.confirmDeactivateButton} From 82895ccea770cf999ba311d1f02e4d93f0d21c13 Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:27:59 +0600 Subject: [PATCH 5/6] feat: integrate Badge component for active license status display and adjust responsive layout --- src/components/license.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/license.tsx b/src/components/license.tsx index 800981e..f7a9120 100644 --- a/src/components/license.tsx +++ b/src/components/license.tsx @@ -17,6 +17,7 @@ import { AlertDialogAction, AlertDialogCancel, } from "@/components/ui/alert-dialog"; +import { Badge } from "./ui"; export interface LicenseStatus { is_valid: boolean; @@ -218,9 +219,9 @@ export function License({

{labels.activationTitle}

{hasActive && ( - + {labels.activeStatus} - + )}

@@ -228,7 +229,7 @@ export function License({

{headerImage && ( -
+
{headerImage}
)} From 73cda82f63361a3a5b7e87e2fbd2791c70890ed8 Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:47:37 +0600 Subject: [PATCH 6/6] feat: add LicenseClassNames interface for customizable styling in License component --- src/components/license.tsx | 95 +++++++++++++++++++++++++++++--------- src/index.ts | 2 +- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/src/components/license.tsx b/src/components/license.tsx index f7a9120..c582ddf 100644 --- a/src/components/license.tsx +++ b/src/components/license.tsx @@ -58,6 +58,53 @@ export interface LicenseLabels { hideKeyLabel?: string; } +export interface LicenseClassNames { + /** Root wrapper div */ + root?: string; + /** Page title (h1) */ + title?: string; + /** Loading overlay backdrop */ + loadingOverlay?: string; + /** Loading spinner icon */ + loadingSpinner?: string; + /** Main license card */ + card?: string; + /** Card header section (contains title, badge, description, image) */ + header?: string; + /** Header title (h2) */ + headerTitle?: string; + /** Header description paragraph */ + headerDescription?: string; + /** Header image container */ + headerImageContainer?: string; + /** Card body section */ + body?: string; + /** Activate license heading section (icon + text) */ + activateHeading?: string; + /** Activate heading icon container */ + activateHeadingIcon?: string; + /** Activate heading title (h3) */ + activateHeadingTitle?: string; + /** Activate heading subtitle */ + activateHeadingSubtitle?: string; + /** License key label */ + licenseKeyLabel?: string; + /** License key input */ + input?: string; + /** Error message text */ + errorMessage?: string; + /** Masked key info row */ + maskedKeyInfo?: string; + /** Action buttons container */ + actions?: string; + /** Status cards grid (activations + expiry) */ + statusGrid?: string; + /** Activations remaining card */ + activationsCard?: string; + /** License expiry card */ + expiryCard?: string; +} + export interface LicenseProps extends HTMLAttributes { /** Current license key value */ licenseKey: string; @@ -85,6 +132,8 @@ export interface LicenseProps extends HTMLAttributes { labels?: LicenseLabels; /** Format a date string for display (e.g., using dateI18n from @wordpress/date) */ formatDate?: (date: Date) => string; + /** Override Tailwind class names for individual sections of the component */ + classNames?: LicenseClassNames; } const maskKey = (key: string): string => { @@ -122,6 +171,7 @@ export function License({ pluginName = "Plugin", labels: labelOverrides = {}, formatDate = defaultFormatDate, + classNames: cx = {}, className, ...props }: LicenseProps) { @@ -197,39 +247,39 @@ export function License({ return (
-

{labels.title}

+

{labels.title}

{loading && ( -
+
- +
)} - + {/* Header section */} -
+
-

{labels.activationTitle}

+

{labels.activationTitle}

{hasActive && ( {labels.activeStatus} )}
-

+

{labels.activationDescription}

{headerImage && ( -
+
{headerImage}
)} @@ -237,18 +287,18 @@ export function License({
{/* Body section */} -
+
{/* Activate heading (shown when not active) */} {!hasActive && ( -
-
+
+
-

+

{labels.activateLicenseHeading}

- + {labels.activateLicenseSubheading}
@@ -256,7 +306,7 @@ export function License({ )} {/* License key label */} -
+
{labels.licenseKeyLabel}
@@ -271,7 +321,8 @@ export function License({ disabled={loading || hasActive} className={cn( "pr-10", - error && "border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20" + error && "border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20", + cx.input )} />