// static/shared/js/feature-store.js /** * Feature Store for Alpine.js * * Provides feature availability checking for tier-based access control. * Loads features from the API on init and caches them for the session. * * Usage in templates: * * 1. Check if feature is available: *
* Analytics content here *
* * 2. Show upgrade prompt if not available: *
*

Upgrade to access Analytics

*
* * 3. Conditionally render with x-if: * * * 4. Use feature data for upgrade prompts: *

* * 5. Get current tier info: * */ (function () { 'use strict'; // Use centralized logger if available const log = window.LogConfig?.log || console; /** * Feature Store */ const featureStore = { // State features: [], // Array of feature codes available to vendor featuresMap: {}, // Full feature info keyed by code tierCode: null, // Current tier code tierName: null, // Current tier name loading: true, // Loading state loaded: false, // Whether features have been loaded error: null, // Error message if load failed /** * Initialize the feature store * Called automatically when Alpine starts */ async init() { // Guard against multiple initialization if (window._featureStoreInitialized) return; window._featureStoreInitialized = true; try { log.debug('[FeatureStore] Initializing...'); await this.loadFeatures(); } catch (error) { log.error('[FeatureStore] Failed to initialize:', error); } }, /** * Load features from API */ async loadFeatures() { // Don't reload if already loaded if (this.loaded) { log.debug('[FeatureStore] Already loaded, skipping'); return; } // Get vendor code from URL const vendorCode = this.getVendorCode(); if (!vendorCode) { log.warn('[FeatureStore] No vendor code found in URL'); this.loading = false; return; } try { this.loading = true; this.error = null; // Fetch available features (lightweight endpoint) const response = await apiClient.get('/vendor/features/available'); this.features = response.features || []; this.tierCode = response.tier_code; this.tierName = response.tier_name; this.loaded = true; log.debug(`[FeatureStore] Loaded ${this.features.length} features for ${this.tierName} tier`); } catch (error) { log.error('[FeatureStore] Failed to load features:', error); this.error = error.message || 'Failed to load features'; // Set empty array so checks don't fail this.features = []; } finally { this.loading = false; } }, /** * Load full feature details (with metadata) * Use this when you need upgrade info */ async loadFullFeatures() { const vendorCode = this.getVendorCode(); if (!vendorCode) return; try { const response = await apiClient.get('/vendor/features'); // Build map for quick lookup this.featuresMap = {}; for (const feature of response.features) { this.featuresMap[feature.code] = feature; } log.debug(`[FeatureStore] Loaded full details for ${response.features.length} features`); } catch (error) { log.error('[FeatureStore] Failed to load full features:', error); } }, /** * Check if vendor has access to a feature * @param {string} featureCode - The feature code to check * @returns {boolean} - Whether the feature is available */ has(featureCode) { return this.features.includes(featureCode); }, /** * Check if vendor has access to ANY of the given features * @param {...string} featureCodes - Feature codes to check * @returns {boolean} - Whether any feature is available */ hasAny(...featureCodes) { return featureCodes.some(code => this.has(code)); }, /** * Check if vendor has access to ALL of the given features * @param {...string} featureCodes - Feature codes to check * @returns {boolean} - Whether all features are available */ hasAll(...featureCodes) { return featureCodes.every(code => this.has(code)); }, /** * Get feature info (requires loadFullFeatures first) * @param {string} featureCode - The feature code * @returns {object|null} - Feature info or null */ getFeature(featureCode) { return this.featuresMap[featureCode] || null; }, /** * Get the tier name required for a feature * @param {string} featureCode - The feature code * @returns {string|null} - Tier name or null */ getUpgradeTier(featureCode) { const feature = this.getFeature(featureCode); return feature?.minimum_tier_name || null; }, /** * Get vendor code from URL * @returns {string|null} */ getVendorCode() { const path = window.location.pathname; const segments = path.split('/').filter(Boolean); if (segments[0] === 'vendor' && segments[1]) { return segments[1]; } return null; }, /** * Reload features (e.g., after tier change) */ async reload() { try { this.loaded = false; this.features = []; this.featuresMap = {}; await this.loadFeatures(); } catch (error) { log.error('[FeatureStore] Failed to reload:', error); } } }; // Register Alpine store when Alpine is available document.addEventListener('alpine:init', () => { Alpine.store('features', featureStore); log.debug('[FeatureStore] Registered as Alpine store'); }); // Also expose globally for non-Alpine usage window.FeatureStore = featureStore; })();