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:
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api/client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -19,114 +20,115 @@ interface WishlistItem {
|
||||
const GUEST_WISHLIST_KEY = 'woonoow_guest_wishlist';
|
||||
|
||||
export function useWishlist() {
|
||||
const [items, setItems] = useState<WishlistItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [productIds, setProductIds] = useState<Set<number>>(new Set());
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const [guestIds, setGuestIds] = useState<Set<number>>(new Set());
|
||||
|
||||
// Check if wishlist is enabled (default true if not explicitly set to false)
|
||||
const settings = (window as any).woonoowCustomer?.settings;
|
||||
const isEnabled = settings?.wishlist_enabled !== false;
|
||||
const isLoggedIn = (window as any).woonoowCustomer?.user?.isLoggedIn;
|
||||
|
||||
// Load guest wishlist from localStorage
|
||||
const loadGuestWishlist = useCallback(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(GUEST_WISHLIST_KEY);
|
||||
if (stored) {
|
||||
const guestIds = JSON.parse(stored) as number[];
|
||||
setProductIds(new Set(guestIds));
|
||||
// Load guest wishlist on mount
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
try {
|
||||
const stored = localStorage.getItem(GUEST_WISHLIST_KEY);
|
||||
if (stored) {
|
||||
const ids = JSON.parse(stored) as number[];
|
||||
setGuestIds(new Set(ids));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load guest wishlist:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load guest wishlist:', error);
|
||||
}
|
||||
}, []);
|
||||
}, [isLoggedIn]);
|
||||
|
||||
// Save guest wishlist to localStorage
|
||||
// Save guest wishlist helper
|
||||
const saveGuestWishlist = useCallback((ids: Set<number>) => {
|
||||
try {
|
||||
localStorage.setItem(GUEST_WISHLIST_KEY, JSON.stringify(Array.from(ids)));
|
||||
setGuestIds(ids);
|
||||
} catch (error) {
|
||||
console.error('Failed to save guest wishlist:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load wishlist on mount
|
||||
useEffect(() => {
|
||||
if (isEnabled) {
|
||||
if (isLoggedIn) {
|
||||
loadWishlist();
|
||||
} else {
|
||||
loadGuestWishlist();
|
||||
}
|
||||
}
|
||||
}, [isEnabled, isLoggedIn]);
|
||||
// Fetch wishlist items (Server)
|
||||
const { data: serverItems = [], isLoading: isServerLoading } = useQuery({
|
||||
queryKey: ['wishlist'],
|
||||
queryFn: async () => {
|
||||
return await api.get<WishlistItem[]>('/account/wishlist');
|
||||
},
|
||||
enabled: isEnabled && isLoggedIn,
|
||||
staleTime: 60 * 1000, // 1 minute cache
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const loadWishlist = useCallback(async () => {
|
||||
if (!isLoggedIn) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await api.get<WishlistItem[]>('/account/wishlist');
|
||||
setItems(data);
|
||||
setProductIds(new Set(data.map(item => item.product_id)));
|
||||
} catch (error) {
|
||||
console.error('Failed to load wishlist:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
// Calculate merged state
|
||||
const items = isLoggedIn ? serverItems : []; // Guest items not stored as full objects here, usually handled by fetching products by ID elsewhere
|
||||
const isLoading = isLoggedIn ? isServerLoading : false;
|
||||
|
||||
const addToWishlist = useCallback(async (productId: number) => {
|
||||
// Guest mode: store in localStorage only
|
||||
if (!isLoggedIn) {
|
||||
const newIds = new Set(productIds);
|
||||
newIds.add(productId);
|
||||
setProductIds(newIds);
|
||||
saveGuestWishlist(newIds);
|
||||
// Compute set of IDs for O(1) lookup
|
||||
const productIds = useMemo(() => {
|
||||
if (isLoggedIn) {
|
||||
return new Set(serverItems.map(item => item.product_id));
|
||||
}
|
||||
return guestIds;
|
||||
}, [isLoggedIn, serverItems, guestIds]);
|
||||
|
||||
// Mutations
|
||||
const addMutation = useMutation({
|
||||
mutationFn: async (productId: number) => {
|
||||
return await api.post('/account/wishlist', { product_id: productId });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['wishlist'] });
|
||||
toast.success('Added to wishlist');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Logged in: use API
|
||||
try {
|
||||
await api.post('/account/wishlist', { product_id: productId });
|
||||
await loadWishlist(); // Reload to get full product details
|
||||
toast.success('Added to wishlist');
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
},
|
||||
onError: (error: any) => {
|
||||
const message = error?.message || 'Failed to add to wishlist';
|
||||
toast.error(message);
|
||||
return false;
|
||||
}
|
||||
}, [isLoggedIn, productIds, loadWishlist, saveGuestWishlist]);
|
||||
});
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: async (productId: number) => {
|
||||
return await api.delete(`/account/wishlist/${productId}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['wishlist'] });
|
||||
toast.success('Removed from wishlist');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to remove from wishlist');
|
||||
}
|
||||
});
|
||||
|
||||
const addToWishlist = useCallback(async (productId: number) => {
|
||||
if (!isLoggedIn) {
|
||||
const newIds = new Set(guestIds);
|
||||
newIds.add(productId);
|
||||
saveGuestWishlist(newIds);
|
||||
toast.success('Added to wishlist');
|
||||
return true;
|
||||
}
|
||||
|
||||
await addMutation.mutateAsync(productId);
|
||||
return true;
|
||||
}, [isLoggedIn, guestIds, saveGuestWishlist, addMutation]);
|
||||
|
||||
const removeFromWishlist = useCallback(async (productId: number) => {
|
||||
// Guest mode: remove from localStorage only
|
||||
if (!isLoggedIn) {
|
||||
const newIds = new Set(productIds);
|
||||
const newIds = new Set(guestIds);
|
||||
newIds.delete(productId);
|
||||
setProductIds(newIds);
|
||||
saveGuestWishlist(newIds);
|
||||
toast.success('Removed from wishlist');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Logged in: use API
|
||||
try {
|
||||
await api.delete(`/account/wishlist/${productId}`);
|
||||
setItems(items.filter(item => item.product_id !== productId));
|
||||
setProductIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(productId);
|
||||
return newSet;
|
||||
});
|
||||
toast.success('Removed from wishlist');
|
||||
return true;
|
||||
} catch (error) {
|
||||
toast.error('Failed to remove from wishlist');
|
||||
return false;
|
||||
}
|
||||
}, [isLoggedIn, productIds, items, saveGuestWishlist]);
|
||||
await removeMutation.mutateAsync(productId);
|
||||
return true;
|
||||
}, [isLoggedIn, guestIds, saveGuestWishlist, removeMutation]);
|
||||
|
||||
const toggleWishlist = useCallback(async (productId: number) => {
|
||||
if (productIds.has(productId)) {
|
||||
@@ -145,12 +147,12 @@ export function useWishlist() {
|
||||
isLoading,
|
||||
isEnabled,
|
||||
isLoggedIn,
|
||||
count: items.length,
|
||||
count: productIds.size,
|
||||
productIds,
|
||||
addToWishlist,
|
||||
removeFromWishlist,
|
||||
toggleWishlist,
|
||||
isInWishlist,
|
||||
refresh: loadWishlist,
|
||||
refresh: () => queryClient.invalidateQueries({ queryKey: ['wishlist'] }),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user