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:
@@ -98,25 +98,47 @@ export default function Product() {
|
||||
if (!v.attributes) return false;
|
||||
|
||||
return Object.entries(selectedAttributes).every(([attrName, attrValue]) => {
|
||||
const normalizedValue = attrValue.toLowerCase().trim();
|
||||
const normalizedSelectedValue = attrValue.toLowerCase().trim();
|
||||
const attrNameLower = attrName.toLowerCase();
|
||||
|
||||
// Check all attribute keys in variation (case-insensitive)
|
||||
for (const [vKey, vValue] of Object.entries(v.attributes)) {
|
||||
const vKeyLower = vKey.toLowerCase();
|
||||
const attrNameLower = attrName.toLowerCase();
|
||||
// Find the attribute definition to get the slug
|
||||
const attrDef = product.attributes?.find((a: any) => a.name === attrName);
|
||||
const attrSlug = attrDef?.slug || attrNameLower.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
|
||||
if (vKeyLower === `attribute_${attrNameLower}` ||
|
||||
vKeyLower === `attribute_pa_${attrNameLower}` ||
|
||||
vKeyLower === attrNameLower) {
|
||||
// Try to find a matching key in the variation attributes
|
||||
let variationValue: string | undefined = undefined;
|
||||
|
||||
const varValueNormalized = String(vValue).toLowerCase().trim();
|
||||
if (varValueNormalized === normalizedValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Check for common WooCommerce attribute key formats
|
||||
// 1. Check strict slug format (attribute_7-days-...)
|
||||
if (`attribute_${attrSlug}` in v.attributes) {
|
||||
variationValue = v.attributes[`attribute_${attrSlug}`];
|
||||
}
|
||||
// 2. Check pa_ format (attribute_pa_color)
|
||||
else if (`attribute_pa_${attrSlug}` in v.attributes) {
|
||||
variationValue = v.attributes[`attribute_pa_${attrSlug}`];
|
||||
}
|
||||
// 3. Fallback to name-based checks (legacy)
|
||||
else if (`attribute_${attrNameLower}` in v.attributes) {
|
||||
variationValue = v.attributes[`attribute_${attrNameLower}`];
|
||||
} else if (`attribute_pa_${attrNameLower}` in v.attributes) {
|
||||
variationValue = v.attributes[`attribute_pa_${attrNameLower}`];
|
||||
} else if (attrNameLower in v.attributes) {
|
||||
variationValue = v.attributes[attrNameLower];
|
||||
}
|
||||
|
||||
return false;
|
||||
// If key is undefined/missing in variation, it means "Any" -> Match
|
||||
if (variationValue === undefined || variationValue === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If empty string, it also means "Any" -> Match
|
||||
const normalizedVarValue = String(variationValue).toLowerCase().trim();
|
||||
if (normalizedVarValue === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, values must match
|
||||
return normalizedVarValue === normalizedSelectedValue;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -181,11 +203,36 @@ export default function Product() {
|
||||
}
|
||||
}
|
||||
|
||||
// Construct variation params using keys from the matched variation
|
||||
// but filling in values from user selection (handles "Any" variations with empty values)
|
||||
let variation_params: Record<string, string> = {};
|
||||
if (product.type === 'variable' && selectedVariation?.attributes) {
|
||||
// Get keys from the variation's attributes (these are the correct WooCommerce keys)
|
||||
Object.keys(selectedVariation.attributes).forEach(key => {
|
||||
// Key format is like "attribute_7-days-auto-closing-variation-plan"
|
||||
// Extract the slug part after "attribute_"
|
||||
const slug = key.replace(/^attribute_/, '');
|
||||
|
||||
// Find the matching user-selected value by attribute name
|
||||
const attrDef = product.attributes?.find((a: any) =>
|
||||
a.slug === slug || a.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') === slug
|
||||
);
|
||||
|
||||
if (attrDef && selectedAttributes[attrDef.name]) {
|
||||
variation_params[key] = selectedAttributes[attrDef.name];
|
||||
} else {
|
||||
// Fallback to stored value if no user selection
|
||||
variation_params[key] = selectedVariation.attributes[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(apiClient.endpoints.cart.add, {
|
||||
product_id: product.id,
|
||||
quantity,
|
||||
variation_id: selectedVariation?.id || 0,
|
||||
variation: variation_params,
|
||||
});
|
||||
|
||||
addItem({
|
||||
@@ -320,8 +367,8 @@ export default function Product() {
|
||||
key={index}
|
||||
onClick={() => setSelectedImage(img)}
|
||||
className={`w-2 h-2 rounded-full transition-all ${selectedImage === img
|
||||
? 'bg-primary w-6'
|
||||
: 'bg-gray-300 hover:bg-gray-400'
|
||||
? 'bg-primary w-6'
|
||||
: 'bg-gray-300 hover:bg-gray-400'
|
||||
}`}
|
||||
aria-label={`View image ${index + 1}`}
|
||||
/>
|
||||
@@ -354,8 +401,8 @@ export default function Product() {
|
||||
key={index}
|
||||
onClick={() => setSelectedImage(img)}
|
||||
className={`flex-shrink-0 w-24 h-24 md:w-28 md:h-28 rounded-lg overflow-hidden border-2 transition-all shadow-md hover:shadow-lg ${selectedImage === img
|
||||
? 'border-primary ring-4 ring-primary ring-offset-2'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
? 'border-primary ring-4 ring-primary ring-offset-2'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
@@ -446,8 +493,8 @@ export default function Product() {
|
||||
key={optIndex}
|
||||
onClick={() => handleAttributeChange(attr.name, option)}
|
||||
className={`min-w-[48px] min-h-[48px] px-5 py-3 rounded-xl border-2 font-medium transition-all ${isSelected
|
||||
? 'bg-gray-900 text-white border-gray-900 shadow-lg'
|
||||
: 'bg-white text-gray-700 border-gray-200 hover:border-gray-400 hover:shadow-md'
|
||||
? 'bg-gray-900 text-white border-gray-900 shadow-lg'
|
||||
: 'bg-white text-gray-700 border-gray-200 hover:border-gray-400 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
@@ -503,8 +550,8 @@ export default function Product() {
|
||||
<button
|
||||
onClick={() => product && toggleWishlist(product.id)}
|
||||
className={`w-full h-14 flex items-center justify-center gap-2 rounded-xl font-semibold text-base border-2 transition-all ${product && isInWishlist(product.id)
|
||||
? 'bg-red-50 text-red-600 border-red-200 hover:border-red-400'
|
||||
: 'bg-white text-gray-900 border-gray-200 hover:border-gray-400'
|
||||
? 'bg-red-50 text-red-600 border-red-200 hover:border-red-400'
|
||||
: 'bg-white text-gray-900 border-gray-200 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<Heart className={`h-5 w-5 ${product && isInWishlist(product.id) ? 'fill-red-500' : ''
|
||||
|
||||
Reference in New Issue
Block a user