From 5bc39d0692f33463a5bdd2d4489f74039be67702 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Atakan=20Do=C4=9Fan=20=C3=96zban?=
+ Separately from monthly credits, Free accounts receive{" "} + 2 lifetime watermark-free successful video renders. After those renders, + subsequent Free-tier videos include a Songs2VID brand watermark (bottom-right) unless you + upgrade to Pro or Enterprise terms that remove that requirement. The lifetime count is based + on successfully completed encodes, not monthly credit resets. +
diff --git a/components/PricingSection.tsx b/components/PricingSection.tsx index 3b4a3c5..43ae681 100644 --- a/components/PricingSection.tsx +++ b/components/PricingSection.tsx @@ -61,11 +61,11 @@ const PLANS: PlanConfig[] = [ "API support": "Not included", "ID3 tags": "Auto-fill title & metadata from MP3 tags", Support: "Community", - Watermark: "Optional Songs2VID badge · bottom-right", + Watermark: "2 clean videos, then Songs2VID badge · bottom-right", }, infrastructureChips: ["cloud"], watermarkNote: - `Show "${getVideoAttributionText()}" to support open-source development - opt out anytime for a clean video.`, + `First 2 successful Free renders are watermark-free; later Free renders include the "${getVideoAttributionText()}" badge (bottom-right). Upgrade to Pro for unwatermarked videos.`, cta: { type: "signin" }, highlighted: false, hover: @@ -323,7 +323,8 @@ export function PricingSection() {
*Free includes 10 videos/month; buy 1–15 extras at €0.25 each (max 15 extras). After both are used, Pro (€5/mo · 50 videos) is required. Total balance capped at 30. Deduction - order: monthly first, then extras. + order: monthly first, then extras. Free also includes 2 lifetime watermark-free + successful videos; later Free renders include a Songs2VID watermark unless you upgrade.
**Technical support is strictly reserved for Managed Cloud and paid Professional Setup diff --git a/components/UploadForm.tsx b/components/UploadForm.tsx index e466571..376b93b 100644 --- a/components/UploadForm.tsx +++ b/components/UploadForm.tsx @@ -18,6 +18,8 @@ import { PrivacyToggle } from "./PrivacyToggle"; import { ResolutionSelect } from "./ResolutionSelect"; import { QuotaErrorMessage, UpgradeProLink } from "./UpgradeProLink"; import { UpgradeProButton } from "./UpgradeProButton"; +import { WatermarkPaywallModal } from "./WatermarkPaywallModal"; +import { FREE_UNWATERMARKED_VIDEO_LIMIT } from "@/lib/watermark-policy"; const UPLOAD_CONCURRENCY = 4; @@ -82,7 +84,12 @@ export function UploadForm() { customWatermark?: boolean; perItemImages?: boolean; artTrackLayouts?: boolean; + subscriptionStatus?: "free" | "pro"; + createdVideoCount?: number; + watermarkFreeRemaining?: number | null; } | null>(null); + const [showWatermarkPaywall, setShowWatermarkPaywall] = useState(false); + const [watermarkDefaultsApplied, setWatermarkDefaultsApplied] = useState(false); const isPro = Boolean(quota?.selfHosted || quota?.plan === "PREMIUM"); const canPerItemImages = Boolean(quota?.perItemImages || isPro); @@ -107,6 +114,12 @@ export function UploadForm() { customWatermark: Boolean(data.customWatermark), perItemImages: Boolean(data.perItemImages), artTrackLayouts: Boolean(data.artTrackLayouts), + subscriptionStatus: data.subscriptionStatus === "pro" ? "pro" : "free", + createdVideoCount: typeof data.createdVideoCount === "number" ? data.createdVideoCount : 0, + watermarkFreeRemaining: + data.watermarkFreeRemaining === null || data.watermarkFreeRemaining === undefined + ? null + : Number(data.watermarkFreeRemaining), }); } }, []); @@ -115,6 +128,21 @@ export function UploadForm() { loadQuota(); }, [loadQuota]); + // Align free-tier watermark default with remaining watermark-free slots (once). + useEffect(() => { + if (!quota || watermarkDefaultsApplied) return; + if (quota.selfHosted || quota.plan === "PREMIUM") { + setWatermarkDefaultsApplied(true); + return; + } + const remaining = quota.watermarkFreeRemaining ?? 0; + setJobWatermark({ + ...DEFAULT_WATERMARK, + mode: remaining > 0 ? "none" : "default", + }); + setWatermarkDefaultsApplied(true); + }, [quota, watermarkDefaultsApplied]); + useEffect(() => { return () => { if (imagePreviewUrl) URL.revokeObjectURL(imagePreviewUrl); @@ -383,14 +411,32 @@ export function UploadForm() { ) && !submitting; - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); + async function submitJob() { if (!canSubmit || !imagePath) return; setSubmitting(true); setError(null); try { + const forcedWatermark = + !isPro && + !quota?.selfHosted && + (quota?.createdVideoCount ?? 0) >= FREE_UNWATERMARKED_VIDEO_LIMIT; + const effectiveWatermark: WatermarkSettings = forcedWatermark + ? { ...DEFAULT_WATERMARK, mode: "default" } + : canCustomWatermark + ? jobWatermark + : { + mode: jobWatermark.mode === "none" ? "none" : "default", + position: "bottom-right", + offsetX: 20, + offsetY: 20, + text: null, + logoPath: null, + fontKey: "system", + fontPath: null, + }; + const res = await fetch("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -402,15 +448,8 @@ export function UploadForm() { metadata: { ...item.metadata, imagePath: canPerItemImages ? item.itemImagePath || item.metadata.imagePath || null : null, - includeWatermark: jobWatermark.mode !== "none", - watermark: canCustomWatermark - ? jobWatermark - : { - mode: jobWatermark.mode === "none" ? "none" : "default", - position: "bottom-right", - offsetX: 20, - offsetY: 20, - }, + includeWatermark: effectiveWatermark.mode !== "none", + watermark: effectiveWatermark, layout: canArtTrackLayouts ? jobLayout : { ...DEFAULT_LAYOUT }, }, })), @@ -432,10 +471,51 @@ export function UploadForm() { } } + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!canSubmit || !imagePath) return; + + const needsPaywall = + !isPro && + !quota?.selfHosted && + (quota?.createdVideoCount ?? 0) >= FREE_UNWATERMARKED_VIDEO_LIMIT; + + if (needsPaywall) { + setShowWatermarkPaywall(true); + return; + } + + await submitJob(); + } + + function watermarkStatusLabel(): string | null { + if (!quota || quota.selfHosted) return null; + if (quota.plan === "PREMIUM" || quota.subscriptionStatus === "pro") { + return "Pro Member (No Watermark)"; + } + const remaining = quota.watermarkFreeRemaining ?? 0; + if (remaining > 0) { + return `Free Tier: ${remaining}/${FREE_UNWATERMARKED_VIDEO_LIMIT} Watermark-Free Renders Left`; + } + return "Free Tier (Watermark Active)"; + } + return (