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.ts

    Configure monetization pattern and pricing

  • app/home-client.tsx

    Main app component with license checking

  • utils/licenseStorage.ts

    License management utilities

  • utils/appLogic.ts

    Your app's core processing logic