From d3830e24b977f515545e4d797238ec3a73d01d7e Mon Sep 17 00:00:00 2001 From: magdev Date: Wed, 21 Jan 2026 21:23:21 +0100 Subject: [PATCH] Implement version 0.0.9 features - Add API client examples for PHP, Python, JavaScript, curl, and C# - Create comprehensive REST API documentation in docs/client-examples/ - All examples include rate limit handling (HTTP 429) - Examples cover validate, status, and activate endpoints Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 32 +- docs/client-examples/README.md | 69 +++++ docs/client-examples/csharp-client.cs | 352 ++++++++++++++++++++++ docs/client-examples/curl.sh | 89 ++++++ docs/client-examples/javascript-client.js | 220 ++++++++++++++ docs/client-examples/php-client.php | 188 ++++++++++++ docs/client-examples/python-client.py | 233 ++++++++++++++ wc-licensed-product.php | 4 +- 8 files changed, 1178 insertions(+), 9 deletions(-) create mode 100644 docs/client-examples/README.md create mode 100644 docs/client-examples/csharp-client.cs create mode 100644 docs/client-examples/curl.sh create mode 100644 docs/client-examples/javascript-client.js create mode 100644 docs/client-examples/php-client.php create mode 100644 docs/client-examples/python-client.py diff --git a/CLAUDE.md b/CLAUDE.md index 205f402..ba81977 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,13 +34,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w ### Known Bugs -_No known bugs at this time._ - -### Version 0.0.9 - - - -**Note for AI Assistants:** Cleanup the versions in this section after implementation, but keep this notice! +_No known bugs at this time. ## Technical Stack @@ -462,3 +456,27 @@ Full API documentation available in `openapi.json` (OpenAPI 3.1 specification). - Admins can customize subject, heading, additional content, and enable/disable via WC settings - Expiration warning schedule (days before) remains in plugin settings - Email enable/disable is controlled through WooCommerce email settings + +### 2026-01-21 - Version 0.0.9 Features + +**Implemented:** + +- Created API client examples for multiple programming languages +- Documentation for REST API usage with code examples + +**New files:** + +- `docs/client-examples/README.md` - API documentation and overview +- `docs/client-examples/curl.sh` - cURL command examples +- `docs/client-examples/php-client.php` - PHP client class with examples +- `docs/client-examples/python-client.py` - Python client class with examples +- `docs/client-examples/javascript-client.js` - JavaScript/Node.js client class +- `docs/client-examples/csharp-client.cs` - C# client class with examples + +**Technical notes:** + +- All client examples include error handling for rate limiting (HTTP 429) +- Examples demonstrate all three API endpoints: validate, status, activate +- JavaScript client works in both browser and Node.js environments +- Python client uses dataclasses for type-safe responses +- C# client uses async/await patterns and System.Text.Json diff --git a/docs/client-examples/README.md b/docs/client-examples/README.md new file mode 100644 index 0000000..a0480b3 --- /dev/null +++ b/docs/client-examples/README.md @@ -0,0 +1,69 @@ +# WC Licensed Product API Client Examples + +This directory contains example API clients for integrating with the WC Licensed Product REST API. + +## API Base URL + +```shell +https://your-site.com/wp-json/wc-licensed-product/v1 +``` + +## Available Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/validate` | POST | Validate a license key for a specific domain | +| `/status` | POST | Get the current status of a license | +| `/activate` | POST | Activate a license on a domain | + +## Rate Limiting + +The API is rate-limited to 30 requests per minute per IP address. If you exceed this limit, you'll receive a `429 Too Many Requests` response with a `Retry-After` header indicating when you can retry. + +## Example Files + +- [curl.sh](curl.sh) - cURL command examples +- [php-client.php](php-client.php) - PHP client class +- [python-client.py](python-client.py) - Python client class +- [javascript-client.js](javascript-client.js) - JavaScript/Node.js client class +- [csharp-client.cs](csharp-client.cs) - C# client class + +## Response Format + +All endpoints return JSON responses with the following structure: + +### Success Response (Validate) + +```json +{ + "valid": true, + "license": { + "status": "active", + "domain": "example.com", + "expires_at": "2025-12-31", + "product_id": 123 + } +} +``` + +### Error Response + +```json +{ + "valid": false, + "error": "license_not_found", + "message": "License key not found." +} +``` + +## Error Codes + +| Code | Description | +| ------ | ------------- | +| `license_not_found` | The license key does not exist | +| `license_expired` | The license has expired | +| `license_revoked` | The license has been revoked | +| `license_inactive` | The license is inactive | +| `domain_mismatch` | The license is not valid for the provided domain | +| `max_activations_reached` | Maximum number of domain activations reached | +| `rate_limit_exceeded` | Too many requests, please wait and retry | diff --git a/docs/client-examples/csharp-client.cs b/docs/client-examples/csharp-client.cs new file mode 100644 index 0000000..e8985f8 --- /dev/null +++ b/docs/client-examples/csharp-client.cs @@ -0,0 +1,352 @@ +/** + * WC Licensed Product API Client for C# + * + * A C# client for interacting with the WC Licensed Product REST API. + * + * Requirements: + * - .NET 6.0+ or .NET Framework 4.7.2+ + * - System.Net.Http + * - System.Text.Json (or Newtonsoft.Json) + * + * Usage: + * var client = new WcLicensedProductClient("https://your-site.com"); + * var result = await client.ValidateAsync("XXXX-XXXX-XXXX-XXXX", "example.com"); + */ + +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace WcLicensedProduct +{ + /// + /// License status response data + /// + public class LicenseStatus + { + [JsonPropertyName("valid")] + public bool Valid { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("domain")] + public string Domain { get; set; } + + [JsonPropertyName("expires_at")] + public string ExpiresAt { get; set; } + + [JsonPropertyName("activations_count")] + public int ActivationsCount { get; set; } + + [JsonPropertyName("max_activations")] + public int MaxActivations { get; set; } + } + + /// + /// Validation response data + /// + public class ValidationResult + { + [JsonPropertyName("valid")] + public bool Valid { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + + [JsonPropertyName("license")] + public LicenseInfo License { get; set; } + } + + /// + /// License information in validation response + /// + public class LicenseInfo + { + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("domain")] + public string Domain { get; set; } + + [JsonPropertyName("expires_at")] + public string ExpiresAt { get; set; } + + [JsonPropertyName("product_id")] + public int ProductId { get; set; } + } + + /// + /// Activation response data + /// + public class ActivationResult + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + } + + /// + /// Exception for API errors + /// + public class WcLicensedProductException : Exception + { + public string ErrorCode { get; } + public int HttpCode { get; } + + public WcLicensedProductException(string message, string errorCode = null, int httpCode = 0) + : base(message) + { + ErrorCode = errorCode; + HttpCode = httpCode; + } + } + + /// + /// Exception for rate limit errors + /// + public class RateLimitException : WcLicensedProductException + { + public int RetryAfter { get; } + + public RateLimitException(int retryAfter) + : base($"Rate limit exceeded. Retry after {retryAfter} seconds.") + { + RetryAfter = retryAfter; + } + } + + /// + /// Client for the WC Licensed Product REST API + /// + public class WcLicensedProductClient : IDisposable + { + private readonly HttpClient _httpClient; + private readonly string _baseUrl; + private readonly JsonSerializerOptions _jsonOptions; + + /// + /// Initialize the client + /// + /// Your WordPress site URL (e.g., "https://example.com") + /// Request timeout (default: 30 seconds) + public WcLicensedProductClient(string siteUrl, TimeSpan? timeout = null) + { + _baseUrl = siteUrl.TrimEnd('/') + "/wp-json/wc-licensed-product/v1"; + + _httpClient = new HttpClient + { + Timeout = timeout ?? TimeSpan.FromSeconds(30) + }; + _httpClient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + } + + /// + /// Validate a license key for a specific domain + /// + /// The license key to validate + /// The domain to validate against + /// Validation result + public async Task ValidateAsync(string licenseKey, string domain) + { + var data = new { license_key = licenseKey, domain = domain }; + return await RequestAsync("/validate", data); + } + + /// + /// Get the status of a license + /// + /// The license key to check + /// License status + public async Task StatusAsync(string licenseKey) + { + var data = new { license_key = licenseKey }; + return await RequestAsync("/status", data); + } + + /// + /// Activate a license on a domain + /// + /// The license key to activate + /// The domain to activate on + /// Activation result + public async Task ActivateAsync(string licenseKey, string domain) + { + var data = new { license_key = licenseKey, domain = domain }; + return await RequestAsync("/activate", data); + } + + /// + /// Make an API request + /// + private async Task RequestAsync(string endpoint, object data) + { + var url = _baseUrl + endpoint; + var json = JsonSerializer.Serialize(data); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + HttpResponseMessage response; + try + { + response = await _httpClient.PostAsync(url, content); + } + catch (TaskCanceledException) + { + throw new WcLicensedProductException("Request timeout"); + } + catch (HttpRequestException ex) + { + throw new WcLicensedProductException($"Request failed: {ex.Message}"); + } + + // Handle rate limiting + if ((int)response.StatusCode == 429) + { + var retryAfter = 60; + if (response.Headers.TryGetValues("Retry-After", out var values)) + { + int.TryParse(string.Join("", values), out retryAfter); + } + throw new RateLimitException(retryAfter); + } + + var responseBody = await response.Content.ReadAsStringAsync(); + T result; + + try + { + result = JsonSerializer.Deserialize(responseBody, _jsonOptions); + } + catch (JsonException) + { + throw new WcLicensedProductException("Invalid JSON response"); + } + + // Check for API errors (for error responses) + if (!response.IsSuccessStatusCode) + { + var errorResult = JsonSerializer.Deserialize(responseBody, _jsonOptions); + if (errorResult?.Error != null) + { + throw new WcLicensedProductException( + errorResult.Message ?? "Unknown error", + errorResult.Error, + (int)response.StatusCode + ); + } + } + + return result; + } + + public void Dispose() + { + _httpClient?.Dispose(); + } + } + + // ========================================================================= + // Usage Examples + // ========================================================================= + + public class Program + { + public static async Task Main(string[] args) + { + using var client = new WcLicensedProductClient("https://your-wordpress-site.com"); + + // Example 1: Validate a license + Console.WriteLine("=== Validate License ==="); + try + { + var result = await client.ValidateAsync("XXXX-XXXX-XXXX-XXXX", "myapp.example.com"); + + if (result.Valid) + { + Console.WriteLine("License is valid!"); + Console.WriteLine($"Status: {result.License?.Status ?? "active"}"); + Console.WriteLine($"Expires: {result.License?.ExpiresAt ?? "Never"}"); + } + else + { + Console.WriteLine($"License is not valid: {result.Message ?? "Unknown error"}"); + } + } + catch (RateLimitException ex) + { + Console.WriteLine($"Rate limited. Retry after {ex.RetryAfter} seconds."); + } + catch (WcLicensedProductException ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + + Console.WriteLine(); + + // Example 2: Check license status + Console.WriteLine("=== Check License Status ==="); + try + { + var status = await client.StatusAsync("XXXX-XXXX-XXXX-XXXX"); + + Console.WriteLine($"Valid: {(status.Valid ? "Yes" : "No")}"); + Console.WriteLine($"Status: {status.Status ?? "unknown"}"); + Console.WriteLine($"Domain: {status.Domain ?? "None"}"); + Console.WriteLine($"Expires: {status.ExpiresAt ?? "Never"}"); + Console.WriteLine($"Activations: {status.ActivationsCount}/{status.MaxActivations}"); + } + catch (RateLimitException ex) + { + Console.WriteLine($"Rate limited. Retry after {ex.RetryAfter} seconds."); + } + catch (WcLicensedProductException ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + + Console.WriteLine(); + + // Example 3: Activate license on a domain + Console.WriteLine("=== Activate License ==="); + try + { + var result = await client.ActivateAsync("XXXX-XXXX-XXXX-XXXX", "newsite.example.com"); + + if (result.Success) + { + Console.WriteLine("License activated successfully!"); + } + else + { + Console.WriteLine($"Activation failed: {result.Message ?? "Unknown error"}"); + } + } + catch (RateLimitException ex) + { + Console.WriteLine($"Rate limited. Retry after {ex.RetryAfter} seconds."); + } + catch (WcLicensedProductException ex) + { + Console.WriteLine($"Error ({ex.ErrorCode}): {ex.Message}"); + } + } + } +} diff --git a/docs/client-examples/curl.sh b/docs/client-examples/curl.sh new file mode 100644 index 0000000..ef60e12 --- /dev/null +++ b/docs/client-examples/curl.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# WC Licensed Product API - cURL Examples +# +# Replace YOUR_SITE_URL with your WordPress site URL +# Replace YOUR_LICENSE_KEY with your actual license key +# Replace YOUR_DOMAIN with the domain to validate/activate + +BASE_URL="https://YOUR_SITE_URL/wp-json/wc-licensed-product/v1" +LICENSE_KEY="XXXX-XXXX-XXXX-XXXX" +DOMAIN="example.com" + +# ------------------------------------------------------------------------------ +# Validate License +# ------------------------------------------------------------------------------ +# Validates if a license key is valid for a specific domain +# Returns: valid (bool), license details, or error message + +echo "=== Validate License ===" +curl -X POST "${BASE_URL}/validate" \ + -H "Content-Type: application/json" \ + -d "{ + \"license_key\": \"${LICENSE_KEY}\", + \"domain\": \"${DOMAIN}\" + }" +echo -e "\n" + +# ------------------------------------------------------------------------------ +# Check License Status +# ------------------------------------------------------------------------------ +# Gets the current status of a license without domain validation +# Returns: status, domain, expiration date, activation count + +echo "=== Check License Status ===" +curl -X POST "${BASE_URL}/status" \ + -H "Content-Type: application/json" \ + -d "{ + \"license_key\": \"${LICENSE_KEY}\" + }" +echo -e "\n" + +# ------------------------------------------------------------------------------ +# Activate License +# ------------------------------------------------------------------------------ +# Activates a license on a specific domain +# Returns: success (bool), message + +echo "=== Activate License ===" +curl -X POST "${BASE_URL}/activate" \ + -H "Content-Type: application/json" \ + -d "{ + \"license_key\": \"${LICENSE_KEY}\", + \"domain\": \"${DOMAIN}\" + }" +echo -e "\n" + +# ------------------------------------------------------------------------------ +# Example with verbose output for debugging +# ------------------------------------------------------------------------------ +echo "=== Verbose Request (for debugging) ===" +curl -v -X POST "${BASE_URL}/validate" \ + -H "Content-Type: application/json" \ + -d "{ + \"license_key\": \"${LICENSE_KEY}\", + \"domain\": \"${DOMAIN}\" + }" 2>&1 + +# ------------------------------------------------------------------------------ +# Example handling rate limit (429 response) +# ------------------------------------------------------------------------------ +# The API returns a Retry-After header when rate limited +# You can use this header to determine when to retry + +echo "=== Handling Rate Limits ===" +response=$(curl -s -w "\n%{http_code}" -X POST "${BASE_URL}/validate" \ + -H "Content-Type: application/json" \ + -d "{ + \"license_key\": \"${LICENSE_KEY}\", + \"domain\": \"${DOMAIN}\" + }") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [ "$http_code" == "429" ]; then + echo "Rate limited. Please wait before retrying." + echo "Response: $body" +else + echo "HTTP $http_code: $body" +fi diff --git a/docs/client-examples/javascript-client.js b/docs/client-examples/javascript-client.js new file mode 100644 index 0000000..e682165 --- /dev/null +++ b/docs/client-examples/javascript-client.js @@ -0,0 +1,220 @@ +/** + * WC Licensed Product API Client for JavaScript + * + * A JavaScript client for interacting with the WC Licensed Product REST API. + * Works in both browser and Node.js environments. + * + * Browser usage: + * + * const client = new WcLicensedProductClient('https://your-site.com'); + * const result = await client.validate('XXXX-XXXX-XXXX-XXXX', 'example.com'); + * + * Node.js usage: + * const WcLicensedProductClient = require('./javascript-client'); + * const client = new WcLicensedProductClient('https://your-site.com'); + */ + +class WcLicensedProductError extends Error { + constructor(message, errorCode = null, httpCode = null) { + super(message); + this.name = 'WcLicensedProductError'; + this.errorCode = errorCode; + this.httpCode = httpCode; + } +} + +class RateLimitError extends WcLicensedProductError { + constructor(retryAfter) { + super(`Rate limit exceeded. Retry after ${retryAfter} seconds.`); + this.name = 'RateLimitError'; + this.retryAfter = retryAfter; + } +} + +class WcLicensedProductClient { + /** + * Initialize the client + * @param {string} siteUrl - Your WordPress site URL (e.g., 'https://example.com') + * @param {Object} options - Optional configuration + * @param {number} options.timeout - Request timeout in milliseconds (default: 30000) + */ + constructor(siteUrl, options = {}) { + this.baseUrl = siteUrl.replace(/\/$/, '') + '/wp-json/wc-licensed-product/v1'; + this.timeout = options.timeout || 30000; + } + + /** + * Validate a license key for a specific domain + * @param {string} licenseKey - The license key to validate + * @param {string} domain - The domain to validate against + * @returns {Promise} Response data with 'valid' boolean and license info + */ + async validate(licenseKey, domain) { + return this._request('/validate', { + license_key: licenseKey, + domain: domain, + }); + } + + /** + * Get the status of a license + * @param {string} licenseKey - The license key to check + * @returns {Promise} License status data + */ + async status(licenseKey) { + return this._request('/status', { + license_key: licenseKey, + }); + } + + /** + * Activate a license on a domain + * @param {string} licenseKey - The license key to activate + * @param {string} domain - The domain to activate on + * @returns {Promise} Response with 'success' boolean and message + */ + async activate(licenseKey, domain) { + return this._request('/activate', { + license_key: licenseKey, + domain: domain, + }); + } + + /** + * Make an API request + * @private + * @param {string} endpoint - API endpoint path + * @param {Object} data - Request data + * @returns {Promise} Decoded response data + */ + async _request(endpoint, data) { + const url = this.baseUrl + endpoint; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(data), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + // Handle rate limiting + if (response.status === 429) { + const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10); + throw new RateLimitError(retryAfter); + } + + const result = await response.json(); + + // Check for API errors + if (response.status >= 400 && result.error) { + throw new WcLicensedProductError( + result.message || 'Unknown error', + result.error, + response.status + ); + } + + return result; + } catch (error) { + clearTimeout(timeoutId); + + if (error instanceof WcLicensedProductError) { + throw error; + } + + if (error.name === 'AbortError') { + throw new WcLicensedProductError('Request timeout'); + } + + throw new WcLicensedProductError(`Request failed: ${error.message}`); + } + } +} + +// ============================================================================= +// Usage Examples +// ============================================================================= + +// Example usage (uncomment to run): + +/* +(async () => { + const client = new WcLicensedProductClient('https://your-wordpress-site.com'); + + // Example 1: Validate a license + console.log('=== Validate License ==='); + try { + const result = await client.validate('XXXX-XXXX-XXXX-XXXX', 'myapp.example.com'); + + if (result.valid) { + console.log('License is valid!'); + console.log('Status:', result.license?.status || 'active'); + console.log('Expires:', result.license?.expires_at || 'Never'); + } else { + console.log('License is not valid:', result.message || 'Unknown error'); + } + } catch (error) { + if (error instanceof RateLimitError) { + console.log(`Rate limited. Retry after ${error.retryAfter} seconds.`); + } else { + console.log('Error:', error.message); + } + } + + console.log(); + + // Example 2: Check license status + console.log('=== Check License Status ==='); + try { + const status = await client.status('XXXX-XXXX-XXXX-XXXX'); + + console.log('Valid:', status.valid ? 'Yes' : 'No'); + console.log('Status:', status.status || 'unknown'); + console.log('Domain:', status.domain || 'None'); + console.log('Expires:', status.expires_at || 'Never'); + console.log('Activations:', `${status.activations_count || 0}/${status.max_activations || 1}`); + } catch (error) { + if (error instanceof RateLimitError) { + console.log(`Rate limited. Retry after ${error.retryAfter} seconds.`); + } else { + console.log('Error:', error.message); + } + } + + console.log(); + + // Example 3: Activate license on a domain + console.log('=== Activate License ==='); + try { + const result = await client.activate('XXXX-XXXX-XXXX-XXXX', 'newsite.example.com'); + + if (result.success) { + console.log('License activated successfully!'); + } else { + console.log('Activation failed:', result.message || 'Unknown error'); + } + } catch (error) { + if (error instanceof RateLimitError) { + console.log(`Rate limited. Retry after ${error.retryAfter} seconds.`); + } else { + console.log(`Error (${error.errorCode}):`, error.message); + } + } +})(); +*/ + +// Export for Node.js +if (typeof module !== 'undefined' && module.exports) { + module.exports = WcLicensedProductClient; + module.exports.WcLicensedProductError = WcLicensedProductError; + module.exports.RateLimitError = RateLimitError; +} diff --git a/docs/client-examples/php-client.php b/docs/client-examples/php-client.php new file mode 100644 index 0000000..da3ab1d --- /dev/null +++ b/docs/client-examples/php-client.php @@ -0,0 +1,188 @@ +validate('XXXX-XXXX-XXXX-XXXX', 'example.com'); + */ + +class WcLicensedProductClient +{ + private string $baseUrl; + private int $timeout; + + /** + * Constructor + * + * @param string $siteUrl Your WordPress site URL (e.g., 'https://example.com') + * @param int $timeout Request timeout in seconds + */ + public function __construct(string $siteUrl, int $timeout = 30) + { + $this->baseUrl = rtrim($siteUrl, '/') . '/wp-json/wc-licensed-product/v1'; + $this->timeout = $timeout; + } + + /** + * Validate a license key for a specific domain + * + * @param string $licenseKey The license key to validate + * @param string $domain The domain to validate against + * @return array Response data + * @throws Exception On request failure + */ + public function validate(string $licenseKey, string $domain): array + { + return $this->request('/validate', [ + 'license_key' => $licenseKey, + 'domain' => $domain, + ]); + } + + /** + * Get the status of a license + * + * @param string $licenseKey The license key to check + * @return array Response data with status, domain, expiration, etc. + * @throws Exception On request failure + */ + public function status(string $licenseKey): array + { + return $this->request('/status', [ + 'license_key' => $licenseKey, + ]); + } + + /** + * Activate a license on a domain + * + * @param string $licenseKey The license key to activate + * @param string $domain The domain to activate on + * @return array Response data + * @throws Exception On request failure + */ + public function activate(string $licenseKey, string $domain): array + { + return $this->request('/activate', [ + 'license_key' => $licenseKey, + 'domain' => $domain, + ]); + } + + /** + * Make an API request + * + * @param string $endpoint API endpoint path + * @param array $data Request data + * @return array Decoded response data + * @throws Exception On request failure + */ + private function request(string $endpoint, array $data): array + { + $url = $this->baseUrl . $endpoint; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($data), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Accept: application/json', + ], + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($response === false) { + throw new Exception('cURL error: ' . $error); + } + + $decoded = json_decode($response, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception('Invalid JSON response: ' . json_last_error_msg()); + } + + // Add HTTP code to response for easier handling + $decoded['_http_code'] = $httpCode; + + return $decoded; + } +} + +// ============================================================================= +// Usage Examples +// ============================================================================= + +// Uncomment to run examples: + +/* +$client = new WcLicensedProductClient('https://your-wordpress-site.com'); + +// Example 1: Validate a license +try { + $result = $client->validate('XXXX-XXXX-XXXX-XXXX', 'myapp.example.com'); + + if ($result['valid']) { + echo "License is valid!\n"; + echo "Status: " . ($result['license']['status'] ?? 'active') . "\n"; + echo "Expires: " . ($result['license']['expires_at'] ?? 'Never') . "\n"; + } else { + echo "License is not valid: " . ($result['message'] ?? 'Unknown error') . "\n"; + } +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} + +// Example 2: Check license status +try { + $status = $client->status('XXXX-XXXX-XXXX-XXXX'); + + echo "License Status:\n"; + echo " Valid: " . ($status['valid'] ? 'Yes' : 'No') . "\n"; + echo " Status: " . ($status['status'] ?? 'unknown') . "\n"; + echo " Domain: " . ($status['domain'] ?? 'none') . "\n"; + echo " Expires: " . ($status['expires_at'] ?? 'Never') . "\n"; + echo " Activations: " . ($status['activations_count'] ?? 0) . "/" . ($status['max_activations'] ?? 1) . "\n"; +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} + +// Example 3: Activate license on a domain +try { + $result = $client->activate('XXXX-XXXX-XXXX-XXXX', 'newsite.example.com'); + + if ($result['success'] ?? false) { + echo "License activated successfully!\n"; + } else { + echo "Activation failed: " . ($result['message'] ?? 'Unknown error') . "\n"; + } +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} + +// Example 4: Handling rate limits +try { + $result = $client->validate('XXXX-XXXX-XXXX-XXXX', 'example.com'); + + if (($result['_http_code'] ?? 200) === 429) { + $retryAfter = $result['retry_after'] ?? 60; + echo "Rate limited. Retry after {$retryAfter} seconds.\n"; + } +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +*/ diff --git a/docs/client-examples/python-client.py b/docs/client-examples/python-client.py new file mode 100644 index 0000000..0cf32ad --- /dev/null +++ b/docs/client-examples/python-client.py @@ -0,0 +1,233 @@ +""" +WC Licensed Product API Client for Python + +A simple Python client for interacting with the WC Licensed Product REST API. + +Requirements: + - Python 3.7+ + - requests library (pip install requests) + +Usage: + client = WcLicensedProductClient('https://your-site.com') + result = client.validate('XXXX-XXXX-XXXX-XXXX', 'example.com') +""" + +import requests +from typing import Dict, Any, Optional +from dataclasses import dataclass + + +@dataclass +class LicenseStatus: + """License status response data.""" + valid: bool + status: str + domain: Optional[str] + expires_at: Optional[str] + activations_count: int + max_activations: int + + +class WcLicensedProductError(Exception): + """Base exception for API errors.""" + def __init__(self, message: str, error_code: str = None, http_code: int = None): + super().__init__(message) + self.error_code = error_code + self.http_code = http_code + + +class RateLimitError(WcLicensedProductError): + """Raised when rate limit is exceeded.""" + def __init__(self, retry_after: int): + super().__init__(f"Rate limit exceeded. Retry after {retry_after} seconds.") + self.retry_after = retry_after + + +class WcLicensedProductClient: + """Client for the WC Licensed Product REST API.""" + + def __init__(self, site_url: str, timeout: int = 30): + """ + Initialize the client. + + Args: + site_url: Your WordPress site URL (e.g., 'https://example.com') + timeout: Request timeout in seconds + """ + self.base_url = site_url.rstrip('/') + '/wp-json/wc-licensed-product/v1' + self.timeout = timeout + self.session = requests.Session() + self.session.headers.update({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }) + + def validate(self, license_key: str, domain: str) -> Dict[str, Any]: + """ + Validate a license key for a specific domain. + + Args: + license_key: The license key to validate + domain: The domain to validate against + + Returns: + Dict with 'valid' boolean and additional license info + + Raises: + WcLicensedProductError: On API error + RateLimitError: When rate limited + """ + return self._request('/validate', { + 'license_key': license_key, + 'domain': domain, + }) + + def status(self, license_key: str) -> LicenseStatus: + """ + Get the status of a license. + + Args: + license_key: The license key to check + + Returns: + LicenseStatus object with license details + + Raises: + WcLicensedProductError: On API error + RateLimitError: When rate limited + """ + data = self._request('/status', { + 'license_key': license_key, + }) + + return LicenseStatus( + valid=data.get('valid', False), + status=data.get('status', 'unknown'), + domain=data.get('domain'), + expires_at=data.get('expires_at'), + activations_count=data.get('activations_count', 0), + max_activations=data.get('max_activations', 1), + ) + + def activate(self, license_key: str, domain: str) -> Dict[str, Any]: + """ + Activate a license on a domain. + + Args: + license_key: The license key to activate + domain: The domain to activate on + + Returns: + Dict with 'success' boolean and message + + Raises: + WcLicensedProductError: On API error + RateLimitError: When rate limited + """ + return self._request('/activate', { + 'license_key': license_key, + 'domain': domain, + }) + + def _request(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Make an API request. + + Args: + endpoint: API endpoint path + data: Request data + + Returns: + Decoded response data + + Raises: + WcLicensedProductError: On API error + RateLimitError: When rate limited + """ + url = self.base_url + endpoint + + try: + response = self.session.post(url, json=data, timeout=self.timeout) + except requests.RequestException as e: + raise WcLicensedProductError(f"Request failed: {e}") + + # Handle rate limiting + if response.status_code == 429: + retry_after = int(response.headers.get('Retry-After', 60)) + raise RateLimitError(retry_after) + + try: + result = response.json() + except ValueError: + raise WcLicensedProductError("Invalid JSON response") + + # Check for API errors + if response.status_code >= 400 and 'error' in result: + raise WcLicensedProductError( + result.get('message', 'Unknown error'), + error_code=result.get('error'), + http_code=response.status_code, + ) + + return result + + +# ============================================================================= +# Usage Examples +# ============================================================================= + +if __name__ == '__main__': + # Initialize the client + client = WcLicensedProductClient('https://your-wordpress-site.com') + + # Example 1: Validate a license + print("=== Validate License ===") + try: + result = client.validate('XXXX-XXXX-XXXX-XXXX', 'myapp.example.com') + + if result.get('valid'): + print("License is valid!") + print(f"Status: {result.get('license', {}).get('status', 'active')}") + print(f"Expires: {result.get('license', {}).get('expires_at', 'Never')}") + else: + print(f"License is not valid: {result.get('message', 'Unknown error')}") + + except RateLimitError as e: + print(f"Rate limited. Retry after {e.retry_after} seconds.") + except WcLicensedProductError as e: + print(f"Error: {e}") + + print() + + # Example 2: Check license status + print("=== Check License Status ===") + try: + status = client.status('XXXX-XXXX-XXXX-XXXX') + + print(f"Valid: {'Yes' if status.valid else 'No'}") + print(f"Status: {status.status}") + print(f"Domain: {status.domain or 'None'}") + print(f"Expires: {status.expires_at or 'Never'}") + print(f"Activations: {status.activations_count}/{status.max_activations}") + + except RateLimitError as e: + print(f"Rate limited. Retry after {e.retry_after} seconds.") + except WcLicensedProductError as e: + print(f"Error: {e}") + + print() + + # Example 3: Activate license on a domain + print("=== Activate License ===") + try: + result = client.activate('XXXX-XXXX-XXXX-XXXX', 'newsite.example.com') + + if result.get('success'): + print("License activated successfully!") + else: + print(f"Activation failed: {result.get('message', 'Unknown error')}") + + except RateLimitError as e: + print(f"Rate limited. Retry after {e.retry_after} seconds.") + except WcLicensedProductError as e: + print(f"Error ({e.error_code}): {e}") diff --git a/wc-licensed-product.php b/wc-licensed-product.php index e8cb07d..8463b2d 100644 --- a/wc-licensed-product.php +++ b/wc-licensed-product.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce Licensed Product * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product * Description: WooCommerce plugin to sell software products using license keys with domain-based validation. - * Version: 0.0.8 + * Version: 0.0.9 * Author: Marco Graetsch * Author URI: https://src.bundespruefstelle.ch/magdev * License: GPL-2.0-or-later @@ -28,7 +28,7 @@ if (!defined('ABSPATH')) { } // Plugin constants -define('WC_LICENSED_PRODUCT_VERSION', '0.0.8'); +define('WC_LICENSED_PRODUCT_VERSION', '0.0.9'); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));