// static/shared/js/upgrade-prompts.js
/**
* Upgrade Prompts System
*
* Provides contextual upgrade prompts based on:
* - Usage limits approaching/reached
* - Locked features
*
* Usage:
*
* 1. Initialize the store (auto-loads usage on init):
*
*
* 2. Show limit warning banner:
*
*
*
*
* 3. Check before action:
*
*
* 4. Show upgrade CTA on dashboard:
*
*
*
*/
(function () {
'use strict';
const log = window.LogConfig?.log || console;
/**
* Upgrade Prompts Store
*/
const upgradeStore = {
// State
usage: null,
loading: false,
loaded: false,
error: null,
// Computed-like getters
get hasLimitsApproaching() {
return this.usage?.has_limits_approaching || false;
},
get hasLimitsReached() {
return this.usage?.has_limits_reached || false;
},
get hasUpgradeRecommendation() {
return this.usage?.upgrade_available && (this.hasLimitsApproaching || this.hasLimitsReached);
},
get upgradeReasons() {
return this.usage?.upgrade_reasons || [];
},
get currentTier() {
return this.usage?.tier || null;
},
get nextTier() {
return this.usage?.upgrade_tier || null;
},
/**
* Load usage data from API
*/
async loadUsage() {
if (this.loaded || this.loading) return;
try {
this.loading = true;
this.error = null;
const response = await apiClient.get('/vendor/usage');
this.usage = response;
this.loaded = true;
log.debug('[UpgradePrompts] Loaded usage data', this.usage);
} catch (error) {
log.error('[UpgradePrompts] Failed to load usage:', error);
this.error = error.message;
} finally {
this.loading = false;
}
},
/**
* Get usage metric by name
*/
getMetric(name) {
if (!this.usage?.usage) return null;
return this.usage.usage.find(m => m.name === name);
},
/**
* Check if should show limit warning for a metric
*/
shouldShowLimitWarning(metricName) {
const metric = this.getMetric(metricName);
return metric && (metric.is_approaching_limit || metric.is_at_limit);
},
/**
* Check if at limit for a metric
*/
isAtLimit(metricName) {
const metric = this.getMetric(metricName);
return metric?.is_at_limit || false;
},
/**
* Get percentage used for a metric
*/
getPercentage(metricName) {
const metric = this.getMetric(metricName);
return metric?.percentage || 0;
},
/**
* Get formatted usage string (e.g., "85/100")
*/
getUsageString(metricName) {
const metric = this.getMetric(metricName);
if (!metric) return '';
if (metric.is_unlimited) return `${metric.current} (unlimited)`;
return `${metric.current}/${metric.limit}`;
},
/**
* Get vendor code from URL
*/
getVendorCode() {
const path = window.location.pathname;
const segments = path.split('/').filter(Boolean);
if (segments[0] === 'vendor' && segments[1]) {
return segments[1];
}
return null;
},
/**
* Get billing URL
*/
getBillingUrl() {
const vendorCode = this.getVendorCode();
return vendorCode ? `/vendor/${vendorCode}/billing` : '#';
},
/**
* Check limit before action, show modal if at limit
*/
async checkLimitAndProceed(limitType, onSuccess) {
try {
const response = await apiClient.get(`/vendor/usage/check/${limitType}`);
if (response.can_proceed) {
if (typeof onSuccess === 'function') {
onSuccess();
}
return true;
} else {
// Show upgrade modal
this.showLimitReachedModal(limitType, response);
return false;
}
} catch (error) {
log.error('[UpgradePrompts] Failed to check limit:', error);
// Proceed anyway on error (fail open)
if (typeof onSuccess === 'function') {
onSuccess();
}
return true;
}
},
/**
* Show limit reached modal
*/
showLimitReachedModal(limitType, response) {
const limitNames = {
'orders': 'monthly orders',
'products': 'products',
'team_members': 'team members'
};
const limitName = limitNames[limitType] || limitType;
const message = response.message || `You've reached your ${limitName} limit.`;
// Use browser confirm for simplicity - could be replaced with custom modal
const shouldUpgrade = confirm(
`${message}\n\n` +
`Current: ${response.current}/${response.limit}\n\n` +
(response.upgrade_tier_name
? `Upgrade to ${response.upgrade_tier_name} to get more ${limitName}.\n\nGo to billing page?`
: 'Contact support for more capacity.')
);
if (shouldUpgrade && response.upgrade_tier_code) {
window.location.href = this.getBillingUrl();
}
},
/**
* Get limit warning banner HTML
*/
getLimitWarningHTML(metricName) {
const metric = this.getMetric(metricName);
if (!metric) return '';
const names = {
'orders': 'monthly orders',
'products': 'products',
'team_members': 'team members'
};
const name = names[metricName] || metricName;
if (metric.is_at_limit) {
return `
You've reached your ${name} limit (${metric.current}/${metric.limit})
Upgrade
`;
} else if (metric.is_approaching_limit) {
return `
You're approaching your ${name} limit (${metric.current}/${metric.limit} - ${Math.round(metric.percentage)}%)
Upgrade
`;
}
return '';
},
/**
* Get upgrade card HTML for dashboard
*/
getUpgradeCardHTML() {
if (!this.usage?.upgrade_tier) return '';
const tier = this.usage.upgrade_tier;
const reasons = this.usage.upgrade_reasons || [];
return `
Upgrade to ${tier.name}
${reasons.length > 0 ? `
${reasons.map(r => `- • ${r}
`).join('')}
` : ''}
${tier.benefits.length > 0 ? `
Get access to:
${tier.benefits.slice(0, 4).map(b => `
-
${b}
`).join('')}
` : ''}
€${(tier.price_monthly_cents / 100).toFixed(0)}
/month
Upgrade Now
`;
},
/**
* Get compact usage bar HTML
*/
getUsageBarHTML(metricName) {
const metric = this.getMetric(metricName);
if (!metric || metric.is_unlimited) return '';
const percentage = Math.min(metric.percentage, 100);
const colorClass = metric.is_at_limit
? 'bg-red-500'
: metric.is_approaching_limit
? 'bg-yellow-500'
: 'bg-green-500';
return `
${metric.current} / ${metric.limit}
${Math.round(percentage)}%
`;
},
/**
* Reload usage data
*/
async reload() {
try {
this.loaded = false;
await this.loadUsage();
} catch (error) {
log.error('[UpgradePrompts] Failed to reload:', error);
}
}
};
// Register Alpine store when Alpine is available
document.addEventListener('alpine:init', () => {
Alpine.store('upgrade', upgradeStore);
log.debug('[UpgradePrompts] Registered as Alpine store');
});
// Also expose globally
window.UpgradePrompts = upgradeStore;
})();