feat: Implement OAuth license activation flow

- Add LicenseConnect.tsx focused OAuth confirmation page in customer SPA
- Add /licenses/oauth/validate and /licenses/oauth/confirm API endpoints
- Update App.tsx to render license-connect outside BaseLayout (no header/footer)
- Add license_activation_method field to product settings in Admin SPA
- Create LICENSING_MODULE.md with comprehensive OAuth flow documentation
- Update API_ROUTES.md with license module endpoints
This commit is contained in:
Dwindi Ramadhana
2026-01-31 22:22:22 +07:00
parent d80f34c8b9
commit a0b5f8496d
23 changed files with 3218 additions and 806 deletions

View File

@@ -60,6 +60,7 @@ export interface PageItem {
slug?: string;
title: string;
url?: string;
isFrontPage?: boolean;
isSpaLanding?: boolean;
}

View File

@@ -40,6 +40,7 @@ export type ProductFormData = {
licensing_enabled?: boolean;
license_activation_limit?: string;
license_duration_days?: string;
license_activation_method?: '' | 'api' | 'oauth';
// Subscription
subscription_enabled?: boolean;
subscription_period?: 'day' | 'week' | 'month' | 'year';
@@ -95,6 +96,7 @@ export function ProductFormTabbed({
const [licensingEnabled, setLicensingEnabled] = useState(initial?.licensing_enabled || false);
const [licenseActivationLimit, setLicenseActivationLimit] = useState(initial?.license_activation_limit || '');
const [licenseDurationDays, setLicenseDurationDays] = useState(initial?.license_duration_days || '');
const [licenseActivationMethod, setLicenseActivationMethod] = useState<'' | 'api' | 'oauth'>(initial?.license_activation_method || '');
// Subscription state
const [subscriptionEnabled, setSubscriptionEnabled] = useState(initial?.subscription_enabled || false);
const [subscriptionPeriod, setSubscriptionPeriod] = useState<'day' | 'week' | 'month' | 'year'>(initial?.subscription_period || 'month');
@@ -131,6 +133,7 @@ export function ProductFormTabbed({
setLicensingEnabled(initial.licensing_enabled || false);
setLicenseActivationLimit(initial.license_activation_limit || '');
setLicenseDurationDays(initial.license_duration_days || '');
setLicenseActivationMethod(initial.license_activation_method || '');
// Subscription
setSubscriptionEnabled(initial.subscription_enabled || false);
setSubscriptionPeriod(initial.subscription_period || 'month');
@@ -199,6 +202,7 @@ export function ProductFormTabbed({
licensing_enabled: licensingEnabled,
license_activation_limit: licensingEnabled ? licenseActivationLimit : undefined,
license_duration_days: licensingEnabled ? licenseDurationDays : undefined,
license_activation_method: licensingEnabled ? licenseActivationMethod : undefined,
// Subscription
subscription_enabled: subscriptionEnabled,
subscription_period: subscriptionEnabled ? subscriptionPeriod : undefined,
@@ -261,6 +265,8 @@ export function ProductFormTabbed({
setLicenseActivationLimit={setLicenseActivationLimit}
licenseDurationDays={licenseDurationDays}
setLicenseDurationDays={setLicenseDurationDays}
licenseActivationMethod={licenseActivationMethod}
setLicenseActivationMethod={setLicenseActivationMethod}
subscriptionEnabled={subscriptionEnabled}
setSubscriptionEnabled={setSubscriptionEnabled}
subscriptionPeriod={subscriptionPeriod}

View File

@@ -50,6 +50,8 @@ type GeneralTabProps = {
setLicenseActivationLimit?: (value: string) => void;
licenseDurationDays?: string;
setLicenseDurationDays?: (value: string) => void;
licenseActivationMethod?: '' | 'api' | 'oauth';
setLicenseActivationMethod?: (value: '' | 'api' | 'oauth') => void;
// Subscription
subscriptionEnabled?: boolean;
setSubscriptionEnabled?: (value: boolean) => void;
@@ -95,6 +97,8 @@ export function GeneralTab({
setLicenseActivationLimit,
licenseDurationDays,
setLicenseDurationDays,
licenseActivationMethod,
setLicenseActivationMethod,
subscriptionEnabled,
setSubscriptionEnabled,
subscriptionPeriod,
@@ -498,6 +502,27 @@ export function GeneralTab({
</p>
</div>
</div>
{setLicenseActivationMethod && (
<div>
<Label className="text-xs">{__('Activation Method')}</Label>
<Select
value={licenseActivationMethod || 'default'}
onValueChange={(v) => setLicenseActivationMethod(v === 'default' ? '' : v as '' | 'api' | 'oauth')}
>
<SelectTrigger className="mt-1">
<SelectValue placeholder={__('Use Site Default')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">{__('Use Site Default')}</SelectItem>
<SelectItem value="api">{__('Simple API (license key only)')}</SelectItem>
<SelectItem value="oauth">{__('Secure OAuth (requires login)')}</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground mt-1">
{__('Override site-level activation method for this product')}
</p>
</div>
)}
</div>
)}
</>