Implement IronLicensing Rust SDK

- Add LicenseClient with validation, activation, deactivation
- Add feature checking with has_feature/require_feature pattern
- Add trial management and in-app purchase support
- Add thread-safe operations with parking_lot::RwLock
- Add global client with once_cell for singleton pattern
- Add machine ID persistence for activation tracking

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Friedel 2025-12-25 11:52:35 +00:00
parent 0cbc6afd53
commit 34842f5bd6
9 changed files with 1291 additions and 2 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
/target
Cargo.lock
**/*.rs.bk
*.pdb
.idea/
.vscode/
*.swp
*.swo
.DS_Store
Thumbs.db
.ironlicensing/

31
Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "ironlicensing"
version = "1.0.0"
edition = "2021"
authors = ["IronServices <support@ironservices.com>"]
description = "Official Rust SDK for IronLicensing - Software licensing and activation"
license = "MIT"
repository = "https://github.com/IronServices/ironlicensing-rust"
homepage = "https://ironlicensing.com"
documentation = "https://docs.rs/ironlicensing"
keywords = ["licensing", "activation", "software-licensing", "drm"]
categories = ["api-bindings", "authentication"]
readme = "README.md"
[dependencies]
reqwest = { version = "0.11", features = ["json", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.0", features = ["full"], optional = true }
thiserror = "1.0"
uuid = { version = "1.0", features = ["v4"] }
parking_lot = "0.12"
once_cell = "1.18"
dirs = "5.0"
hostname = "0.3"
[features]
default = []
[dev-dependencies]
tokio = { version = "1.0", features = ["full", "test-util"] }

308
README.md
View File

@ -1,2 +1,306 @@
# ironlicensing-rust
IronLicensing SDK for Rust - Software licensing and activation
# IronLicensing Rust SDK
Official Rust SDK for [IronLicensing](https://ironlicensing.com) - Software licensing and activation for your applications.
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
ironlicensing = "1.0"
```
## Quick Start
### Using Client Instance
```rust
use ironlicensing::{LicenseClient, LicenseOptions};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a client
let client = LicenseClient::with_credentials(
"pk_live_your_public_key",
"your-product-slug"
)?;
// Validate a license
let result = client.validate("IRON-XXXX-XXXX-XXXX-XXXX");
if result.valid {
println!("License is valid!");
if let Some(license) = &result.license {
println!("Status: {:?}", license.status);
}
} else {
println!("Validation failed: {:?}", result.error);
}
// Check for features
if client.has_feature("premium") {
println!("Premium features enabled!");
}
Ok(())
}
```
### Using Global Client
```rust
use ironlicensing::{init, validate, has_feature};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize the global client
init("pk_live_your_public_key", "your-product-slug")?;
// Use global functions
let result = validate("IRON-XXXX-XXXX-XXXX-XXXX")?;
if result.valid {
println!("License is valid!");
}
if has_feature("premium")? {
println!("Premium features enabled!");
}
Ok(())
}
```
## Configuration
```rust
use ironlicensing::{LicenseClient, LicenseOptions};
use std::time::Duration;
let options = LicenseOptions::new("pk_live_xxx", "your-product")
.api_base_url("https://api.ironlicensing.com")
.debug(true)
.enable_offline_cache(true)
.cache_validation_minutes(60)
.offline_grace_days(7)
.http_timeout(Duration::from_secs(30));
let client = LicenseClient::new(options)?;
```
## License Validation
```rust
let result = client.validate("IRON-XXXX-XXXX-XXXX-XXXX");
if result.valid {
if let Some(license) = &result.license {
println!("License: {}", license.key);
println!("Status: {:?}", license.status);
println!("Type: {:?}", license.license_type);
println!("Activations: {}/{}",
license.current_activations,
license.max_activations);
}
}
```
## License Activation
```rust
// Simple activation (uses hostname as machine name)
let result = client.activate("IRON-XXXX-XXXX-XXXX-XXXX");
// With custom machine name
let result = client.activate_with_name(
"IRON-XXXX-XXXX-XXXX-XXXX",
Some("Production Server")
);
if result.valid {
println!("License activated successfully!");
// View activations
if let Some(activations) = &result.activations {
for activation in activations {
println!("- {} ({:?})",
activation.machine_name.as_deref().unwrap_or("Unknown"),
activation.platform);
}
}
}
```
## License Deactivation
```rust
if client.deactivate() {
println!("License deactivated from this machine");
}
```
## Feature Checking
```rust
// Check if feature is available
if client.has_feature("advanced-analytics") {
// Enable advanced analytics
}
// Require feature (returns error if not available)
client.require_feature("export-pdf")?;
// Feature is available, continue with export
// Get feature details
if let Some(feature) = client.get_feature("max-users") {
println!("Feature: {} - {:?}", feature.name, feature.description);
}
```
## Trial Management
```rust
let result = client.start_trial("user@example.com");
if result.valid {
println!("Trial started!");
if let Some(license) = &result.license {
println!("Trial key: {}", license.key);
if let Some(expires) = &license.expires_at {
println!("Expires: {}", expires);
}
}
}
```
## In-App Purchase
```rust
// Get available tiers
let tiers = client.get_tiers();
for tier in &tiers {
println!("{} - ${:.2} {}", tier.name, tier.price, tier.currency);
}
// Start checkout
let checkout = client.start_purchase("tier-id", "user@example.com");
if checkout.success {
if let Some(url) = &checkout.checkout_url {
println!("Checkout URL: {}", url);
// Open URL in browser for user to complete purchase
}
}
```
## License Status
```rust
use ironlicensing::LicenseStatus;
// Get current license
if let Some(license) = client.license() {
println!("Licensed to: {:?}", license.email);
}
// Check status
match client.status() {
LicenseStatus::Valid => println!("License is valid"),
LicenseStatus::Expired => println!("License has expired"),
LicenseStatus::Trial => println!("Running in trial mode"),
LicenseStatus::NotActivated => println!("No license activated"),
status => println!("Status: {:?}", status),
}
// Quick checks
if client.is_licensed() {
println!("Application is licensed");
}
if client.is_trial() {
println!("Running in trial mode");
}
```
## License Types
| Type | Description |
|------|-------------|
| `Perpetual` | One-time purchase, never expires |
| `Subscription` | Recurring payment, expires if not renewed |
| `Trial` | Time-limited trial license |
## License Statuses
| Status | Description |
|--------|-------------|
| `Valid` | License is valid and active |
| `Expired` | License has expired |
| `Suspended` | License temporarily suspended |
| `Revoked` | License permanently revoked |
| `Trial` | Active trial license |
| `TrialExpired` | Trial period ended |
| `NotActivated` | No license activated |
## Thread Safety
The client uses `parking_lot::RwLock` and is safe to share across threads:
```rust
use std::sync::Arc;
use std::thread;
let client = Arc::new(LicenseClient::with_credentials("pk_live_xxx", "product")?);
let handles: Vec<_> = (0..10).map(|_| {
let client = Arc::clone(&client);
thread::spawn(move || {
if client.has_feature("concurrent-feature") {
// Safe to call from multiple threads
}
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
```
## Error Handling
```rust
use ironlicensing::{LicenseError, LicenseResult};
// Validation errors
let result = client.validate(license_key);
if !result.valid {
if let Some(error) = &result.error {
match error.as_str() {
"license_not_found" => println!("Invalid license key"),
"license_expired" => println!("Your license has expired"),
"max_activations_reached" => println!("No more activations available"),
_ => println!("Error: {}", error),
}
}
}
// Feature requirement errors
match client.require_feature("premium") {
Ok(()) => println!("Feature available"),
Err(LicenseError::FeatureRequired(feature)) => {
println!("Feature '{}' requires a valid license", feature);
}
Err(e) => println!("Error: {}", e),
}
```
## Machine ID
The SDK automatically generates and persists a unique machine ID at `~/.ironlicensing/machine_id`. This ID is used for:
- Tracking activations per machine
- Preventing license sharing
- Offline validation
```rust
let machine_id = client.machine_id();
```
## License
MIT License - see LICENSE file for details.

172
src/client.rs Normal file
View File

@ -0,0 +1,172 @@
use crate::config::LicenseOptions;
use crate::error::{LicenseError, Result};
use crate::transport::Transport;
use crate::types::{CheckoutResult, Feature, License, LicenseResult, LicenseStatus, LicenseType, ProductTier};
use parking_lot::RwLock;
/// The main IronLicensing client.
/// Thread-safe and can be shared across threads.
pub struct LicenseClient {
options: LicenseOptions,
transport: Transport,
current_license: RwLock<Option<License>>,
license_key: RwLock<Option<String>>,
}
impl LicenseClient {
/// Create a new LicenseClient with the given options.
pub fn new(options: LicenseOptions) -> Result<Self> {
if options.public_key.is_empty() {
return Err(LicenseError::PublicKeyRequired);
}
if options.product_slug.is_empty() {
return Err(LicenseError::ProductSlugRequired);
}
let transport = Transport::new(&options);
if options.debug {
println!("[IronLicensing] Client initialized");
}
Ok(Self {
options,
transport,
current_license: RwLock::new(None),
license_key: RwLock::new(None),
})
}
/// Create a new client with public key and product slug.
pub fn with_credentials(public_key: impl Into<String>, product_slug: impl Into<String>) -> Result<Self> {
Self::new(LicenseOptions::new(public_key, product_slug))
}
/// Validate a license key.
pub fn validate(&self, license_key: &str) -> LicenseResult {
let result = self.transport.validate(license_key);
if result.valid {
if let Some(license) = &result.license {
*self.current_license.write() = Some(license.clone());
*self.license_key.write() = Some(license_key.to_string());
}
}
result
}
/// Activate a license key on this machine.
pub fn activate(&self, license_key: &str) -> LicenseResult {
self.activate_with_name(license_key, None)
}
/// Activate a license key with a custom machine name.
pub fn activate_with_name(&self, license_key: &str, machine_name: Option<&str>) -> LicenseResult {
let result = self.transport.activate(license_key, machine_name);
if result.valid {
if let Some(license) = &result.license {
*self.current_license.write() = Some(license.clone());
*self.license_key.write() = Some(license_key.to_string());
}
}
result
}
/// Deactivate the current license from this machine.
pub fn deactivate(&self) -> bool {
let key = self.license_key.read().clone();
if let Some(key) = key {
if self.transport.deactivate(&key) {
*self.current_license.write() = None;
*self.license_key.write() = None;
return true;
}
}
false
}
/// Start a trial for the given email.
pub fn start_trial(&self, email: &str) -> LicenseResult {
let result = self.transport.start_trial(email);
if result.valid {
if let Some(license) = &result.license {
*self.current_license.write() = Some(license.clone());
*self.license_key.write() = Some(license.key.clone());
}
}
result
}
/// Check if a feature is available in the current license.
pub fn has_feature(&self, feature_key: &str) -> bool {
self.current_license
.read()
.as_ref()
.map(|l| l.has_feature(feature_key))
.unwrap_or(false)
}
/// Require a feature to be available.
/// Returns an error if the feature is not available.
pub fn require_feature(&self, feature_key: &str) -> Result<()> {
if !self.has_feature(feature_key) {
return Err(LicenseError::FeatureRequired(feature_key.to_string()));
}
Ok(())
}
/// Get a feature from the current license.
pub fn get_feature(&self, feature_key: &str) -> Option<Feature> {
self.current_license
.read()
.as_ref()
.and_then(|l| l.get_feature(feature_key).cloned())
}
/// Get the current license.
pub fn license(&self) -> Option<License> {
self.current_license.read().clone()
}
/// Get the current license status.
pub fn status(&self) -> LicenseStatus {
self.current_license
.read()
.as_ref()
.map(|l| l.status)
.unwrap_or(LicenseStatus::NotActivated)
}
/// Check if the application is licensed (valid or trial).
pub fn is_licensed(&self) -> bool {
self.current_license
.read()
.as_ref()
.map(|l| matches!(l.status, LicenseStatus::Valid | LicenseStatus::Trial))
.unwrap_or(false)
}
/// Check if running in trial mode.
pub fn is_trial(&self) -> bool {
self.current_license
.read()
.as_ref()
.map(|l| l.status == LicenseStatus::Trial || l.license_type == LicenseType::Trial)
.unwrap_or(false)
}
/// Get available product tiers for purchase.
pub fn get_tiers(&self) -> Vec<ProductTier> {
self.transport.get_tiers()
}
/// Start a checkout session for the specified tier.
pub fn start_purchase(&self, tier_id: &str, email: &str) -> CheckoutResult {
self.transport.start_checkout(tier_id, email)
}
/// Get the machine ID used for activations.
pub fn machine_id(&self) -> &str {
self.transport.machine_id()
}
}

84
src/config.rs Normal file
View File

@ -0,0 +1,84 @@
use std::time::Duration;
/// Configuration options for the LicenseClient.
#[derive(Debug, Clone)]
pub struct LicenseOptions {
/// Public key for your product (required).
pub public_key: String,
/// Product slug identifier (required).
pub product_slug: String,
/// API base URL.
pub api_base_url: String,
/// Enable debug logging.
pub debug: bool,
/// Enable offline license caching.
pub enable_offline_cache: bool,
/// Cache validation interval in minutes.
pub cache_validation_minutes: u32,
/// Offline grace period in days.
pub offline_grace_days: u32,
/// HTTP request timeout.
pub http_timeout: Duration,
}
impl LicenseOptions {
/// Create new options with required public key and product slug.
pub fn new(public_key: impl Into<String>, product_slug: impl Into<String>) -> Self {
Self {
public_key: public_key.into(),
product_slug: product_slug.into(),
..Default::default()
}
}
/// Set the API base URL.
pub fn api_base_url(mut self, url: impl Into<String>) -> Self {
self.api_base_url = url.into();
self
}
/// Enable or disable debug mode.
pub fn debug(mut self, debug: bool) -> Self {
self.debug = debug;
self
}
/// Enable or disable offline caching.
pub fn enable_offline_cache(mut self, enable: bool) -> Self {
self.enable_offline_cache = enable;
self
}
/// Set cache validation interval in minutes.
pub fn cache_validation_minutes(mut self, minutes: u32) -> Self {
self.cache_validation_minutes = minutes;
self
}
/// Set offline grace period in days.
pub fn offline_grace_days(mut self, days: u32) -> Self {
self.offline_grace_days = days;
self
}
/// Set HTTP request timeout.
pub fn http_timeout(mut self, timeout: Duration) -> Self {
self.http_timeout = timeout;
self
}
}
impl Default for LicenseOptions {
fn default() -> Self {
Self {
public_key: String::new(),
product_slug: String::new(),
api_base_url: "https://api.ironlicensing.com".to_string(),
debug: false,
enable_offline_cache: true,
cache_validation_minutes: 60,
offline_grace_days: 7,
http_timeout: Duration::from_secs(30),
}
}
}

39
src/error.rs Normal file
View File

@ -0,0 +1,39 @@
use thiserror::Error;
/// Errors that can occur in the IronLicensing SDK.
#[derive(Debug, Error)]
pub enum LicenseError {
/// Client is not initialized.
#[error("IronLicensing client not initialized")]
NotInitialized,
/// Public key is required.
#[error("Public key is required")]
PublicKeyRequired,
/// Product slug is required.
#[error("Product slug is required")]
ProductSlugRequired,
/// A required feature is not available.
#[error("Feature '{0}' requires a valid license")]
FeatureRequired(String),
/// HTTP request error.
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
/// JSON serialization/deserialization error.
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
/// IO error.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// API error returned from the server.
#[error("API error: {0}")]
Api(String),
}
pub type Result<T> = std::result::Result<T, LicenseError>;

160
src/lib.rs Normal file
View File

@ -0,0 +1,160 @@
//! IronLicensing SDK for Rust
//!
//! Official Rust SDK for [IronLicensing](https://ironlicensing.com) - Software licensing
//! and activation for your applications.
//!
//! # Quick Start
//!
//! ```rust,no_run
//! use ironlicensing::{LicenseClient, LicenseOptions};
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Create a client
//! let client = LicenseClient::with_credentials(
//! "pk_live_your_public_key",
//! "your-product-slug"
//! )?;
//!
//! // Validate a license
//! let result = client.validate("IRON-XXXX-XXXX-XXXX-XXXX");
//! if result.valid {
//! println!("License is valid!");
//! }
//!
//! // Check for features
//! if client.has_feature("premium") {
//! println!("Premium features enabled!");
//! }
//!
//! Ok(())
//! }
//! ```
//!
//! # Global Client
//!
//! For convenience, you can use a global client:
//!
//! ```rust,no_run
//! use ironlicensing::{init, validate, has_feature};
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Initialize the global client
//! init("pk_live_your_public_key", "your-product-slug")?;
//!
//! // Use global functions
//! let result = validate("IRON-XXXX-XXXX-XXXX-XXXX")?;
//! if result.valid {
//! println!("Valid!");
//! }
//!
//! if has_feature("premium")? {
//! println!("Premium enabled!");
//! }
//!
//! Ok(())
//! }
//! ```
mod client;
mod config;
mod error;
mod transport;
mod types;
pub use client::LicenseClient;
pub use config::LicenseOptions;
pub use error::{LicenseError, Result};
pub use types::*;
use once_cell::sync::OnceCell;
use std::sync::Arc;
static GLOBAL_CLIENT: OnceCell<Arc<LicenseClient>> = OnceCell::new();
/// Initialize the global IronLicensing client.
pub fn init(public_key: impl Into<String>, product_slug: impl Into<String>) -> Result<()> {
init_with_options(LicenseOptions::new(public_key, product_slug))
}
/// Initialize the global client with custom options.
pub fn init_with_options(options: LicenseOptions) -> Result<()> {
let client = LicenseClient::new(options)?;
GLOBAL_CLIENT
.set(Arc::new(client))
.map_err(|_| LicenseError::Api("Client already initialized".to_string()))
}
/// Get the global client.
pub fn get_client() -> Result<&'static Arc<LicenseClient>> {
GLOBAL_CLIENT.get().ok_or(LicenseError::NotInitialized)
}
/// Validate a license key using the global client.
pub fn validate(license_key: &str) -> Result<LicenseResult> {
Ok(get_client()?.validate(license_key))
}
/// Activate a license key using the global client.
pub fn activate(license_key: &str) -> Result<LicenseResult> {
Ok(get_client()?.activate(license_key))
}
/// Activate a license key with a machine name using the global client.
pub fn activate_with_name(license_key: &str, machine_name: Option<&str>) -> Result<LicenseResult> {
Ok(get_client()?.activate_with_name(license_key, machine_name))
}
/// Deactivate the current license using the global client.
pub fn deactivate() -> Result<bool> {
Ok(get_client()?.deactivate())
}
/// Start a trial using the global client.
pub fn start_trial(email: &str) -> Result<LicenseResult> {
Ok(get_client()?.start_trial(email))
}
/// Check if a feature is available using the global client.
pub fn has_feature(feature_key: &str) -> Result<bool> {
Ok(get_client()?.has_feature(feature_key))
}
/// Require a feature using the global client.
pub fn require_feature(feature_key: &str) -> Result<()> {
get_client()?.require_feature(feature_key)
}
/// Get a feature using the global client.
pub fn get_feature(feature_key: &str) -> Result<Option<Feature>> {
Ok(get_client()?.get_feature(feature_key))
}
/// Get the current license using the global client.
pub fn license() -> Result<Option<License>> {
Ok(get_client()?.license())
}
/// Get the license status using the global client.
pub fn status() -> Result<LicenseStatus> {
Ok(get_client()?.status())
}
/// Check if licensed using the global client.
pub fn is_licensed() -> Result<bool> {
Ok(get_client()?.is_licensed())
}
/// Check if in trial mode using the global client.
pub fn is_trial() -> Result<bool> {
Ok(get_client()?.is_trial())
}
/// Get available tiers using the global client.
pub fn get_tiers() -> Result<Vec<ProductTier>> {
Ok(get_client()?.get_tiers())
}
/// Start a purchase using the global client.
pub fn start_purchase(tier_id: &str, email: &str) -> Result<CheckoutResult> {
Ok(get_client()?.start_purchase(tier_id, email))
}

290
src/transport.rs Normal file
View File

@ -0,0 +1,290 @@
use crate::config::LicenseOptions;
use crate::error::{LicenseError, Result};
use crate::types::{CheckoutResult, LicenseResult, ProductTier};
use reqwest::blocking::Client as HttpClient;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use uuid::Uuid;
pub struct Transport {
base_url: String,
public_key: String,
product_slug: String,
debug: bool,
http_client: HttpClient,
machine_id: String,
}
#[derive(Serialize)]
struct ValidateRequest {
#[serde(rename = "licenseKey")]
license_key: String,
#[serde(rename = "machineId")]
machine_id: String,
}
#[derive(Serialize)]
struct ActivateRequest {
#[serde(rename = "licenseKey")]
license_key: String,
#[serde(rename = "machineId")]
machine_id: String,
#[serde(rename = "machineName")]
machine_name: String,
platform: String,
}
#[derive(Serialize)]
struct DeactivateRequest {
#[serde(rename = "licenseKey")]
license_key: String,
#[serde(rename = "machineId")]
machine_id: String,
}
#[derive(Serialize)]
struct TrialRequest {
email: String,
#[serde(rename = "machineId")]
machine_id: String,
}
#[derive(Serialize)]
struct CheckoutRequest {
#[serde(rename = "tierId")]
tier_id: String,
email: String,
}
#[derive(Deserialize)]
struct TiersResponse {
tiers: Vec<ProductTier>,
}
#[derive(Deserialize)]
struct ErrorResponse {
error: String,
}
impl Transport {
pub fn new(options: &LicenseOptions) -> Self {
let http_client = HttpClient::builder()
.timeout(options.http_timeout)
.build()
.unwrap_or_default();
let machine_id = Self::get_or_create_machine_id();
Self {
base_url: options.api_base_url.clone(),
public_key: options.public_key.clone(),
product_slug: options.product_slug.clone(),
debug: options.debug,
http_client,
machine_id,
}
}
fn log(&self, msg: &str) {
if self.debug {
println!("[IronLicensing] {}", msg);
}
}
fn get_machine_id_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironlicensing")
.join("machine_id")
}
fn get_or_create_machine_id() -> String {
let id_path = Self::get_machine_id_path();
if let Ok(id) = fs::read_to_string(&id_path) {
return id.trim().to_string();
}
let id = Uuid::new_v4().to_string();
if let Some(parent) = id_path.parent() {
let _ = fs::create_dir_all(parent);
}
let _ = fs::write(&id_path, &id);
id
}
pub fn machine_id(&self) -> &str {
&self.machine_id
}
fn get_hostname() -> String {
hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "unknown".to_string())
}
fn get_platform() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}
pub fn validate(&self, license_key: &str) -> LicenseResult {
let preview = &license_key[..license_key.len().min(10)];
self.log(&format!("Validating: {}...", preview));
let request = ValidateRequest {
license_key: license_key.to_string(),
machine_id: self.machine_id.clone(),
};
self.post("/api/v1/validate", &request)
}
pub fn activate(&self, license_key: &str, machine_name: Option<&str>) -> LicenseResult {
let preview = &license_key[..license_key.len().min(10)];
self.log(&format!("Activating: {}...", preview));
let machine_name = machine_name
.map(String::from)
.unwrap_or_else(Self::get_hostname);
let request = ActivateRequest {
license_key: license_key.to_string(),
machine_id: self.machine_id.clone(),
machine_name,
platform: Self::get_platform().to_string(),
};
self.post("/api/v1/activate", &request)
}
pub fn deactivate(&self, license_key: &str) -> bool {
self.log("Deactivating license");
let request = DeactivateRequest {
license_key: license_key.to_string(),
machine_id: self.machine_id.clone(),
};
match self
.http_client
.post(format!("{}/api/v1/deactivate", self.base_url))
.header("Content-Type", "application/json")
.header("X-Public-Key", &self.public_key)
.header("X-Product-Slug", &self.product_slug)
.json(&request)
.send()
{
Ok(resp) => resp.status().is_success(),
Err(_) => false,
}
}
pub fn start_trial(&self, email: &str) -> LicenseResult {
self.log(&format!("Starting trial for: {}", email));
let request = TrialRequest {
email: email.to_string(),
machine_id: self.machine_id.clone(),
};
self.post("/api/v1/trial", &request)
}
pub fn get_tiers(&self) -> Vec<ProductTier> {
self.log("Fetching product tiers");
match self
.http_client
.get(format!("{}/api/v1/tiers", self.base_url))
.header("Content-Type", "application/json")
.header("X-Public-Key", &self.public_key)
.header("X-Product-Slug", &self.product_slug)
.send()
{
Ok(resp) if resp.status().is_success() => resp
.json::<TiersResponse>()
.map(|r| r.tiers)
.unwrap_or_default(),
_ => vec![],
}
}
pub fn start_checkout(&self, tier_id: &str, email: &str) -> CheckoutResult {
self.log(&format!("Starting checkout for tier: {}", tier_id));
let request = CheckoutRequest {
tier_id: tier_id.to_string(),
email: email.to_string(),
};
match self
.http_client
.post(format!("{}/api/v1/checkout", self.base_url))
.header("Content-Type", "application/json")
.header("X-Public-Key", &self.public_key)
.header("X-Product-Slug", &self.product_slug)
.json(&request)
.send()
{
Ok(resp) => {
let status = resp.status();
let body = resp.text().unwrap_or_default();
if status.is_success() {
match serde_json::from_str::<CheckoutResult>(&body) {
Ok(mut result) => {
result.success = true;
result
}
Err(e) => CheckoutResult::failure(e.to_string()),
}
} else {
let error = serde_json::from_str::<ErrorResponse>(&body)
.map(|e| e.error)
.unwrap_or_else(|_| "Checkout failed".to_string());
CheckoutResult::failure(error)
}
}
Err(e) => CheckoutResult::failure(e.to_string()),
}
}
fn post<T: Serialize>(&self, path: &str, body: &T) -> LicenseResult {
match self
.http_client
.post(format!("{}{}", self.base_url, path))
.header("Content-Type", "application/json")
.header("X-Public-Key", &self.public_key)
.header("X-Product-Slug", &self.product_slug)
.json(body)
.send()
{
Ok(resp) => {
let status = resp.status();
let body = resp.text().unwrap_or_default();
if status.is_success() {
serde_json::from_str(&body).unwrap_or_else(|e| LicenseResult::failure(e.to_string()))
} else {
let error = serde_json::from_str::<ErrorResponse>(&body)
.map(|e| e.error)
.unwrap_or_else(|_| "Request failed".to_string());
LicenseResult::failure(error)
}
}
Err(e) => LicenseResult::failure(e.to_string()),
}
}
}

198
src/types.rs Normal file
View File

@ -0,0 +1,198 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// License status representing the current state of a license.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LicenseStatus {
Valid,
Expired,
Suspended,
Revoked,
Invalid,
Trial,
TrialExpired,
NotActivated,
#[serde(other)]
Unknown,
}
impl Default for LicenseStatus {
fn default() -> Self {
Self::NotActivated
}
}
/// License type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LicenseType {
Perpetual,
Subscription,
Trial,
}
impl Default for LicenseType {
fn default() -> Self {
Self::Perpetual
}
}
/// A feature included in a license.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Feature {
pub key: String,
pub name: String,
pub enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
/// License information.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct License {
pub id: String,
pub key: String,
pub status: LicenseStatus,
#[serde(rename = "type")]
pub license_type: LicenseType,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub company: Option<String>,
#[serde(default)]
pub features: Vec<Feature>,
#[serde(default)]
pub max_activations: i32,
#[serde(default)]
pub current_activations: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_validated_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
impl License {
/// Check if a feature is enabled.
pub fn has_feature(&self, feature_key: &str) -> bool {
self.features
.iter()
.any(|f| f.key == feature_key && f.enabled)
}
/// Get a feature by key.
pub fn get_feature(&self, feature_key: &str) -> Option<&Feature> {
self.features.iter().find(|f| f.key == feature_key)
}
}
/// An activation of a license on a machine.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Activation {
pub id: String,
pub machine_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub machine_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub activated_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_seen_at: Option<String>,
}
/// Result of a license validation or activation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseResult {
pub valid: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<License>,
#[serde(skip_serializing_if = "Option::is_none")]
pub activations: Option<Vec<Activation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default)]
pub cached: bool,
}
impl LicenseResult {
pub fn success(license: License) -> Self {
Self {
valid: true,
license: Some(license),
activations: None,
error: None,
cached: false,
}
}
pub fn failure(error: impl Into<String>) -> Self {
Self {
valid: false,
license: None,
activations: None,
error: Some(error.into()),
cached: false,
}
}
}
/// Result of starting a checkout.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckoutResult {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub checkout_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl CheckoutResult {
pub fn success(checkout_url: String, session_id: String) -> Self {
Self {
success: true,
checkout_url: Some(checkout_url),
session_id: Some(session_id),
error: None,
}
}
pub fn failure(error: impl Into<String>) -> Self {
Self {
success: false,
checkout_url: None,
session_id: None,
error: Some(error.into()),
}
}
}
/// A product tier available for purchase.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProductTier {
pub id: String,
pub slug: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub price: f64,
pub currency: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub billing_period: Option<String>,
#[serde(default)]
pub features: Vec<Feature>,
}