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:
parent
0cbc6afd53
commit
34842f5bd6
|
|
@ -0,0 +1,11 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
**/*.rs.bk
|
||||||
|
*.pdb
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.ironlicensing/
|
||||||
|
|
@ -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
308
README.md
|
|
@ -1,2 +1,306 @@
|
||||||
# ironlicensing-rust
|
# IronLicensing Rust SDK
|
||||||
IronLicensing SDK for Rust - Software licensing and activation
|
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>,
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue