Skip to content

Latest commit

 

History

History
483 lines (408 loc) · 12.9 KB

PRO_IMPLEMENTATION_GUIDE.md

File metadata and controls

483 lines (408 loc) · 12.9 KB

Implementation Guide: Pro Feature Licensing

This guide shows you how to implement the licensing system for Linear Buddy's Pro features.

Directory Structure

src/
├── commands/           # Free features
├── utils/             # Free utilities
├── views/             # Free views
└── pro/               # 💎 Pro features directory
    ├── LICENSE        # Symlink/copy of LICENSE.pro
    ├── commands/      # Pro commands
    ├── utils/         # Pro utilities
    │   ├── licenseManager.ts    # License validation
    │   └── proFeatureGate.ts    # Feature access control
    └── views/         # Pro views

webview-ui/src/
├── shared/            # Free components
├── ticket-panel/      # Free panels
└── pro/               # 💎 Pro webview components
    └── analytics/     # Pro analytics UI

Core Components

1. License Manager (src/pro/utils/licenseManager.ts)

import * as vscode from 'vscode';

export interface LicenseInfo {
  key: string;
  email: string;
  type: 'personal' | 'team' | 'enterprise';
  expiresAt: Date;
  isValid: boolean;
  isTrial: boolean;
  trialEndsAt?: Date;
}

export class LicenseManager {
  private static instance: LicenseManager;
  private context: vscode.ExtensionContext;
  private licenseInfo: LicenseInfo | null = null;
  private readonly LICENSE_KEY = 'linearBuddy.licenseKey';
  private readonly LICENSE_API = 'https://api.yourservice.com/validate'; // Your backend

  private constructor(context: vscode.ExtensionContext) {
    this.context = context;
  }

  public static getInstance(context: vscode.ExtensionContext): LicenseManager {
    if (!LicenseManager.instance) {
      LicenseManager.instance = new LicenseManager(context);
    }
    return LicenseManager.instance;
  }

  /**
   * Initialize license on extension activation
   */
  async initialize(): Promise<void> {
    const storedKey = await this.context.secrets.get(this.LICENSE_KEY);
    if (storedKey) {
      await this.validateLicense(storedKey);
    } else {
      // Check if in trial period
      await this.initializeTrial();
    }
  }

  /**
   * Activate a license key
   */
  async activateLicense(licenseKey: string): Promise<boolean> {
    const isValid = await this.validateLicense(licenseKey);
    if (isValid) {
      await this.context.secrets.store(this.LICENSE_KEY, licenseKey);
      vscode.window.showInformationMessage('✨ Linear Buddy Pro activated successfully!');
      return true;
    } else {
      vscode.window.showErrorMessage('❌ Invalid license key. Please check and try again.');
      return false;
    }
  }

  /**
   * Validate license with backend
   */
  private async validateLicense(licenseKey: string): Promise<boolean> {
    try {
      const response = await fetch(this.LICENSE_API, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          licenseKey,
          product: 'linear-buddy',
          version: vscode.extensions.getExtension('your-publisher.linear-buddy')?.packageJSON.version,
        }),
      });

      if (!response.ok) {
        return false;
      }

      const data = await response.json();
      this.licenseInfo = {
        key: licenseKey,
        email: data.email,
        type: data.type,
        expiresAt: new Date(data.expiresAt),
        isValid: data.isValid,
        isTrial: data.isTrial,
        trialEndsAt: data.trialEndsAt ? new Date(data.trialEndsAt) : undefined,
      };

      return data.isValid;
    } catch (error) {
      console.error('License validation failed:', error);
      // Offline grace period: allow if validated within last 7 days
      return this.checkOfflineGracePeriod();
    }
  }

  /**
   * Initialize trial period
   */
  private async initializeTrial(): Promise<void> {
    const trialStartKey = 'linearBuddy.trialStartDate';
    const trialStart = this.context.globalState.get<string>(trialStartKey);

    if (!trialStart) {
      // First time - start trial
      const now = new Date();
      await this.context.globalState.update(trialStartKey, now.toISOString());
      
      this.licenseInfo = {
        key: 'TRIAL',
        email: '',
        type: 'personal',
        expiresAt: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000), // 30 days
        isValid: true,
        isTrial: true,
        trialEndsAt: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000),
      };

      vscode.window.showInformationMessage(
        '🎉 Welcome to Linear Buddy Pro! You have 30 days free trial.',
        'Learn More'
      ).then(selection => {
        if (selection === 'Learn More') {
          vscode.env.openExternal(vscode.Uri.parse('https://yoursite.com/pro'));
        }
      });
    } else {
      // Check if trial expired
      const trialDate = new Date(trialStart);
      const now = new Date();
      const daysElapsed = (now.getTime() - trialDate.getTime()) / (1000 * 60 * 60 * 24);

      if (daysElapsed > 30) {
        this.licenseInfo = {
          key: 'TRIAL_EXPIRED',
          email: '',
          type: 'personal',
          expiresAt: new Date(trialDate.getTime() + 30 * 24 * 60 * 60 * 1000),
          isValid: false,
          isTrial: true,
          trialEndsAt: new Date(trialDate.getTime() + 30 * 24 * 60 * 60 * 1000),
        };
      } else {
        this.licenseInfo = {
          key: 'TRIAL',
          email: '',
          type: 'personal',
          expiresAt: new Date(trialDate.getTime() + 30 * 24 * 60 * 60 * 1000),
          isValid: true,
          isTrial: true,
          trialEndsAt: new Date(trialDate.getTime() + 30 * 24 * 60 * 60 * 1000),
        };
      }
    }
  }

  /**
   * Check offline grace period (7 days)
   */
  private checkOfflineGracePeriod(): boolean {
    const lastValidation = this.context.globalState.get<string>('linearBuddy.lastValidation');
    if (!lastValidation) {
      return false;
    }

    const lastDate = new Date(lastValidation);
    const now = new Date();
    const daysSince = (now.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24);

    return daysSince <= 7;
  }

  /**
   * Check if Pro features are available
   */
  public hasProAccess(): boolean {
    return this.licenseInfo?.isValid ?? false;
  }

  /**
   * Get license info
   */
  public getLicenseInfo(): LicenseInfo | null {
    return this.licenseInfo;
  }

  /**
   * Show upgrade prompt
   */
  public async promptUpgrade(featureName: string): Promise<void> {
    const action = await vscode.window.showInformationMessage(
      `💎 "${featureName}" is a Pro feature. Upgrade to access advanced capabilities!`,
      'View Plans',
      'Activate License',
      'Dismiss'
    );

    if (action === 'View Plans') {
      vscode.env.openExternal(vscode.Uri.parse('https://yoursite.com/pricing'));
    } else if (action === 'Activate License') {
      const key = await vscode.window.showInputBox({
        prompt: 'Enter your Linear Buddy Pro license key',
        placeHolder: 'XXXX-XXXX-XXXX-XXXX',
        password: true,
      });

      if (key) {
        await this.activateLicense(key);
      }
    }
  }

  /**
   * Deactivate license
   */
  public async deactivateLicense(): Promise<void> {
    await this.context.secrets.delete(this.LICENSE_KEY);
    this.licenseInfo = null;
    vscode.window.showInformationMessage('License deactivated.');
  }
}

2. Pro Feature Gate (src/pro/utils/proFeatureGate.ts)

import * as vscode from 'vscode';
import { LicenseManager } from './licenseManager';

/**
 * Decorator for Pro features
 */
