v1.7.0: piani Basic/Pro/Premium/Annual + quota giornaliera

Pricing model: subscription mensili (Basic 2.99 / Pro 5.99 / Premium 9.99)
piu' Annual 49.90 one-time. Feature gating per piano (Basic = solo audio,
Pro+ = tutto). Quota giornaliera (10/30/150) con reset mezzanotte Europe/Rome.

Server: tabella daily_usage, endpoint /api/usage/consume e /status,
webhook LS gestisce subscription_* events + mappatura variant_id -> plan
via .env (LS_VARIANT_BASIC|PRO|PREMIUM|ANNUAL).

Client: gating tab in sidebar in base ai features del piano, contatore
"X/Y oggi" + bottone Upgrade, modal blocco con CTA verso landing.
This commit is contained in:
luzadev committed 2026-06-09 11:02:42 +02:00
1 parent bcd4f22b1e
commit 61459099f9
15 files changed
+1395 -108

No files matched your search

+66 -8
View File
@@ -1,21 +1,72 @@
/**
* Invio email transazionali via Resend (https://resend.com).
* Usa fetch nativo di Node >= 20 — nessuna dipendenza.
* Invio email transazionali via Resend.
*
* sendLicenseEmail(to, key, { plan, expiresAt, periodEnd })
* - plan: codice piano ("basic" | "pro" | "premium" | "annual")
* - expiresAt: epoch seconds (annual one-time) o null
* - periodEnd: epoch seconds (subscription mensile) o null
*
* Variabili env richieste:
* RESEND_API_KEY dal dashboard Resend
* EMAIL_FROM "MusicTools <noreply@djluza.com>" (dominio verificato)
*/
import { getPlan } from "./plans.js";
const FROM = process.env.EMAIL_FROM || "MusicTools <noreply@djluza.com>";
const BASE = process.env.PUBLIC_BASE_URL || "https://musictools.djluza.com";
export async function sendLicenseEmail(to, licenseKey) {
function fmtItalianDate(epochSeconds) {
if (!epochSeconds) return "";
const d = new Date(Number(epochSeconds) * 1000);
return new Intl.DateTimeFormat("it-IT", {
timeZone: "Europe/Rome",
day: "2-digit",
month: "long",
year: "numeric",
}).format(d);
}
function describePlan(planCode, expiresAt, periodEnd) {
const p = getPlan(planCode);
if (!p) {
return {
name: "MusicTools",
summary: "La tua licenza e' attiva.",
};
}
const features = {
audio: "download brani audio",
video: "download video",
record: "registrazione audio",
metadata: "editor metadati",
upgrade: "upgrade qualita' libreria",
};
const featList = p.features.map((f) => features[f] || f).join(", ");
const limit = p.daily_limit === null
? "senza limiti giornalieri"
: `fino a ${p.daily_limit} download al giorno`;
let validity = "";
if (planCode === "annual" && expiresAt) {
validity = `Validita': 1 anno, scade il <strong>${fmtItalianDate(expiresAt)}</strong>.`;
} else if (p.is_subscription && periodEnd) {
validity = `Prossimo rinnovo: <strong>${fmtItalianDate(periodEnd)}</strong>.`;
}
return {
name: p.name,
summary: `Piano <strong>${p.name}</strong> &mdash; ${limit}. Funzioni incluse: ${featList}. ${validity}`,
};
}
export async function sendLicenseEmail(to, licenseKey, opts = {}) {
if (!process.env.RESEND_API_KEY) {
console.warn("[email] RESEND_API_KEY non impostata, skip invio a", to);
return;
}
const { plan: planCode, expiresAt, periodEnd } = opts;
const planInfo = describePlan(planCode, expiresAt, periodEnd);
const q = `key=${encodeURIComponent(licenseKey)}&email=${encodeURIComponent(to)}`;
const macUrl = `${BASE}/api/download/macos?${q}`;
const winUrl = `${BASE}/api/download/windows?${q}`;
@@ -23,8 +74,11 @@ export async function sendLicenseEmail(to, licenseKey) {
const html = `
<div style="font-family:-apple-system,Segoe UI,sans-serif;max-width:560px;margin:0 auto;padding:24px;color:#111">
<h1 style="color:#1db954;margin:0 0 14px;font-size:22px">Grazie per aver scelto MusicTools!</h1>
<p style="font-size:15px;line-height:1.55;margin:0 0 14px">
${planInfo.summary}
</p>
<p style="font-size:15px;line-height:1.55;margin:0 0 18px">
Ecco la tua chiave di licenza. Conservala con cura — ti servira' per attivare l'app.
Ecco la tua chiave di licenza. Conservala con cura &mdash; ti servira' per attivare l'app.
</p>
<p style="font-size:22px;letter-spacing:2px;font-family:monospace;background:#f4f4f4;padding:16px;border-radius:10px;text-align:center;margin:0 0 24px;color:#000">
${licenseKey}
@@ -35,12 +89,12 @@ export async function sendLicenseEmail(to, licenseKey) {
<tr>
<td style="padding-right:10px">
<a href="${macUrl}" style="display:inline-block;background:#1db954;color:#fff;text-decoration:none;padding:12px 22px;border-radius:999px;font-weight:700;font-size:14px">
🍎 Scarica per Mac
Scarica per Mac
</a>
</td>
<td>
<a href="${winUrl}" style="display:inline-block;background:#1db954;color:#fff;text-decoration:none;padding:12px 22px;border-radius:999px;font-weight:700;font-size:14px">
🪟 Scarica per Windows
Scarica per Windows
</a>
</td>
</tr>
@@ -49,7 +103,7 @@ export async function sendLicenseEmail(to, licenseKey) {
<p style="font-size:15px;margin:0 0 8px"><strong>Come attivare:</strong></p>
<ol style="font-size:14.5px;line-height:1.6;padding-left:22px">
<li>Clicca uno dei bottoni qui sopra per scaricare l'installer</li>
<li>Installa l'app (su Mac: tasto destro → Apri → Apri; su Windows: Esegui comunque)</li>
<li>Installa l'app (su Mac: tasto destro &rarr; Apri &rarr; Apri; su Windows: Esegui comunque)</li>
<li>Apri MusicTools: la prima schermata ti chiede email e chiave</li>
<li>Inserisci <strong>${to}</strong> e la chiave qui sopra</li>
</ol>
@@ -62,6 +116,10 @@ export async function sendLicenseEmail(to, licenseKey) {
</div>
`;
const subject = planInfo.name === "MusicTools"
? "La tua licenza MusicTools"
: `La tua licenza MusicTools ${planInfo.name}`;
const resp = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
@@ -71,7 +129,7 @@ export async function sendLicenseEmail(to, licenseKey) {
body: JSON.stringify({
from: FROM,
to: [to],
subject: "La tua licenza MusicTools",
subject,
html,
}),
});