Unlock Patterns Implementation
Implement optional paid unlocks: bulk processing, export formats, saved history, and ad-free experience.
Pattern 1: Bulk Processing
Free: 1 item at a time. Paid: Process 50+ items in bulk.
Implementation
import { isLicenseValid } from '@/utils/licenseStorage';
import { toast } from 'sonner';
export function BulkProcessor() {
const [items, setItems] = useState<string[]>([]);
const isPaid = isLicenseValid();
const handleProcess = () => {
// Free users: 1 item only
const limit = isPaid ? items.length : 1;
const itemsToProcess = items.slice(0, limit);
if (!isPaid && items.length > 1) {
toast.info(
`Free: 1 item at a time. Unlock bulk processing for ${items.length} items!`,
{
action: {
label: 'Unlock',
onClick: () => window.location.href = config.monetization.checkoutUrl,
},
}
);
}
// Process items
const results = itemsToProcess.map(item => processItem(item));
return results;
};
return (
<div>
<textarea
placeholder="Enter items (one per line)"
onChange={(e) => setItems(e.target.value.split('\n').filter(Boolean))}
/>
<Button onClick={handleProcess}>
Process {isPaid ? 'All' : '1 Item'}
</Button>
{!isPaid && items.length > 1 && (
<span className="text-sm text-gray-600">
{items.length} items detected. Unlock to process all at once.
</span>
)}
</div>
);
}Pattern 2: Export Format Unlocks
Free: Copy to clipboard (always free). Paid: Export as CSV, JSON, PDF.
Implementation
import { Button } from '@/components/ui/button';
import { Download, Copy } from 'lucide-react';
import { isLicenseValid } from '@/utils/licenseStorage';
import { toast } from 'sonner';
export function ExportButtons({ data, onUpgradeClick }) {
const isPaid = isLicenseValid();
// Always free: Copy to clipboard
const handleCopy = () => {
navigator.clipboard.writeText(JSON.stringify(data, null, 2));
toast.success('Copied to clipboard!');
};
// Paid: Export as CSV
const handleExportCSV = () => {
if (!isPaid) {
toast.info('Unlock to export as CSV');
onUpgradeClick();
return;
}
const csv = convertToCSV(data);
downloadFile(csv, 'export.csv', 'text/csv');
};
return (
<div className="flex gap-2">
{/* Always Free */}
<Button onClick={handleCopy} variant="outline">
<Copy className="w-4 h-4 mr-2" />
Copy
</Button>
{/* Paid Unlocks */}
<Button
onClick={handleExportCSV}
disabled={!isPaid}
>
<Download className="w-4 h-4 mr-2" />
Export CSV {!isPaid && '🔒'}
</Button>
</div>
);
}
function downloadFile(content: string, filename: string, mimeType: string) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}Pattern 3: Saved History
Free: One-off usage. Paid: Save results history and access anytime.
Storage Utilities
// utils/historyStorage.ts
export interface HistoryItem {
id: string;
input: string;
output: string;
timestamp: number;
}
export function saveToHistory(input: string, output: string, isPaid: boolean): boolean {
if (!isPaid) {
return false; // History is paid feature
}
const history = getHistory();
const item: HistoryItem = {
id: Date.now().toString(),
input,
output,
timestamp: Date.now(),
};
history.unshift(item);
localStorage.setItem('tool_history', JSON.stringify(history.slice(0, 50))); // Keep last 50
return true;
}
export function getHistory(): HistoryItem[] {
const data = localStorage.getItem('tool_history');
return data ? JSON.parse(data) : [];
}History Panel Component
export function HistoryPanel({ isPaid, onUpgradeClick }) {
const [history, setHistory] = useState<HistoryItem[]>([]);
useEffect(() => {
if (isPaid) {
setHistory(getHistory());
}
}, [isPaid]);
if (!isPaid) {
return (
<div className="border-2 border-dashed rounded-lg p-8 text-center">
<h3 className="text-lg font-semibold mb-2">Save Your Work</h3>
<p className="text-gray-600 mb-4">
Unlock to save history and access your past results anytime
</p>
<Button onClick={onUpgradeClick}>
Unlock History Feature
</Button>
</div>
);
}
return (
<div>
<h3 className="text-lg font-semibold mb-4">Your History</h3>
{history.length === 0 ? (
<p className="text-gray-600">No saved items yet</p>
) : (
<div className="space-y-2">
{history.map(item => (
<HistoryItem key={item.id} item={item} />
))}
</div>
)}
</div>
);
}License State Management
Check License Status
import { isLicenseValid, getLicense } from '@/utils/licenseStorage';
// Simple check
const isPaid = isLicenseValid();
// Get full license info
const license = getLicense();
if (license) {
console.log('License key:', license.licenseKey);
console.log('Email:', license.email);
console.log('Verified at:', new Date(license.verifiedAt));
}Clear License (Testing)
import { clearLicense } from '@/utils/licenseStorage';
function LogoutButton() {
const handleLogout = () => {
clearLicense();
window.location.reload(); // Refresh to show free tier
};
return <Button onClick={handleLogout}>Clear License</Button>;
}Key Files
boilerplate.config.tsConfigure monetization pattern and pricing
app/home-client.tsxMain app component with license checking
utils/licenseStorage.tsLicense management utilities
utils/appLogic.tsYour app's core processing logic