export function requireProLicense(featureName: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const context = (this as any).context as vscode.ExtensionContext;
      const licenseManager = LicenseManager.getInstance(context);

      if (!licenseManager.hasProAccess()) {
        await licenseManager.promptUpgrade(featureName);
        return;
      }

      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

/**
 * Check if Pro feature is available
 */
export function isProFeatureAvailable(context: vscode.ExtensionContext): boolean {
  const licenseManager = LicenseManager.getInstance(context);
  return licenseManager.hasProAccess();
}

/**
 * Show Pro badge in UI
 */
export function getProBadge(): string {
  return '💎';
}

3. Example Pro Command

// src/pro/commands/advancedAnalytics.ts
import * as vscode from 'vscode';
import { requireProLicense } from '../utils/proFeatureGate';

export class AdvancedAnalyticsCommand {
  constructor(private context: vscode.ExtensionContext) {}

  @requireProLicense('Advanced Analytics')
  async execute() {
    // This code only runs if user has Pro license
    vscode.window.showInformationMessage('Opening Advanced Analytics...');
    
    // Your Pro feature implementation
    const panel = vscode.window.createWebviewPanel(
      'advancedAnalytics',
      'Advanced Analytics',
      vscode.ViewColumn.One,
      {}
    );

    // ... rest of implementation
  }
}

4. Registration in Extension

// src/extension.ts
import { LicenseManager } from './pro/utils/licenseManager';

export async function activate(context: vscode.ExtensionContext) {
  // Initialize license manager
  const licenseManager = LicenseManager.getInstance(context);
  await licenseManager.initialize();

  // Register Pro license activation command
  context.subscriptions.push(
    vscode.commands.registerCommand('linearBuddy.activateProLicense', async () => {
      const key = await vscode.window.showInputBox({
        prompt: 'Enter your Linear Buddy Pro license key',
        placeHolder: 'XXXX-XXXX-XXXX-XXXX',
        password: true,
      });

      if (key) {
        await licenseManager.activateLicense(key);
      }
    })
  );

  // Register Pro license status command
  context.subscriptions.push(
    vscode.commands.registerCommand('linearBuddy.showLicenseInfo', () => {
      const info = licenseManager.getLicenseInfo();
      if (info) {
        const status = info.isTrial ? 'Trial' : info.type;
        const expires = info.expiresAt.toLocaleDateString();
        vscode.window.showInformationMessage(
          `License Status: ${status} (expires ${expires})`
        );
      } else {
        vscode.window.showInformationMessage('No active license. Using free features.');
      }
    })
  );

  // Your existing activation code...
}

Backend API

You'll need a simple backend to validate licenses. Options:

Option 1: Use Gumroad

  • Gumroad provides license key API
  • Simple integration
  • Handles payments automatically

Option 2: Use Lemon Squeezy

  • More flexible than Gumroad
  • Better API
  • European company (GDPR friendly)

Option 3: Use Paddle

  • Enterprise-grade
  • Handles VAT/taxes globally
  • More expensive

Option 4: Build Your Own

Simple Node.js/Express backend:

// Backend API example
app.post('/api/validate', async (req, res) => {
  const { licenseKey, product, version } = req.body;
  
  // Check database for license key
  const license = await db.licenses.findOne({ key: licenseKey, product });
  
  if (!license) {
    return res.status(404).json({ isValid: false });
  }
  
  if (new Date() > license.expiresAt) {
    return res.status(200).json({ 
      isValid: false, 
      reason: 'expired' 
    });
  }
  
  // Log usage
  await db.validations.create({
    licenseKey,
    version,
    timestamp: new Date(),
  });
  
  return res.json({
    isValid: true,
    email: license.email,
    type: license.type,
    expiresAt: license.expiresAt,
    isTrial: false,
  });
});

Next Steps

  1. Create src/pro/ directory
  2. Implement LicenseManager
  3. Set up backend API (or use Gumroad/Lemon Squeezy)
  4. Add activation command to package.json
  5. Create first Pro feature as proof of concept
  6. Test thoroughly with trial and license key
  7. Update documentation

Testing

// Test license flow
1. Install extension → Should start trial
2. Use Pro feature → Should work during trial
3. Wait 30 days (or mock date) → Should show upgrade prompt
4. Enter license key → Should unlock Pro features
5. Restart VS Code → Should remember license
6. Go offline → Should work for 7 days

Important Notes

⚠️ Security Considerations:

  • Never store sensitive data in extension code
  • Use VS Code's SecretStorage for license keys
  • Validate server-side, not just client-side
  • Implement rate limiting on validation API
  • Log validation attempts for abuse detection

✅ User Experience:

  • Make trial period generous (30 days)
  • Show clear upgrade prompts
  • Don't nag users repeatedly
  • Provide offline grace period
  • Keep free features fully functional

📝 Legal:

  • Clearly mark Pro features in UI
  • Update marketplace listing
  • Add license terms to docs
  • Provide refund policy

Would you like me to help implement any of these components?